Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions vocs/docs/pages/forge/reference/forge-lint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ exclude_lints = ["incorrect-shift"]

#### Medium Severity

##### divide-before-multiply
##### divide-before-multiply

Warns against performing division before multiplication within the same expression, especially with integer arithmetic.

In Solidity, integer division truncates (rounds down towards zero).
Performing division before multiplication can lead to a loss of precision that might be unintended and could have been avoided by reordering operations.
For example, `(a / b) * c` might result in `0` if `a < b`, even if `(a * c) / b` would have yielded a non-zero result.
For example, `(a / b) * c` might result in `0` if `a < b`, even if `(a * c) / b` would have yielded a non-zero result.

```solidity
// SPDX-License-Identifier: MIT
Expand Down
186 changes: 186 additions & 0 deletions vocs/docs/pages/guides/eip712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
## 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.

:::tip
Use the `forge eip712` command to generate the canonical type definitions and manually copy-paste them into your contracts. This way you will avoid typos.
:::

### 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.

```solidity
// 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:

```solidity
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");
}
}
```

:::note
If the library's `typeHash` was flawed, the assertion against the cheatcode would surface it.
:::

### 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.

```solidity
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");
}
}
```

:::note
If the library's `structHash` was flawed, the assertion against the cheatcode would surface it.
:::
2 changes: 1 addition & 1 deletion vocs/docs/pages/guides/foundry-in-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This guide shows you how to build, test, and deploy a smart contract using Found
The only installation required to run this guide is Docker, and optionally, an IDE of your choice.
Follow the [Docker installation instructions](/introduction/installation).

To keep future commands succinct, let's re-tag the image:
To keep future commands succinct, let's re-tag the image:
`docker tag ghcr.io/foundry-rs/foundry:latest foundry:latest`

Having Foundry installed locally is not strictly required, but it may be helpful for debugging. You can install it using [foundryup](/introduction/installation#using-foundryup).
Expand Down
1 change: 1 addition & 0 deletions vocs/docs/pages/introduction/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Comprehensive tutorials and best practices for building robust smart contracts a
- [Deterministic deployments using CREATE2](/guides/deterministic-deployments-using-create2) - Predictable contract addresses
- [Forking Mainnet with Cast and Anvil](/guides/forking-mainnet-with-cast-anvil) - Test against live chain state
- [Running Foundry inside of Docker](/guides/foundry-in-docker) - Containerized development environments
- [Implementing and Testing EIP-712 signatures](/guides/eip712)

### Project Setup

Expand Down
1 change: 1 addition & 0 deletions vocs/sidebar/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const sidebar: Sidebar = [
{ text: 'Project Layout', link: '/guides/project-setup/project-layout' }
]},
{ text: 'Scripting with Solidity', link: '/guides/scripting-with-solidity' },
{ text: 'Implementing and Testing EIP-712 signatures', link: '/guides/eip712' },
{ text: 'Deterministic deployments using CREATE2', link: '/guides/deterministic-deployments-using-create2' },
{ text: 'Forking Mainnet with Cast and Anvil', link: '/guides/forking-mainnet-with-cast-anvil' },
{ text: 'Running Foundry inside of Docker', link: '/guides/foundry-in-docker' },
Expand Down