-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: x402BatchSettlement contract #1950
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
Draft
CarsonRoscoe
wants to merge
22
commits into
x402-foundation:main
Choose a base branch
from
coinbase:feat/evm-contracts-batch-settlement
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
3854d4d
spec: added reference spec
CarsonRoscoe 1bdf093
feat: added x402BatchSettlement contract
CarsonRoscoe 298e965
feat: added tests
CarsonRoscoe 465a30e
feat: mining 0x4020...0003 vanity & deploy to base sepolia
CarsonRoscoe f5b4307
feat: updaded spec/contract per recommendations
CarsonRoscoe 20f47c4
feat: updated tests
CarsonRoscoe 646fb02
feat: update x402BatchSettlement
CarsonRoscoe da4298b
feat: add peripheral contracts
CarsonRoscoe 154d733
feat: updated spec
CarsonRoscoe 1181bad
feat: updated tests and deploy
CarsonRoscoe 1e9d806
feat: removed extra.receiver from spec
CarsonRoscoe a1a4408
spec v1.2 (#44)
phdargen 00d93b1
feat: added 4th deposit; renamed others
CarsonRoscoe d768c91
feat: updated tests
CarsonRoscoe 81638a4
feat: add collectors model
CarsonRoscoe 0638e2c
feat: removed periphery contracts
CarsonRoscoe 721145c
feat: rename cooperativeWithdraw to refund
CarsonRoscoe 9b8f795
feat: added multicall to x402BatchSettlement for migration support
CarsonRoscoe cf20c25
feat: added multichannel & migration test scenarios
CarsonRoscoe 3b577ae
feat: add deploy for peripery contracts
CarsonRoscoe aaa5ef7
feat: pr feedback
CarsonRoscoe b84cd24
feat: pr review feedback
CarsonRoscoe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {Script, console2} from "forge-std/Script.sol"; | ||
| import {x402BatchSettlement} from "../src/x402BatchSettlement.sol"; | ||
| import {Permit2DepositCollector} from "../src/periphery/Permit2DepositCollector.sol"; | ||
| import {Permit2WithERC2612DepositCollector} from "../src/periphery/Permit2WithERC2612DepositCollector.sol"; | ||
| import {ERC3009DepositCollector} from "../src/periphery/ERC3009DepositCollector.sol"; | ||
|
|
||
| /// @title DeployBatchSettlement | ||
| /// @notice Deployment script for x402BatchSettlement and deposit collectors using CREATE2 | ||
| /// @dev Run with: forge script script/DeployBatchSettlement.s.sol --rpc-url $RPC_URL --broadcast --verify | ||
| /// | ||
| /// Uses deterministic bytecode (cbor_metadata = false in foundry.toml) so | ||
| /// any machine compiling at the same git commit produces the same initCode | ||
| /// and therefore the same CREATE2 address. | ||
| contract DeployBatchSettlement is Script { | ||
| /// @notice Canonical Permit2 address (Uniswap's official deployment) | ||
| address constant CANONICAL_PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; | ||
|
|
||
| /// @notice Arachnid's deterministic CREATE2 deployer (same on all EVM chains) | ||
| address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; | ||
|
|
||
| bytes32 constant BATCH_SALT = 0x3ae0178c98d9f45c37d2b84ff96b0b02268269ea699dfbcb47374a79ef0156b0; | ||
| bytes32 constant ERC3009_SALT = 0xb2ec2379b202aede0729d830e1d0d267773fc2cac72c62f3225e29664737db12; | ||
| bytes32 constant PERMIT2_COLLECTOR_SALT = 0x111b85d49160296fc0fb956a78308c07197adb99d9bba5901e94e7c4929ec8e2; | ||
| bytes32 constant PERMIT2_ERC2612_SALT = 0x785692bab508580f81a234bd806b3044ef4da86fd9aaeb281f107bd49ed11859; | ||
|
|
||
| function run() public { | ||
| address permit2 = vm.envOr("PERMIT2_ADDRESS", CANONICAL_PERMIT2); | ||
|
|
||
| console2.log(""); | ||
| console2.log("============================================================"); | ||
| console2.log(" x402BatchSettlement Deterministic Deployment (CREATE2)"); | ||
| console2.log(" Model: Dual-Authorizer Channel-Config + Deposit Collectors"); | ||
| console2.log("============================================================"); | ||
| console2.log(""); | ||
|
|
||
| console2.log("Network: chainId", block.chainid); | ||
| console2.log("Permit2:", permit2); | ||
| console2.log("CREATE2 Deployer:", CREATE2_DEPLOYER); | ||
| console2.log(""); | ||
|
|
||
| if (block.chainid != 31_337 && block.chainid != 1337) { | ||
| require(permit2.code.length > 0, "Permit2 not found on this network"); | ||
| console2.log("Permit2 verified"); | ||
|
|
||
| require(CREATE2_DEPLOYER.code.length > 0, "CREATE2 deployer not found on this network"); | ||
| console2.log("CREATE2 deployer verified"); | ||
| } | ||
|
|
||
| _deploySettlement(); | ||
| _deployCollectors(permit2); | ||
|
|
||
| console2.log(""); | ||
| console2.log("Deployment complete!"); | ||
| console2.log(""); | ||
| } | ||
|
|
||
| function _deploySettlement() internal { | ||
| console2.log(""); | ||
| console2.log("------------------------------------------------------------"); | ||
| console2.log(" Deploying x402BatchSettlement"); | ||
| console2.log("------------------------------------------------------------"); | ||
|
|
||
| _deployCreate2("x402BatchSettlement", BATCH_SALT, type(x402BatchSettlement).creationCode); | ||
| } | ||
|
|
||
| function _deployCollectors( | ||
| address permit2 | ||
| ) internal { | ||
| console2.log(""); | ||
| console2.log("------------------------------------------------------------"); | ||
| console2.log(" Deploying Deposit Collectors"); | ||
| console2.log("------------------------------------------------------------"); | ||
|
|
||
| _deployCreate2("ERC3009DepositCollector", ERC3009_SALT, type(ERC3009DepositCollector).creationCode); | ||
|
|
||
| _deployCreate2( | ||
| "Permit2DepositCollector", | ||
| PERMIT2_COLLECTOR_SALT, | ||
| abi.encodePacked(type(Permit2DepositCollector).creationCode, abi.encode(permit2)) | ||
| ); | ||
|
|
||
| _deployCreate2( | ||
| "Permit2WithERC2612DepositCollector", | ||
| PERMIT2_ERC2612_SALT, | ||
| abi.encodePacked(type(Permit2WithERC2612DepositCollector).creationCode, abi.encode(permit2)) | ||
| ); | ||
| } | ||
|
|
||
| function _deployCreate2(string memory name, bytes32 salt, bytes memory initCode) internal { | ||
| bytes32 initCodeHash = keccak256(initCode); | ||
| address expectedAddress = _computeCreate2Addr(salt, initCodeHash, CREATE2_DEPLOYER); | ||
|
|
||
| console2.log(""); | ||
| console2.log(string.concat(" ", name)); | ||
| console2.log(" Salt:", vm.toString(salt)); | ||
| console2.log(" Expected address:", expectedAddress); | ||
|
|
||
| if (expectedAddress.code.length > 0) { | ||
| console2.log(" Already deployed, skipping"); | ||
| return; | ||
| } | ||
|
|
||
| vm.startBroadcast(); | ||
|
|
||
| if (block.chainid == 31_337 || block.chainid == 1337) { | ||
| console2.log(" (Local network - using regular CREATE)"); | ||
| assembly { | ||
| let addr := create(0, add(initCode, 0x20), mload(initCode)) | ||
| if iszero(addr) { revert(0, 0) } | ||
| } | ||
| } else { | ||
| bytes memory deploymentData = abi.encodePacked(salt, initCode); | ||
| (bool success,) = CREATE2_DEPLOYER.call(deploymentData); | ||
| require(success, string.concat("CREATE2 deployment failed for ", name)); | ||
| require(expectedAddress.code.length > 0, string.concat("No bytecode at expected address for ", name)); | ||
| } | ||
|
|
||
| vm.stopBroadcast(); | ||
|
|
||
| console2.log(" Deployed to:", expectedAddress); | ||
| } | ||
|
|
||
| function _computeCreate2Addr( | ||
| bytes32 salt, | ||
| bytes32 initCodeHash, | ||
| address deployer | ||
| ) internal pure returns (address) { | ||
| return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash))))); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| /// @title IDepositCollector | ||
| /// @notice Interface for pluggable deposit collectors used by x402BatchSettlement. | ||
| /// @dev Collectors handle the token transfer mechanics (ERC-3009, Permit2, etc.) | ||
| /// while the settlement contract verifies actual token receipt via balance checks. | ||
| /// Collectors MUST transfer tokens to msg.sender (the settlement contract). | ||
| interface IDepositCollector { | ||
| /// @notice Pull tokens from payer to the calling settlement contract. | ||
| /// @param payer The address that owns the tokens being deposited | ||
| /// @param token The ERC-20 token address | ||
| /// @param amount The exact amount of tokens to transfer | ||
| /// @param channelId The channel identifier (used by Permit2 collectors for witness binding) | ||
| /// @param caller The address that called deposit() on the settlement contract | ||
| /// @param collectorData Opaque bytes containing collector-specific parameters (signatures, nonces, etc.) | ||
| function collect( | ||
| address payer, | ||
| address token, | ||
| uint256 amount, | ||
| bytes32 channelId, | ||
| address caller, | ||
| bytes calldata collectorData | ||
| ) external; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| /** | ||
| * @title IERC3009 | ||
| * @notice Minimal interface for EIP-3009 transferWithAuthorization / receiveWithAuthorization | ||
| * @dev Used by tokens like USDC that support gasless transfers via signed authorizations. | ||
| * See https://eips.ethereum.org/EIPS/eip-3009 | ||
| */ | ||
| interface IERC3009 { | ||
| function receiveWithAuthorization( | ||
| address from, | ||
| address to, | ||
| uint256 value, | ||
| uint256 validAfter, | ||
| uint256 validBefore, | ||
| bytes32 nonce, | ||
| bytes memory signature | ||
| ) external; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IDepositCollector} from "../interfaces/IDepositCollector.sol"; | ||
|
|
||
| /// @title DepositCollector | ||
| /// @notice Abstract base for deposit collectors bound to a single x402BatchSettlement instance. | ||
| /// @dev All collectors MUST inherit this to ensure only the settlement contract can call collect(), | ||
| /// preventing frontrunning and fund theft. | ||
| abstract contract DepositCollector is IDepositCollector { | ||
| address public immutable settlement; | ||
|
|
||
| error OnlySettlement(); | ||
| error InvalidSettlementAddress(); | ||
|
|
||
| constructor( | ||
| address _settlement | ||
| ) { | ||
| if (_settlement == address(0)) revert InvalidSettlementAddress(); | ||
| settlement = _settlement; | ||
| } | ||
|
|
||
| modifier onlySettlement() { | ||
| if (msg.sender != settlement) revert OnlySettlement(); | ||
| _; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
|
||
| import {IDepositCollector} from "../interfaces/IDepositCollector.sol"; | ||
| import {DepositCollector} from "./DepositCollector.sol"; | ||
| import {IERC3009} from "../interfaces/IERC3009.sol"; | ||
|
|
||
| /// @title ERC3009DepositCollector | ||
| /// @notice Deposit collector that uses ERC-3009 receiveWithAuthorization for gasless token collection. | ||
| /// @dev ERC-3009 requires msg.sender == to, so this collector receives tokens first, then forwards | ||
| /// them to the settlement contract (msg.sender). This incurs an extra transfer (~30k gas overhead). | ||
| contract ERC3009DepositCollector is DepositCollector { | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| constructor( | ||
| address _settlement | ||
| ) DepositCollector(_settlement) {} | ||
|
|
||
| /// @inheritdoc IDepositCollector | ||
| function collect( | ||
| address payer, | ||
| address token, | ||
| uint256 amount, | ||
| bytes32, | ||
| address, | ||
| bytes calldata collectorData | ||
| ) external override onlySettlement { | ||
| (uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes memory signature) = | ||
| abi.decode(collectorData, (uint256, uint256, bytes32, bytes)); | ||
|
|
||
| IERC3009(token).receiveWithAuthorization( | ||
| payer, address(this), amount, validAfter, validBefore, nonce, signature | ||
| ); | ||
|
|
||
| IERC20(token).safeTransfer(msg.sender, amount); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IDepositCollector} from "../interfaces/IDepositCollector.sol"; | ||
| import {ISignatureTransfer} from "../interfaces/ISignatureTransfer.sol"; | ||
| import {Permit2DepositCollectorBase} from "./Permit2DepositCollectorBase.sol"; | ||
|
|
||
| /// @title Permit2DepositCollector | ||
| /// @notice Deposit collector that uses Permit2 permitWitnessTransferFrom with channelId witness binding. | ||
| /// @dev Tokens flow directly from payer to settlement via Permit2 (no intermediate hop). | ||
| /// The payer's Permit2 signature must name this collector as the spender. | ||
| contract Permit2DepositCollector is Permit2DepositCollectorBase { | ||
| constructor(address _settlement, address _permit2) Permit2DepositCollectorBase(_settlement, _permit2) {} | ||
|
|
||
| /// @inheritdoc IDepositCollector | ||
| function collect( | ||
| address payer, | ||
| address, | ||
| uint256 amount, | ||
| bytes32 channelId, | ||
| address, | ||
| bytes calldata collectorData | ||
| ) external override onlySettlement { | ||
| (ISignatureTransfer.PermitTransferFrom memory permit, bytes memory signature) = | ||
| abi.decode(collectorData, (ISignatureTransfer.PermitTransferFrom, bytes)); | ||
|
|
||
| _executePermit2Transfer(payer, amount, channelId, permit, signature); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CRITICAL: This function is permissionless and lets anyone choose the recipient, letting anyone frontrun transactions and steal funds. This needs to be bound to the BatchSettlement singleton and only forward funds there and also lock down the caller to the singleton too. Same for all other collectors and would recommend making an abstract
DepositCollectorfor this purpose.