Skip to content
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d10fa74
feat: added and implemented @x402/evm-contracts
CarsonRoscoe Jan 7, 2026
fe637d2
feat: initial draft of extensions
CarsonRoscoe Jan 9, 2026
82e20c7
feat: added contracts/evm foundry project
CarsonRoscoe Jan 12, 2026
92d523b
chore: removed @x402/evm-contracts (as contracts/evm replaced)
CarsonRoscoe Jan 12, 2026
e1c018a
feat: add foundry lib dependencies as git submodules
CarsonRoscoe Jan 14, 2026
7ffbee8
feat: pr feedback
CarsonRoscoe Jan 14, 2026
75d3dd1
feat: forge ci
CarsonRoscoe Jan 14, 2026
38066f2
fix: reduced scope of permissions for workflow
CarsonRoscoe Jan 14, 2026
1a2cb3c
fix: lockfiles
CarsonRoscoe Jan 14, 2026
7674e49
fix: update foundry version in ci from nightly to current
CarsonRoscoe Jan 14, 2026
2b03486
feat: sha pin the foundry-rs/foundry-toolchain version in ci
CarsonRoscoe Jan 14, 2026
b0a2b10
fix: lint
CarsonRoscoe Jan 14, 2026
5801c26
feat: cleaned up tests
CarsonRoscoe Jan 15, 2026
7249abb
feat: replace X402PermitTransfer event with Settled and SettledWith26…
CarsonRoscoe Jan 15, 2026
c3e234e
feat: refactor x402Permit2Proxy into x402ExactPermit2Proxy and x402Up…
CarsonRoscoe Jan 15, 2026
84ba46f
feat: removed extraneous test
CarsonRoscoe Jan 15, 2026
23b60f6
feat: add @x402/extensions exports & unit tests for gasless extensions
CarsonRoscoe Jan 20, 2026
ec11c73
feat: extract out common base from exact and upto contracts
CarsonRoscoe Jan 20, 2026
5a973b7
feat: mined vanity addresses & formatted solidity
CarsonRoscoe Jan 20, 2026
082614e
feat: removed validBefore from witness; Permit2 deadline enforces upp…
CarsonRoscoe Jan 21, 2026
9ecccb5
fix: transient network issue with forge checks in ci
CarsonRoscoe Jan 21, 2026
a513eb5
fix: normalize etherscan api keys for all networks
CarsonRoscoe Jan 21, 2026
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
47 changes: 47 additions & 0 deletions .github/workflows/forge_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Forge CI

permissions:
contents: read

on:
pull_request:
branches:
- main
paths:
- 'contracts/evm/**'

env:
FOUNDRY_PROFILE: ci

jobs:
forge-checks:
name: Forge Build, Test & Format
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts/evm
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@8b0419c685ef46cb79ec93fbdc131174afceb730 # v1.6.0
with:
version: v1.2.2

- name: Show Forge version
run: forge --version

- name: Run Forge build
run: forge build --sizes
id: build

- name: Run Forge tests
run: forge test -vvv
id: test

- name: Check formatting
run: forge fmt --check
id: fmt

6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "contracts/evm/lib/forge-std"]
path = contracts/evm/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/evm/lib/openzeppelin-contracts"]
path = contracts/evm/lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
32 changes: 32 additions & 0 deletions contracts/evm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Compiler output
out/
cache/

# Environment
.env
.env.local

# Coverage
lcov.info
coverage/

# Gas snapshots (optional - can commit for tracking)
# .gas-snapshot

# IDE
.idea/
.vscode/

# OS
.DS_Store
Thumbs.db

# Broadcast files (deployment artifacts)
broadcast/

# Debug
debug/


# Rust vanity-miner build artifacts
vanity-miner/target/
214 changes: 214 additions & 0 deletions contracts/evm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# x402 EVM Contracts

Smart contracts for the x402 payment protocol on EVM chains.

## Overview

The x402 Permit2 Proxy contracts enable trustless, gasless payments using [Permit2](https://github.com/Uniswap/permit2). There are two variants:

### `x402ExactPermit2Proxy`
Transfers the **exact** permitted amount (similar to EIP-3009's `transferWithAuthorization`). The facilitator cannot choose a different amount—it's always the full permitted amount.

### `x402UptoPermit2Proxy`
Allows the facilitator to transfer **up to** the permitted amount. Useful for scenarios where the actual amount is determined at settlement time.

Both contracts:
- Use the **witness pattern** to cryptographically bind payment destinations
- Prevent facilitators from redirecting funds
- Support both standard Permit2 and EIP-2612 flows
- Deploy to the **same address on all EVM chains** via CREATE2

## Prerequisites

- [Foundry](https://book.getfoundry.sh/getting-started/installation)

## Installation

```bash
# Install dependencies
forge install

# Build contracts
forge build
```

## Testing

```bash
# Run all tests
forge test

# Run with verbosity
forge test -vvv

# Run Exact proxy tests
forge test --match-contract X402ExactPermit2ProxyTest

# Run Upto proxy tests
forge test --match-contract X402UptoPermit2ProxyTest

# Run with gas reporting
forge test --gas-report

# Run fuzz tests with more runs
forge test --fuzz-runs 1000

# Run invariant tests
forge test --match-contract Invariants
```

### Fork Testing

Fork tests run against real Permit2 on Base Sepolia:

```bash
# Set up environment
export BASE_SEPOLIA_RPC_URL="https://sepolia.base.org"

# Run fork tests for Exact variant
forge test --match-contract X402ExactPermit2ProxyForkTest --fork-url $BASE_SEPOLIA_RPC_URL

# Run fork tests for Upto variant
forge test --match-contract X402UptoPermit2ProxyForkTest --fork-url $BASE_SEPOLIA_RPC_URL
```

## Deployment

### Compute Expected Addresses

```bash
forge script script/ComputeAddress.s.sol
```

### Deploy to Testnet

```bash
# Set environment variables
export PRIVATE_KEY="your_private_key"
export BASE_SEPOLIA_RPC_URL="https://sepolia.base.org"
export BASESCAN_API_KEY="your_api_key"

# Deploy both contracts with verification
forge script script/Deploy.s.sol \
--rpc-url $BASE_SEPOLIA_RPC_URL \
--broadcast \
--verify
```

### Deploy to Mainnet

```bash
export BASE_RPC_URL="https://mainnet.base.org"

forge script script/Deploy.s.sol \
--rpc-url $BASE_RPC_URL \
--broadcast \
--verify
```

## Vanity Address Mining

The deployment uses vanity addresses starting with `0x4020`. To mine new salts:

```bash
# Simple Solidity miner (slower)
forge script script/MineVanity.s.sol

# For faster mining, use create2crunch or the TypeScript miner
```

## Contract Architecture

```
src/
├── x402ExactPermit2Proxy.sol # Exact amount transfers (EIP-3009-like)
├── x402UptoPermit2Proxy.sol # Flexible amount transfers (up to permitted)
└── interfaces/
└── ISignatureTransfer.sol # Permit2 SignatureTransfer interface

test/
├── x402ExactPermit2Proxy.t.sol # Exact variant unit tests
├── x402ExactPermit2Proxy.fork.t.sol # Exact variant fork tests
├── x402UptoPermit2Proxy.t.sol # Upto variant unit tests
├── x402UptoPermit2Proxy.fork.t.sol # Upto variant fork tests
├── invariants/
│ ├── X402ExactInvariants.t.sol # Exact variant invariant tests
│ └── X402UptoInvariants.t.sol # Upto variant invariant tests
└── mocks/
├── MockERC20.sol
├── MockERC20Permit.sol
├── MockPermit2.sol
├── MaliciousReentrantExact.sol
└── MaliciousReentrantUpto.sol

script/
├── Deploy.s.sol # CREATE2 deployment for both contracts
├── ComputeAddress.s.sol # Address computation for both contracts
└── MineVanity.s.sol # Vanity address miner for both contracts
```

## Key Functions

### `x402ExactPermit2Proxy.settle()`

Standard settlement path - always transfers the exact permitted amount.

```solidity
function settle(
ISignatureTransfer.PermitTransferFrom calldata permit,
address owner,
Witness calldata witness,
bytes calldata signature
) external;
```

### `x402UptoPermit2Proxy.settle()`

Standard settlement path - transfers the specified amount (up to permitted).

```solidity
function settle(
ISignatureTransfer.PermitTransferFrom calldata permit,
uint256 amount, // Facilitator specifies amount to transfer
address owner,
Witness calldata witness,
bytes calldata signature
) external;
```

### `settleWithPermit()`

Both contracts support settlement with EIP-2612 permit for fully gasless flow.
The function signatures follow the same pattern as `settle()` for each variant.

## Security

- **Immutable:** No upgrade mechanism
- **No custody:** Contracts never hold tokens
- **Destination locked:** Witness pattern enforces payTo address
- **Reentrancy protected:** Uses OpenZeppelin's ReentrancyGuard
- **Deterministic:** Same address on all chains via CREATE2

## Coverage

```bash
# Full coverage report (includes test/script files)
forge coverage

# Coverage for src/ contracts only (excludes mocks, tests, scripts)
forge coverage --no-match-coverage "(test|script)/.*" --offline
```

## Gas Snapshots

```bash
# Create snapshot
forge snapshot

# Compare against baseline
forge snapshot --diff
```

## License

Apache-2.0
51 changes: 51 additions & 0 deletions contracts/evm/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
test = "test"
script = "script"
solc = "0.8.28"
optimizer = true
optimizer_runs = 200
via_ir = false
ffi = false
fs_permissions = [{ access = "read", path = "./" }]
gas_reports = ["x402ExactPermit2Proxy", "x402UptoPermit2Proxy"]

[profile.default.fuzz]
runs = 256
max_test_rejects = 65536

[profile.default.invariant]
runs = 256
depth = 50
fail_on_revert = false

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
base = "${BASE_RPC_URL}"
optimism = "${OPTIMISM_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"
polygon = "${POLYGON_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
optimism_sepolia = "${OPTIMISM_SEPOLIA_RPC_URL}"
arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"

[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
base = { key = "${BASESCAN_API_KEY}" }
base_sepolia = { key = "${BASESCAN_API_KEY}" }
optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}" }
arbitrum = { key = "${ARBISCAN_API_KEY}" }

[fmt]
line_length = 120
tab_width = 4
bracket_spacing = false
int_types = "long"
multiline_func_header = "params_first"
quote_style = "double"
number_underscore = "thousands"
single_line_statement_blocks = "preserve"

1 change: 1 addition & 0 deletions contracts/evm/lib/forge-std
Submodule forge-std added at 1801b0
1 change: 1 addition & 0 deletions contracts/evm/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at fcbae5
4 changes: 4 additions & 0 deletions contracts/evm/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
forge-std/=lib/forge-std/src/
permit2/=lib/permit2/src/

Loading
Loading