Skip to content

Implementing and Testing EIP-712 signatures

Foundry offers multiple utilities to make it easy and reliable to work with EIP-712 signatures.

EIP-712 is a standard for hashing and signing typed structured data. Instead of signing an opaque hash, users can sign human-readable messages, significantly improving usability and security. This is particularly useful for meta-transactions, permit functions (like in ERC-20 permits), and other off-chain signature schemes. However, correctly implementing EIP-712 hashing logic can be intricate. Foundry's suite provides powerful utilities specifically designed to help developers test and validate their EIP-712 implementations with confidence.

This guide will show you how to leverage Foundry's EIP-712 commands and cheatcodes with a practical, real-world example, demonstrating how to validate a complex library like Uniswap's PermitHash.sol from their Permit2 system. This will showcase how to ensure that a custom EIP-712 hashing implementation aligns perfectly with the standard.

EIP-712 commands

Forge offers a couple of commands which are useful when working with EIP-712 types:

forge eip712

Outputs the canonical type definitions of the structs in the target files in the terminal.

forge bind-json

Automatically generates solidity bindings for the structs in the target files. The generated bindings can easily be serialized to JSON strings, and also parsed from JSON strings. Additionally, these bindings also allow the EIP-712 cheatcodes to derive the type definitions just their name.

EIP-712 cheatcodes

Foundry offers several cheatcodes to interact with EIP-712 types:

vm.eip712HashType

  • Generates the typeHash for an EIP-712 struct definition. This is keccak256 of the canonical type encoding.
  • It can take a direct string definition (i.e. "Mail(address from,string contents)") or a type name if you've used forge bind-json to generate bindings from your Solidity structs.

vm.eip712HashStruct

  • Computes the structHash: keccak256(typeHash + encodeData(struct)).
  • encodeData(struct) is the ABI-encoded values of the struct's members.
  • Like vm.eip712HashType, it accepts either a direct type definition string or a type name (with bindings).

vm.eip712HashTypedData

  • Generates the final EIP-712 digest to be signed: keccak256("\x19\x01" + domainSeparator + structHash).
  • It takes a full JSON string representing the typed data as per the EIP-712 specification. Useful for end-to-end testing of signature verification.

Testing Uniswap's PermitHash library

Uniswap's Permit2 system utilizes the PermitHash.sol library to create hashes that comply with the EIP-712 standard for various permit structures. In this guide, we will demonstrate how to use Foundry to verify that the library correctly implements the EIP-712 hashing rules.

Our objective is to focus on a few hashing functions within PermitHash.sol. We will provide these functions with sample data and then use vm.eip712HashStruct —with the same data and the canonical EIP-712 type definition— to determine if the generated hashes match.

Setting up the test environment

Before starting with the validations, we have to create the PermitHash.t.sol test file.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
 
import "forge-std/Test.sol";
// Import the library we are testing
import {PermitHash} from "src/libraries/PermitHash.sol";
import {IAllowanceTransfer as IAT} from "src/interfaces/IAllowanceTransfer.sol";
 
/* These are the structs, defined in `IAT`, that `PermitHash` relies on:
 
struct PermitDetails {
  address token;
  uint160 amount;
  uint48 expiration;
  uint48 nonce;
}
 
struct PermitSingle {
  PermitDetails details;
  address spender;
  uint256 sigDeadline;
}
*/

Tip: as previously explained, you can use forge bind-json to leverage Foundry's capabilities, and have higher guarantees when testing. By running that command, you can simply use the struct name when using the EIP-712 cheatcodes, and Foundry will automatically derive the canonical type definition.

Validating typHash

First of all, ensure that the type hashes for each of the structs are correct:

contract PermitHashTest is Test {
  function test_validatePermitDetails_typeHash() public {
    // This test doesn't rely on the bindings generated by `forge json`, therefore it requires
    // the string representation of the type as an input for the cheatcode.
 
    // Assume available on Uniswap's library. Otherwise you'd have to copy-paste it manually.
    string memory _PERMIT_DETAILS_TYPEDEF =
      "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)";
 
    // The type hash constant defined in Uniswap's library
    bytes32 typeHash = PermitHash._PERMIT_DETAILS_TYPEHASH;
 
    // Use the cheatcode to get the expected hash (with string representation)
    bytes32 expected = vm.eip712HashType(_PERMIT_DETAILS_TYPEDEF);
 
    assertEq(typeHash, expected, "PermitDetails typeHash mismatch");
  }
 
  function test_validatePermitSingle_typeHash() public {
    // The type hash constant defined in Uniswap's library
    bytes32 typeHash = PermitHash._PERMIT_SINGLE_TYPEHASH;
 
    // Use the cheatcode to get the expected hash (needs bindings)
    bytes32 expected = vm.eip712HashType("PermitSingle");
 
    assertEq(typeHash, expected, "PermitSingle typeHash mismatch");
  }
}

Validating structHash

After being certain that the hashes of the type definitions are correct, let's validate that the hashes of the structs follow the EIP-712 specification.

contract PermitHashTest is Test {
  address TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
  address SPENDER = 0xdEADBEeF00000000000000000000000000000000;
 
  function test_validatePermitDetails_structHash() public {
    // This test doesn't rely on the bindings generated by `forge bind-json`, therefore it requires
    // the string representation of the type as an input for the cheatcode.
 
    // Assume available on Uniswap's library. Otherwise you'd have to copy-paste it manually.
    string memory _PERMIT_DETAILS_TYPEDEF =
      "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)";
 
    // Prepare the test data for PermitDetails
    IAllowanceTransfer.PermitDetails memory details = IAllowanceTransfer.PermitDetails({
      token: TOKEN,
      amount: 100 ether,
      expiration: uint48(block.timestamp + 3600),
      nonce: 123
    });
 
    // Get the structHash from Uniswap's library.
    // Despite private, assume it is available with a public function.
    bytes32 structHash = PermitHash._hashPermitDetails(details);
 
    // Use the cheatcode to get the expected hash (with string representation)
    bytes32 expected = vm.eip712HashStruct(_PERMIT_DETAILS_TYPEDEF, abi.encode(details));
 
    assertEq(structHash, expected, "PermitDetails structHash mismatch");
  }
 
  function test_validatePermitSingle_structHash() public {
    IAT.PermitDetails memory details = IAT.PermitDetails({
      token: TOKEN,
      amount: 200 ether,
      expiration: uint48(block.timestamp + 7200),
      nonce: 456
    });
 
    IAT.PermitSingle memory permitSingle = IAT.PermitSingle({
      details: details,
      spender: SPENDER,
      sigDeadline: block.timestamp + 10800
    });
 
    // Get the structHash from Uniswap's library.
    bytes32 structHash = PermitHash.hash(permitSingle);
 
    // Use the cheatcode to get the expected hash (needs bindings)
    bytes32 expectedStructHash = vm.eip712HashStruct("PermitSingle", abi.encode(permitSingle));
 
    assertEq(structHash, expected, "PermitSingle structHash mismatch");
  }
}