foundry-rs / foundry

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.
https://getfoundry.sh
Apache License 2.0
8.35k stars 1.77k forks source link

cast/forge: preserve contract types when generating interfaces from ABI #8837

Open mds1 opened 2 months ago

mds1 commented 2 months ago

Component

Forge

Have you ensured that all of these are up to date?

What version of Foundry are you on?

forge 0.2.0 (143abd6 2024-09-04T00:24:41.963834000Z)

What command(s) is the bug in?

cast interface / forge inspect

Operating System

None

Describe the bug

To reproduce, forge init a new project and add this contract:

contract Foo {
  function bar(Counter x) public view returns (Counter y) {
    return x;
  }
}

When generating an interface, the function inputs and return type both are converted to address, which strips information. You can see the Counter type is present in the ABI of Foo:

"abi": [
  {
    "type": "function",
    "name": "bar",
    "inputs": [
      {
        "name": "x",
        "type": "address",
        "internalType": "contract Counter"
      }
    ],
    "outputs": [
      {
        "name": "y",
        "type": "address",
        "internalType": "contract Counter"
      }
    ],
    "stateMutability": "view"
  }
],

But when generating interfaces:

$ forge inspect Foo abi --pretty
interface Foo {
    function bar(address x) external view returns (address y);
}

$ cast interface Foo
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

interface Foo {
    function bar(address x) external view returns (address y);
}

I can see the rationale for this being that forge might not know what path to use for the Counter import. However, even if no import statement is provided, I would still prefer a stronger version of interface generation that preserves the internal Counter type. Perhaps this should be behind a --preserve-internal-types flag

cc @smartcontracts

yash-atreya commented 2 months ago

Solidity contract types are mapped to address in the ABI. https://docs.soliditylang.org/en/v0.8.27/abi-spec.html#mapping-solidity-to-abi-types

alloy-json-abi handles it accordingly https://github.com/alloy-rs/core/blob/022c63db87b6bca348dd8e55f86c94a614b99cb6/crates/json-abi/src/to_sol.rs#L564

cc @DaniPopes

DaniPopes commented 2 months ago

There is no information about Counter in Foo, so I don't see the point in preserving this since we would have to generate an empty Counter interface; in any case they're ABI-equivalent

mds1 commented 2 months ago

Right, forge would generate an incomplete interface, and the user would have to fix it by importing Counter from the proper path. The value here is that having Counter in the interface is a stronger type than address, and currently the user must manually fix the interface to preserve the stronger typing. This manual fix is tedious and error prone for large interfaces

smartcontracts commented 2 months ago

I think you'd need to use the AST to figure this out correctly. My ideal version of this would generate a full interface file complete with imports, e.g.:

// Foo.sol
contract Foo {
  function bar(Counter x) public view returns (Counter y) {
    return x;
  }
}
// Counter.sol
contract Counter {
  function whatever() public view returns (whatever) {
    return whatever;
  }
}

Ideal generated file:

// IFoo.sol

import { Counter } from "./Counter.sol";

interface IFoo {
  function bar(Counter x) external view returns (Counter y);
}