Skip to content

feat(forge): cheatcode eip712 #10570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 29, 2025
Merged

Conversation

0xrusowsky
Copy link
Contributor

@0xrusowsky 0xrusowsky commented May 20, 2025

Adds a new cheatcode to safely generate hashes following the EIP-712 spec.

implementation details

ref #4818

due to the lack of polymorphism, it is not possible for the cheatcode to accept a struct. Because of that, users must provide a string.

this impl allows users to either pass:

  1. the type name, which will we will attempt to retrieve from the bindings generated by forge bind-json (recommended)
  2. the full string representation of the type definition. In this case, we leverage alloy's eip712 support to generate the canonical representation of the type even if users pass malformed types (unsorted or with extra whitespaces).

context

usually developers manually set up constants on their smart contracts to easily access the EIP-712 type hashes:

library PermitHash {
    bytes32 public constant _PERMIT_DETAILS_TYPEHASH =
        keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)");

    bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256(
        "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
    );

    bytes32 public constant _PERMIT_BATCH_TYPEHASH = keccak256(
        "PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
    );
}

because of that, it is extremely important for these hashes to be canonical (properly computed following the EIP spec). However, as any manual approach this is error prone.

this new cheatcode aims to provide robustness to the codebases, and peace of mind for developers, giving them guarantees that no typos or sorting errors where made when manually generating the type definitions.

example usage

// represents a well-formatted EIP-712 type definition
string constant CANONICAL = "PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)";

// represents a mal-formatted EIP-712 type definition, emulating a human error
string constant MESSY = "PermitDetails(address token, uint160 amount, uint48 expiration, uint48 nonce) PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)";

contract Eip712Test is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);

    function testEip712HashType() public {
        bytes32 canonicalHash = keccak256(bytes(CANONICAL));

        // Can figure out the canonical type from a messy string representation of the type,
        // with an invalid order and extra whitespaces
        bytes32 fromTypeDef = vm.eip712HashType(MESSY);
        assertEq(fromTypeDef, canonicalHash);

        // Can figure out the canonical type from the previously generated bindings with `forge bind-json`
        bytes32 fromTypeName = vm.eip712HashType("PermitBatch");
        assertEq(fromTypeName, canonicalHash);

        // In practice, this means that devs only need to compare the hash with the cheatcode output
        assertEq(keccak256(bytes(MESSY)), vm.eip712HashType(MESSY));          // this check would fail
        assertEq(keccak256(bytes(MESSY)), vm.eip712HashType("PermitBatch"));  // this check would fail
    }
}

important note

when using vm.⁠eip712HashType:

  • Passing a type string representation: validates EIP-712 formatting, canonicalizing messy inputs. However, it does NOT guarantee the string perfectly matches the smart contract's struct. For instance, a typo like ⁠amont instead of ⁠amount in the string will still produce a valid EIP-712 hash, but for the incorrect type, leading to runtime signature verification errors.
  • Passing a type name: (RECOMMENDED METHOD) uses ⁠forge bind-json generated bindings, ensuring the hash corresponds to your actual compiled contract structs. This provides a guarantee of alignment, catching discrepancies that the string method would miss.

@0xrusowsky 0xrusowsky added this to the v1.3.0 milestone May 20, 2025
@0xrusowsky 0xrusowsky self-assigned this May 20, 2025
@0xrusowsky 0xrusowsky added the A-cheatcodes Area: cheatcodes label May 20, 2025
@0xrusowsky 0xrusowsky requested a review from zerosnacks as a code owner May 20, 2025 15:45
@0xrusowsky 0xrusowsky linked an issue May 20, 2025 that may be closed by this pull request
@0xrusowsky 0xrusowsky changed the base branch from master to feat/eip712-with-solar May 20, 2025 15:47
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please run cargo cheats twice to fix failing tests

Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, left couple of comments / nits!
So if I get this right the DX would be:

  • generate bindings with forge bind-json (by default goes to utils/JsonBindings.sol but could be configurable to different pathToJsonBindings path)
  • use vm.eip712HashType(typeDefinition) or vm.eip712HashType(pathToJsonBindings, typeDefinition) to generate EIP-712 representation

@grandizzy grandizzy self-requested a review May 22, 2025 14:17
@grandizzy grandizzy self-requested a review May 22, 2025 15:25
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, just realized params should be calldata and not memory

@Philogy
Copy link
Contributor

Philogy commented May 23, 2025

Getting the type string right is only half the battle, the other is also encoding/hashing. Would be nice if you had a json style chteatcode where you pass in the abi encoded bytes + the type name and you construct the EIP-712 bytes to be hashed to get the struct hash (including the typehash).

As an auditor/dev I see people messing up the hashing of sub-collections / arrays in EIP-712 all the time would be nice to have a good way to verify/compare this.

@0xrusowsky 0xrusowsky merged commit 9f0b62c into feat/eip712-with-solar May 29, 2025
22 checks passed
@0xrusowsky 0xrusowsky deleted the chore/cheat-eip712 branch May 29, 2025 20:32
@github-project-automation github-project-automation bot moved this from Ready For Review to Done in Foundry May 29, 2025
@grandizzy grandizzy moved this from Done to Completed in Foundry Jun 3, 2025
grandizzy added a commit that referenced this pull request Jun 4, 2025
…solar (#10510)

* wip

* feat: eip712 type hash PoC

* style: json

* style: json

* style: json

* style: comments

* wip

* initial impl using solar

* fix: untracked change

* fix: optimize resolve_type

* initial working impl

* feat: eip712 solar resolver

* style: docs + fmt + clippy

* todo: cheatcode

* docs: comments

* fix: use HIR rather than AST

* from build opts

* docs

* fix: rmv hashset

* create utils for solar_pcx_from_build_opts

* incorporate version logic into `solar_pcx_from_build_opts`

* wip bind-json: eip712 resolver integration

* forge(bind-json): integrate solar

* fix: tests

* style: clippy

* undo cheatcode setup (will tackle it on its own PR)

* rmv old test

* style: fix typo

* fix: win path

* fix: merge conflicts

* fix: dani's feedback

* docs: explain bindings overriding

* chore: patch solar

* feat(forge):  eip712 cheatcodes (#10570)

* fix: bump solang parser

* Remove unused from forge crate

* Move tests to eip712

* Nit: comments

---------

Co-authored-by: grandizzy <grandizzy.the.egg@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-cheatcodes Area: cheatcodes
Projects
Status: Completed
Development

Successfully merging this pull request may close these issues.

5 participants