From 19b19d5e16e044b4557c4fa70089118f8c66188d Mon Sep 17 00:00:00 2001 From: app Date: Mon, 15 Jun 2026 14:44:30 +0800 Subject: [PATCH 01/12] add new x402 contract --- contracts/interfaces/IDepositCollector.sol | 23 + contracts/interfaces/IERC3009.sol | 20 + contracts/interfaces/ISignatureTransfer.sol | 96 ++ contracts/{ => legacy}/EIP712.sol | 0 contracts/{ => legacy}/PaymentPermit.sol | 0 contracts/{ => legacy}/interface/IEIP712.sol | 0 .../{ => legacy}/interface/IPaymentPermit.sol | 0 .../{ => legacy}/libraries/PermitHash.sol | 0 contracts/mocks/MockGenericERC20.sol | 23 + contracts/periphery/DepositCollector.sol | 36 + .../periphery/ERC3009DepositCollector.sol | 52 + .../periphery/Permit2DepositCollector.sol | 134 ++ contracts/x402BasePermit2Proxy.sol | 172 ++ contracts/x402BatchSettlement.sol | 587 +++++++ contracts/x402ExactPermit2Proxy.sol | 106 ++ contracts/x402UptoPermit2Proxy.sol | 105 ++ hardhat.config.ts | 1 + package-lock.json | 444 ++++- package.json | 3 + test/MockGenericERC20.t.sol | 18 + test/invariants/X402ExactInvariants.t.sol | 104 ++ test/invariants/X402UptoInvariants.t.sol | 105 ++ test/{ => legacy}/MockERC20.sol | 0 test/{ => legacy}/PaymentPermit.t.sol | 6 +- test/mocks/GasMockDepositCollector.sol | 14 + test/mocks/MaliciousReentrantExact.sol | 74 + test/mocks/MaliciousReentrantUpto.sol | 79 + test/mocks/MockBatchDepositCollectors.sol | 23 + test/mocks/MockERC20.sol | 20 + test/mocks/MockERC20Permit.sol | 58 + test/mocks/MockERC3009Token.sol | 36 + test/mocks/MockPermit2.sol | 46 + test/periphery/ERC3009DepositCollector.t.sol | 76 + test/periphery/Permit2DepositCollector.t.sol | 107 ++ .../Permit2WithERC2612DepositCollector.t.sol | 120 ++ test/x402BatchSettlement.fork.t.sol | 320 ++++ test/x402BatchSettlement.gas.t.sol | 278 +++ test/x402BatchSettlement.t.sol | 1535 +++++++++++++++++ test/x402ExactPermit2Proxy.fork.t.sol | 223 +++ test/x402ExactPermit2Proxy.t.sol | 459 +++++ test/x402UptoPermit2Proxy.fork.t.sol | 253 +++ test/x402UptoPermit2Proxy.t.sol | 651 +++++++ 42 files changed, 6331 insertions(+), 76 deletions(-) create mode 100644 contracts/interfaces/IDepositCollector.sol create mode 100644 contracts/interfaces/IERC3009.sol create mode 100644 contracts/interfaces/ISignatureTransfer.sol rename contracts/{ => legacy}/EIP712.sol (100%) rename contracts/{ => legacy}/PaymentPermit.sol (100%) rename contracts/{ => legacy}/interface/IEIP712.sol (100%) rename contracts/{ => legacy}/interface/IPaymentPermit.sol (100%) rename contracts/{ => legacy}/libraries/PermitHash.sol (100%) create mode 100644 contracts/mocks/MockGenericERC20.sol create mode 100644 contracts/periphery/DepositCollector.sol create mode 100644 contracts/periphery/ERC3009DepositCollector.sol create mode 100644 contracts/periphery/Permit2DepositCollector.sol create mode 100644 contracts/x402BasePermit2Proxy.sol create mode 100644 contracts/x402BatchSettlement.sol create mode 100644 contracts/x402ExactPermit2Proxy.sol create mode 100644 contracts/x402UptoPermit2Proxy.sol create mode 100644 test/MockGenericERC20.t.sol create mode 100644 test/invariants/X402ExactInvariants.t.sol create mode 100644 test/invariants/X402UptoInvariants.t.sol rename test/{ => legacy}/MockERC20.sol (100%) rename test/{ => legacy}/PaymentPermit.t.sol (98%) create mode 100644 test/mocks/GasMockDepositCollector.sol create mode 100644 test/mocks/MaliciousReentrantExact.sol create mode 100644 test/mocks/MaliciousReentrantUpto.sol create mode 100644 test/mocks/MockBatchDepositCollectors.sol create mode 100644 test/mocks/MockERC20.sol create mode 100644 test/mocks/MockERC20Permit.sol create mode 100644 test/mocks/MockERC3009Token.sol create mode 100644 test/mocks/MockPermit2.sol create mode 100644 test/periphery/ERC3009DepositCollector.t.sol create mode 100644 test/periphery/Permit2DepositCollector.t.sol create mode 100644 test/periphery/Permit2WithERC2612DepositCollector.t.sol create mode 100644 test/x402BatchSettlement.fork.t.sol create mode 100644 test/x402BatchSettlement.gas.t.sol create mode 100644 test/x402BatchSettlement.t.sol create mode 100644 test/x402ExactPermit2Proxy.fork.t.sol create mode 100644 test/x402ExactPermit2Proxy.t.sol create mode 100644 test/x402UptoPermit2Proxy.fork.t.sol create mode 100644 test/x402UptoPermit2Proxy.t.sol diff --git a/contracts/interfaces/IDepositCollector.sol b/contracts/interfaces/IDepositCollector.sol new file mode 100644 index 0000000..7208e24 --- /dev/null +++ b/contracts/interfaces/IDepositCollector.sol @@ -0,0 +1,23 @@ +// 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 `x402BatchSettlement` verifies actual token receipt via balance checks. +/// Collectors MUST transfer tokens to `msg.sender` (the calling `x402BatchSettlement`). +interface IDepositCollector { + /// @notice Pull tokens from payer to the calling `x402BatchSettlement` 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 collectorData Opaque bytes containing collector-specific parameters (signatures, nonces, etc.) + function collect( + address payer, + address token, + uint256 amount, + bytes32 channelId, + bytes calldata collectorData + ) external; +} diff --git a/contracts/interfaces/IERC3009.sol b/contracts/interfaces/IERC3009.sol new file mode 100644 index 0000000..891b811 --- /dev/null +++ b/contracts/interfaces/IERC3009.sol @@ -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; +} diff --git a/contracts/interfaces/ISignatureTransfer.sol b/contracts/interfaces/ISignatureTransfer.sol new file mode 100644 index 0000000..6b23846 --- /dev/null +++ b/contracts/interfaces/ISignatureTransfer.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title ISignatureTransfer + * @notice Interface for Permit2's SignatureTransfer functionality + * @dev Based on Uniswap's canonical Permit2 contract + */ +interface ISignatureTransfer { + /** + * @notice The token and amount details for a transfer signed in the permit transfer signature + */ + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /** + * @notice The signed permit message for a single token transfer + */ + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /** + * @notice Specifies the recipient address and amount for batched transfers. + * @dev Recipients and amounts correspond to the index of the signed token permissions array. + * @dev Reverts if the requested amount is greater than the permitted signed amount. + */ + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /** + * @notice A map from token owner address and a caller specified word + * index to a bitmap. Used to set bits in the bitmap to prevent + * against signature replay protection + * @dev Uses unordered nonces so that permit messages do not need to be + * spent in a certain order + * @dev The mapping is indexed first by the token owner, then by an + * index specified in the nonce + * @dev It returns a uint256 bitmap + * @dev The index, or wordPosition is capped at type(uint248).max + */ + function nonceBitmap(address, uint256) external view returns (uint256); + + /** + * @notice Transfers a token using a signed permit message + * @dev Reverts if the requested amount is greater than the permitted signed amount + * @param permit The permit data signed over by the owner + * @param owner The owner of the tokens to transfer + * @param transferDetails The spender's requested transfer details for the permitted token + * @param signature The signature to verify + */ + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /** + * @notice Transfers a token using a signed permit message + * @notice Includes extra data provided by the caller to verify + * signature over + * @dev The witness type string must follow EIP712 ordering of nested + * structs and must include the TokenPermissions type definition + * @dev Reverts if the requested amount is greater than the permitted + * signed amount + * @param permit The permit data signed over by the owner + * @param transferDetails The spender's requested transfer details for + * the permitted token + * @param owner The owner of the tokens to transfer + * @param witness Extra data to include when checking the user signature + * @param witnessTypeString The EIP-712 type definition for remaining + * string stub of the typehash + * @param signature The signature to verify + */ + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; +} diff --git a/contracts/EIP712.sol b/contracts/legacy/EIP712.sol similarity index 100% rename from contracts/EIP712.sol rename to contracts/legacy/EIP712.sol diff --git a/contracts/PaymentPermit.sol b/contracts/legacy/PaymentPermit.sol similarity index 100% rename from contracts/PaymentPermit.sol rename to contracts/legacy/PaymentPermit.sol diff --git a/contracts/interface/IEIP712.sol b/contracts/legacy/interface/IEIP712.sol similarity index 100% rename from contracts/interface/IEIP712.sol rename to contracts/legacy/interface/IEIP712.sol diff --git a/contracts/interface/IPaymentPermit.sol b/contracts/legacy/interface/IPaymentPermit.sol similarity index 100% rename from contracts/interface/IPaymentPermit.sol rename to contracts/legacy/interface/IPaymentPermit.sol diff --git a/contracts/libraries/PermitHash.sol b/contracts/legacy/libraries/PermitHash.sol similarity index 100% rename from contracts/libraries/PermitHash.sol rename to contracts/legacy/libraries/PermitHash.sol diff --git a/contracts/mocks/MockGenericERC20.sol b/contracts/mocks/MockGenericERC20.sol new file mode 100644 index 0000000..cc6c503 --- /dev/null +++ b/contracts/mocks/MockGenericERC20.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title MockGenericERC20 + * @notice Minimal ERC20 mock with no constructor arguments for deterministic CREATE2 deployment. + * @dev No EIP-2612 / EIP-3009 extensions — pure vanilla ERC20. + * All metadata is hardcoded so that initCode == creationCode (no constructor args), + * which keeps the deterministic address independent of encoding. + */ +contract MockGenericERC20 is ERC20 { + constructor() ERC20("Mock Generic ERC20", "MOCK") {} + + function decimals() public pure override returns (uint8) { + return 6; + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/contracts/periphery/DepositCollector.sol b/contracts/periphery/DepositCollector.sol new file mode 100644 index 0000000..e283c03 --- /dev/null +++ b/contracts/periphery/DepositCollector.sol @@ -0,0 +1,36 @@ +// 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` deployment. +/// +/// @dev All collectors must inherit this so only `x402BatchSettlement` can call `collect`, +/// mitigating frontrunning and theft of user funds. +/// +/// @author Coinbase +abstract contract DepositCollector is IDepositCollector { + /// @notice The `x402BatchSettlement` deployment that may invoke `collect` on this collector. + address public immutable x402BatchSettlement; + + /// @notice Thrown when `collect` is called by any address other than `x402BatchSettlement`. + error OnlyX402BatchSettlement(); + + /// @notice Thrown when the `x402BatchSettlement` address passed to the constructor is zero. + error InvalidX402BatchSettlementAddress(); + + /// @param _x402BatchSettlement The `x402BatchSettlement` instance this collector serves. + constructor( + address _x402BatchSettlement + ) { + if (_x402BatchSettlement == address(0)) revert InvalidX402BatchSettlementAddress(); + x402BatchSettlement = _x402BatchSettlement; + } + + /// @dev Restricts the function body to calls originating from `x402BatchSettlement`. + modifier onlyx402BatchSettlement() { + if (msg.sender != x402BatchSettlement) revert OnlyX402BatchSettlement(); + _; + } +} diff --git a/contracts/periphery/ERC3009DepositCollector.sol b/contracts/periphery/ERC3009DepositCollector.sol new file mode 100644 index 0000000..4b55ed5 --- /dev/null +++ b/contracts/periphery/ERC3009DepositCollector.sol @@ -0,0 +1,52 @@ +// 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 {DepositCollector} from "./DepositCollector.sol"; +import {IDepositCollector} from "../interfaces/IDepositCollector.sol"; +import {IERC3009} from "../interfaces/IERC3009.sol"; + +/// @title ERC3009DepositCollector +/// @notice Collects deposits via ERC-3009 `receiveWithAuthorization` for gasless pulls into escrow. +/// +/// @dev ERC-3009 requires `msg.sender == to`, so tokens arrive at this contract and are forwarded to +/// `x402BatchSettlement`. The authorization nonce is `keccak256(abi.encode(channelId, salt))` where `salt` +/// is supplied in `collectorData`. +/// +/// @author Coinbase +contract ERC3009DepositCollector is DepositCollector { + using SafeERC20 for IERC20; + + /// @param _x402BatchSettlement The `x402BatchSettlement` contract that receives pulled tokens. + constructor( + address _x402BatchSettlement + ) DepositCollector(_x402BatchSettlement) {} + + /// @inheritdoc IDepositCollector + /// + /// @param payer The token owner authorizing the transfer. + /// @param token The ERC-20 implementing ERC-3009. + /// @param amount The amount to collect. + /// @param channelId The channel identifier (hashed into the ERC-3009 nonce). + /// @param collectorData `abi.encode(validAfter, validBefore, salt, signature)` for `receiveWithAuthorization`. + function collect( + address payer, + address token, + uint256 amount, + bytes32 channelId, + bytes calldata collectorData + ) external override onlyx402BatchSettlement { + (uint256 validAfter, uint256 validBefore, uint256 salt, bytes memory signature) = + abi.decode(collectorData, (uint256, uint256, uint256, bytes)); + + bytes32 expectedNonce = keccak256(abi.encode(channelId, salt)); + + IERC3009(token).receiveWithAuthorization( + payer, address(this), amount, validAfter, validBefore, expectedNonce, signature + ); + + IERC20(token).safeTransfer(x402BatchSettlement, amount); + } +} diff --git a/contracts/periphery/Permit2DepositCollector.sol b/contracts/periphery/Permit2DepositCollector.sol new file mode 100644 index 0000000..9abe91f --- /dev/null +++ b/contracts/periphery/Permit2DepositCollector.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import {DepositCollector} from "./DepositCollector.sol"; +import {IDepositCollector} from "../interfaces/IDepositCollector.sol"; +import {ISignatureTransfer} from "../interfaces/ISignatureTransfer.sol"; + +/// @title Permit2DepositCollector +/// @notice Collects deposits using Permit2 `permitWitnessTransferFrom` with a channel-bound witness. +/// +/// @dev Tokens move directly from the payer to `x402BatchSettlement` through Permit2. +/// `collectorData` is `abi.encode(nonce, deadline, permit2Signature, eip2612PermitData)`. +/// The token and amount on the `collect` call must match the signed Permit2 transfer. +/// If `eip2612PermitData` is empty, no EIP-2612 `permit` is attempted. If non-empty, it must be +/// `abi.encode(value, permitDeadline, v, r, s)` and `IERC20Permit.permit(owner, Permit2, ...)` runs +/// before the Permit2 transfer (failures are logged, then execution continues). +/// +/// @author Coinbase +contract Permit2DepositCollector is DepositCollector { + /// @notice Uniswap Permit2 `SignatureTransfer` singleton used for witness transfers. + ISignatureTransfer public immutable PERMIT2; + + /// @notice String passed to Permit2 for EIP-712 witness typing of `DepositWitness`. + /// + /// @dev Referenced struct definitions must follow EIP-712 `encodeType`: alphabetically by struct name + /// (`DepositWitness` before `TokenPermissions`). Matches wallets that reconstruct the stub via sorted types. + string public constant DEPOSIT_WITNESS_TYPE_STRING = + "DepositWitness witness)DepositWitness(bytes32 channelId)TokenPermissions(address token,uint256 amount)"; + + /// @notice Typehash for the `DepositWitness` struct bound to each transfer. + bytes32 public constant DEPOSIT_WITNESS_TYPEHASH = keccak256("DepositWitness(bytes32 channelId)"); + + /// @notice Thrown when `_permit2` in the constructor is the zero address. + error InvalidPermit2Address(); + + /// @notice Thrown when optional EIP-2612 `permit` `value` does not match `collect` `amount`. + error Permit2612AmountMismatch(); + + /// @notice Logged when an optional EIP-2612 `permit` reverts with a string reason. + /// + /// @param token The ERC-20 `permit` was called on. + /// @param owner The owner passed to `permit`. + /// @param reason The revert string from the token. + event EIP2612PermitFailedWithReason(address indexed token, address indexed owner, string reason); + + /// @notice Logged when an optional EIP-2612 `permit` reverts with a panic code. + /// + /// @param token The ERC-20 `permit` was called on. + /// @param owner The owner passed to `permit`. + /// @param errorCode The panic code. + event EIP2612PermitFailedWithPanic(address indexed token, address indexed owner, uint256 errorCode); + + /// @notice Logged when an optional EIP-2612 `permit` reverts with arbitrary data. + /// + /// @param token The ERC-20 `permit` was called on. + /// @param owner The owner passed to `permit`. + /// @param data Low-level revert data. + event EIP2612PermitFailedWithData(address indexed token, address indexed owner, bytes data); + + /// @param _x402BatchSettlement The `x402BatchSettlement` contract that receives Permit2 transfers. + /// @param _permit2 The canonical Permit2 `SignatureTransfer` contract. + constructor(address _x402BatchSettlement, address _permit2) DepositCollector(_x402BatchSettlement) { + if (_permit2 == address(0)) revert InvalidPermit2Address(); + PERMIT2 = ISignatureTransfer(_permit2); + } + + /// @inheritdoc IDepositCollector + /// + /// @param payer The owner of the Permit2 transfer. + /// @param token The ERC-20 being transferred. + /// @param amount The amount; must match the signed Permit2 permit and optional EIP-2612 segment. + /// @param channelId Bound into the Permit2 witness for this deposit. + /// @param collectorData `abi.encode(nonce, deadline, permit2Signature, eip2612PermitData)`. + function collect( + address payer, + address token, + uint256 amount, + bytes32 channelId, + bytes calldata collectorData + ) external override onlyx402BatchSettlement { + (uint256 nonce, uint256 deadline, bytes memory permit2Signature, bytes memory eip2612PermitData) = + abi.decode(collectorData, (uint256, uint256, bytes, bytes)); + + if (eip2612PermitData.length > 0) { + _tryEip2612Permit(payer, token, amount, eip2612PermitData); + } + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: token, amount: amount}), + nonce: nonce, + deadline: deadline + }); + + _executePermit2Transfer(payer, amount, channelId, permit, permit2Signature); + } + + /// @dev Performs `permitWitnessTransferFrom` to pull from `payer` into `x402BatchSettlement` with `channelId` in the witness. + function _executePermit2Transfer( + address payer, + uint256 amount, + bytes32 channelId, + ISignatureTransfer.PermitTransferFrom memory permit, + bytes memory signature + ) internal { + bytes32 witnessHash = keccak256(abi.encode(DEPOSIT_WITNESS_TYPEHASH, channelId)); + + PERMIT2.permitWitnessTransferFrom( + permit, + ISignatureTransfer.SignatureTransferDetails({to: x402BatchSettlement, requestedAmount: amount}), + payer, + witnessHash, + DEPOSIT_WITNESS_TYPE_STRING, + signature + ); + } + + /// @dev Best-effort EIP-2612 `permit` helper; failures emit one of the `EIP2612PermitFailed*` events. + function _tryEip2612Permit(address payer, address token, uint256 amount, bytes memory eip2612PermitData) private { + (uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s) = + abi.decode(eip2612PermitData, (uint256, uint256, uint8, bytes32, bytes32)); + if (permitValue != amount) revert Permit2612AmountMismatch(); + + try IERC20Permit(token).permit(payer, address(PERMIT2), permitValue, permitDeadline, v, r, s) {} + catch Error(string memory reason) { + emit EIP2612PermitFailedWithReason(token, payer, reason); + } catch Panic(uint256 code) { + emit EIP2612PermitFailedWithPanic(token, payer, code); + } catch (bytes memory lowLevelData) { + emit EIP2612PermitFailedWithData(token, payer, lowLevelData); + } + } +} diff --git a/contracts/x402BasePermit2Proxy.sol b/contracts/x402BasePermit2Proxy.sol new file mode 100644 index 0000000..1cc5bff --- /dev/null +++ b/contracts/x402BasePermit2Proxy.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol"; + +/** + * @title x402BasePermit2Proxy + * @notice Abstract base contract for x402 payments using Permit2 + * + * @dev This contract provides the shared logic for x402 payment proxies. + * It acts as the authorized spender in Permit2 signatures and uses the + * "witness" pattern to cryptographically bind the payment destination, + * preventing facilitators from redirecting funds. + * + * The Permit2 address is passed as a constructor argument and stored as + * an immutable. Since Permit2 is deployed via a deterministic CREATE2 + * deployer, its canonical address (0x000000000022D473030F116dDEE9F6B43aC78BA3) + * is the same on all EVM chains. Using the same constructor argument on + * every chain keeps the initCode identical, preserving a uniform CREATE2 + * address for these proxies across all chains. + * + * Uses {ReentrancyGuardTransient} (EIP-1153); deploy only on chains with transient storage support. + * + * @author x402 Protocol + */ +abstract contract x402BasePermit2Proxy is ReentrancyGuardTransient { + /// @notice The Permit2 contract address (set once at construction, immutable) + ISignatureTransfer public immutable PERMIT2; + + /// @notice Emitted when settle() completes successfully + event Settled(); + + /// @notice Emitted when settleWithPermit() completes successfully + event SettledWithPermit(); + + /// @notice Emitted when EIP-2612 permit() reverts with an Error(string) reason + /// @param token The token whose permit() was called + /// @param owner The token owner for whom permit was attempted + /// @param reason The human-readable revert reason string + event EIP2612PermitFailedWithReason(address indexed token, address indexed owner, string reason); + + /// @notice Emitted when EIP-2612 permit() reverts with a Panic(uint256) code + /// @param token The token whose permit() was called + /// @param owner The token owner for whom permit was attempted + /// @param errorCode The Solidity panic code (e.g. 0x11 for overflow, 0x01 for assert) + event EIP2612PermitFailedWithPanic(address indexed token, address indexed owner, uint256 errorCode); + + /// @notice Emitted when EIP-2612 permit() reverts with a custom error or empty data + /// @param token The token whose permit() was called + /// @param owner The token owner for whom permit was attempted + /// @param data The raw revert data (custom error selector + params, or empty) + event EIP2612PermitFailedWithData(address indexed token, address indexed owner, bytes data); + + /// @notice Thrown when Permit2 address is zero + error InvalidPermit2Address(); + + /// @notice Thrown when destination address is zero + error InvalidDestination(); + + /// @notice Thrown when payment is attempted before validAfter timestamp + error PaymentTooEarly(); + + /// @notice Thrown when owner address is zero + error InvalidOwner(); + + /// @notice Thrown when settlement amount is zero + error InvalidAmount(); + + /// @notice Thrown when EIP-2612 permit value doesn't match Permit2 permitted amount + error Permit2612AmountMismatch(); + + /** + * @notice EIP-2612 permit parameters grouped to reduce stack depth + * @param value Approval amount for Permit2 + * @param deadline Permit expiration timestamp + * @param r ECDSA signature parameter + * @param s ECDSA signature parameter + * @param v ECDSA signature parameter + */ + struct EIP2612Permit { + uint256 value; + uint256 deadline; + bytes32 r; + bytes32 s; + uint8 v; + } + + /** + * @notice Constructs the proxy with the Permit2 contract address + * @param _permit2 Address of the Permit2 contract (canonical on all EVM chains) + * @dev The Permit2 address is stored as an immutable, eliminating any post-deployment + * initialization race. Using the same canonical Permit2 address on every chain + * keeps the initCode identical, preserving CREATE2 address determinism. + */ + constructor( + address _permit2 + ) { + if (_permit2 == address(0)) revert InvalidPermit2Address(); + PERMIT2 = ISignatureTransfer(_permit2); + } + + /** + * @notice Internal settlement logic shared by all settlement functions + * @dev Validates common parameters and executes the Permit2 witness transfer. + * Each child contract computes its own witnessHash and witnessTypeString + * based on its Witness struct definition. + * @param permit The Permit2 transfer authorization + * @param settlementAmount The actual amount to transfer (may be <= permit.permitted.amount) + * @param owner The token owner (payer) + * @param to The destination address for the transfer + * @param validAfter Earliest timestamp when payment can be settled + * @param witnessHash The EIP-712 hash of the child's witness struct + * @param witnessTypeString The EIP-712 type string for the child's witness + * @param signature The payer's signature + */ + function _settle( + ISignatureTransfer.PermitTransferFrom calldata permit, + uint256 settlementAmount, + address owner, + address to, + uint256 validAfter, + bytes32 witnessHash, + string memory witnessTypeString, + bytes calldata signature + ) internal { + if (settlementAmount == 0) revert InvalidAmount(); + if (owner == address(0)) revert InvalidOwner(); + if (to == address(0)) revert InvalidDestination(); + if (block.timestamp < validAfter) revert PaymentTooEarly(); + + ISignatureTransfer.SignatureTransferDetails memory transferDetails = + ISignatureTransfer.SignatureTransferDetails({to: to, requestedAmount: settlementAmount}); + + PERMIT2.permitWitnessTransferFrom(permit, transferDetails, owner, witnessHash, witnessTypeString, signature); + } + + /** + * @notice Validates and attempts to execute an EIP-2612 permit to approve Permit2 + * @dev Reverts if permit2612.value does not match permittedAmount. + * The actual permit call does not revert on failure because the approval + * might already exist or the token might not support EIP-2612. + * @param token The token address + * @param owner The token owner + * @param permit2612 The EIP-2612 permit parameters + * @param permittedAmount The Permit2 permitted amount + */ + function _executePermit( + address token, + address owner, + EIP2612Permit calldata permit2612, + uint256 permittedAmount + ) internal { + if (permit2612.value != permittedAmount) { + revert Permit2612AmountMismatch(); + } + + try IERC20Permit(token).permit( + owner, address(PERMIT2), permit2612.value, permit2612.deadline, permit2612.v, permit2612.r, permit2612.s + ) { + // EIP-2612 permit succeeded + } catch Error(string memory reason) { + emit EIP2612PermitFailedWithReason(token, owner, reason); + } catch Panic(uint256 errorCode) { + emit EIP2612PermitFailedWithPanic(token, owner, errorCode); + } catch (bytes memory data) { + emit EIP2612PermitFailedWithData(token, owner, data); + } + } +} diff --git a/contracts/x402BatchSettlement.sol b/contracts/x402BatchSettlement.sol new file mode 100644 index 0000000..2661360 --- /dev/null +++ b/contracts/x402BatchSettlement.sol @@ -0,0 +1,587 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +import {IDepositCollector} from "./interfaces/IDepositCollector.sol"; + +/// @title x402BatchSettlement +/// @notice Stateless unidirectional payment channel contract for the x402 `batch-settlement` scheme on EVM. +/// +/// @dev Channel identity is derived from an immutable `ChannelConfig`: +/// `channelId = keccak256(abi.encode(channelConfig))`. +/// Deploy at the same address on every chain via CREATE2. +/// Uses `ReentrancyGuardTransient` (EIP-1153); deploy only where transient storage is supported. +/// Fee-on-transfer and rebasing tokens are not recommended and are not guaranteed to behave like standard ERC-20s. +/// +/// @author Coinbase +contract x402BatchSettlement is EIP712, Multicall, ReentrancyGuardTransient { + using SafeERC20 for IERC20; + + // ========================================================================= + // Structs + // ========================================================================= + + /// @notice Immutable channel parameters; hashed to form `channelId`. + /// + /// @dev `withdrawDelay` must be between `MIN_WITHDRAW_DELAY` and `MAX_WITHDRAW_DELAY` (inclusive) at deposit time. + /// The floor is a protocol minimum; high-value channels often use **longer** delays so receiver-side `claim` + /// transactions have time to land before a payer can finalize a timed withdrawal (an integrator policy choice). + struct ChannelConfig { + address payer; + address payerAuthorizer; + address receiver; + address receiverAuthorizer; + address token; + uint40 withdrawDelay; + bytes32 salt; + } + + /// @notice Per-channel escrow and claim totals. + struct ChannelState { + uint128 balance; + uint128 totalClaimed; + } + + /// @notice In-flight timed withdrawal for a channel. + struct WithdrawalState { + uint128 amount; + /// @dev Timestamp (seconds) when the withdrawal was started; zero if none pending. + uint40 initiatedAt; + } + + /// @notice Per-receiver, per-token aggregates for settlement sweeps. + struct ReceiverState { + uint128 totalClaimed; + uint128 totalSettled; + } + + /// @notice Payer-signed authorization data (no signature field here; that lives on `VoucherClaim`). + struct Voucher { + ChannelConfig channel; + uint128 maxClaimableAmount; + } + + /// @notice One signed voucher row plus the cumulative amount the receiver side is claiming. + /// + /// @dev Cumulative `totalClaimed` (not a delta) gives replay protection: replaying an old voucher after it + /// was applied is a no-op. + struct VoucherClaim { + Voucher voucher; + bytes signature; + uint128 totalClaimed; + } + + // ========================================================================= + // Constants + // ========================================================================= + + uint40 public constant MIN_WITHDRAW_DELAY = 15 minutes; + uint40 public constant MAX_WITHDRAW_DELAY = 30 days; + + // ========================================================================= + // Constants — EIP-712 Type Hashes + // ========================================================================= + + bytes32 public constant CHANNEL_CONFIG_TYPEHASH = keccak256( + "ChannelConfig(address payer,address payerAuthorizer,address receiver,address receiverAuthorizer,address token,uint40 withdrawDelay,bytes32 salt)" + ); + + bytes32 public constant VOUCHER_TYPEHASH = keccak256("Voucher(bytes32 channelId,uint128 maxClaimableAmount)"); + + bytes32 public constant REFUND_TYPEHASH = keccak256("Refund(bytes32 channelId,uint256 nonce,uint128 amount)"); + + /// @dev EIP-712 entry for one row in a signed claim batch (mirrors on-chain `VoucherClaim` fields used for authorization). + bytes32 public constant CLAIM_ENTRY_TYPEHASH = + keccak256("ClaimEntry(bytes32 channelId,uint128 maxClaimableAmount,uint128 totalClaimed)"); + + /// @dev Full nested EIP-712 type so wallets can render `ClaimEntry[]` for user review. + bytes32 public constant CLAIM_BATCH_TYPEHASH = keccak256( + "ClaimBatch(ClaimEntry[] claims)ClaimEntry(bytes32 channelId,uint128 maxClaimableAmount,uint128 totalClaimed)" + ); + + // ========================================================================= + // Storage + // ========================================================================= + + mapping(bytes32 channelId => ChannelState) public channels; + + /// @dev Monotonic sequence per `channelId` for EIP-712 `Refund` replay protection. Incremented inside + /// `_executeRefund` **before** the capped refund amount is applied, for **both** `refund` and + /// `refundWithSignature`, including when the capped amount is zero (no transfer, no `Refunded` event). + /// A direct `refund` therefore advances the nonce and invalidates any presigned `refundWithSignature` + /// digest that was bound to the previous nonce; coordinate receiver vs relayer flows off-chain. + mapping(bytes32 channelId => uint256) public refundNonce; + mapping(bytes32 channelId => WithdrawalState) public pendingWithdrawals; + mapping(address receiver => mapping(address token => ReceiverState)) public receivers; + + // ========================================================================= + // Events + // ========================================================================= + + /// @notice Emitted the first time a channel receives escrowed balance. + event ChannelCreated(bytes32 indexed channelId, ChannelConfig config); + + /// @notice Emitted when unclaimed escrow returns to zero and no claims were recorded. + event ChannelClosed(bytes32 indexed channelId, ChannelConfig config); + + /// @notice Emitted after a successful deposit into a channel. + event Deposited(bytes32 indexed channelId, address indexed sender, uint128 amount, uint128 newBalance); + + /// @notice Emitted when the receiver side increases the cumulative claimed amount for a channel. + event Claimed(bytes32 indexed channelId, address indexed sender, uint128 claimAmount, uint128 newTotalClaimed); + + /// @notice Emitted when claimed funds are transferred to the receiver during settlement. + event Settled(address indexed receiver, address indexed token, address indexed sender, uint128 amount); + + /// @notice Emitted when escrowed funds are refunded to the payer. + event Refunded(bytes32 indexed channelId, address indexed sender, uint128 amount); + + /// @notice Emitted when a timed withdrawal is started. + event WithdrawInitiated(bytes32 indexed channelId, uint128 amount, uint40 finalizeAfter); + + /// @notice Emitted when a timed withdrawal completes and funds move to the payer. + event WithdrawFinalized(bytes32 indexed channelId, address indexed sender, uint128 amount); + + // ========================================================================= + // Errors + // ========================================================================= + + /// @dev Errors use CapWords naming. + + error InvalidChannel(); + error ZeroDeposit(); + error DepositOverflow(); + error InvalidSignature(); + error NotReceiverAuthorizer(); + error NotAuthorizedToClaim(); + error NotAuthorizedToRefund(); + error ClaimExceedsCeiling(); + error ClaimExceedsBalance(); + error WithdrawalAlreadyPending(); + error WithdrawalNotPending(); + error NotAuthorizedToFinalizeWithdraw(); + error WithdrawDelayNotElapsed(); + error NothingToWithdraw(); + error WithdrawAmountExceedsAvailable(); + error WithdrawDelayOutOfRange(); + error EmptyBatch(); + error DepositCollectionFailed(); + error InvalidCollector(); + error InvalidRefundNonce(); + error ZeroRefund(); + + // ========================================================================= + // Constructor + // ========================================================================= + + /// @notice Sets the EIP-712 domain for vouchers, refunds, and claim batches. + constructor() EIP712("x402 Batch Settlement", "1") {} + + // ========================================================================= + // Deposits + // ========================================================================= + + /// @notice Deposits tokens into a channel using a pluggable collector. + /// + /// @dev The collector executes the pull; this function checks the contract's token balance increased + /// by `amount` after the call (post-condition) to detect fee-on-transfer or failed pulls. + /// + /// @param config The immutable channel configuration. + /// @param amount The exact amount of tokens to deposit. + /// @param collector The deposit collector contract address. + /// @param collectorData Opaque bytes forwarded to the collector (signatures, nonces, etc.). + function deposit( + ChannelConfig calldata config, + uint128 amount, + address collector, + bytes calldata collectorData + ) external nonReentrant { + if (config.payer == address(0)) revert InvalidChannel(); + if (config.receiver == address(0)) revert InvalidChannel(); + if (config.receiverAuthorizer == address(0)) revert InvalidChannel(); + if (config.token == address(0)) revert InvalidChannel(); + if (config.withdrawDelay < MIN_WITHDRAW_DELAY || config.withdrawDelay > MAX_WITHDRAW_DELAY) { + revert WithdrawDelayOutOfRange(); + } + if (amount == 0) revert ZeroDeposit(); + if (collector == address(0)) revert InvalidCollector(); + + bytes32 channelId = getChannelId(config); + ChannelState storage ch = channels[channelId]; + + if (amount > type(uint128).max - ch.balance) revert DepositOverflow(); + ch.balance += amount; + + uint256 balBefore = IERC20(config.token).balanceOf(address(this)); + IDepositCollector(collector).collect(config.payer, config.token, amount, channelId, collectorData); + uint256 balAfter = IERC20(config.token).balanceOf(address(this)); + if (balAfter != balBefore + amount) revert DepositCollectionFailed(); + + // First deposit on this channel (escrow was empty before this deposit) + if (ch.balance == amount && ch.totalClaimed == 0) { + emit ChannelCreated(channelId, config); + } + emit Deposited(channelId, msg.sender, amount, ch.balance); + } + + // ========================================================================= + // Claim & Settle + // ========================================================================= + + /// @notice Applies a batch of voucher claims and updates channel accounting. + /// + /// @param voucherClaims Signed voucher rows with cumulative claim totals. + /// + /// @dev For each row, `msg.sender` must be `receiverAuthorizer` or `receiver` for that row's channel. + /// + /// Receiver-side integrators should submit `claim` **promptly** once a payer-signed voucher should bind on-chain + /// entitlement: only **recorded** claims (`totalClaimed`) reduce what the payer can withdraw. If the payer + /// finalizes a timed withdrawal before this call executes, later `claim` can revert with `ClaimExceedsBalance`. + function claim( + VoucherClaim[] calldata voucherClaims + ) external nonReentrant { + if (voucherClaims.length == 0) revert EmptyBatch(); + + for (uint256 i = 0; i < voucherClaims.length; ++i) { + ChannelConfig calldata c = voucherClaims[i].voucher.channel; + if (!_isReceiverSide(msg.sender, c)) { + revert NotAuthorizedToClaim(); + } + _processVoucherClaim(voucherClaims[i]); + } + } + + /// @notice Applies voucher claims authorized by a `receiverAuthorizer` EIP-712 signature over the batch. + /// + /// @param voucherClaims Claim rows for channels sharing the same `receiverAuthorizer`. + /// @param authorizerSignature Signature from `receiverAuthorizer` over `getClaimBatchDigest(voucherClaims)`. + /// + /// @dev Callable by anyone (relay-friendly). All rows must match the same `receiverAuthorizer`. + /// + /// Same liveness consideration as `claim`: relays should submit before payer timed withdrawal drains escrow + /// (see `finalizeWithdraw` / `initiateWithdraw`). + function claimWithSignature( + VoucherClaim[] calldata voucherClaims, + bytes calldata authorizerSignature + ) external nonReentrant { + if (voucherClaims.length == 0) revert EmptyBatch(); + + address authorizer = voucherClaims[0].voucher.channel.receiverAuthorizer; + + bytes32 digest = getClaimBatchDigest(voucherClaims); + if (!SignatureChecker.isValidSignatureNow(authorizer, digest, authorizerSignature)) { + revert InvalidSignature(); + } + + for (uint256 i = 0; i < voucherClaims.length; ++i) { + if (voucherClaims[i].voucher.channel.receiverAuthorizer != authorizer) { + revert NotReceiverAuthorizer(); + } + _processVoucherClaim(voucherClaims[i]); + } + } + + /// @notice Transfers claimed-but-not-yet-settled tokens to the receiver for a `(receiver, token)` pair. + /// + /// @param receiver The receiver address whose pending settlement is swept. + /// @param token The ERC-20 token to transfer. + /// + /// @dev Permissionless: typically called by the receiver or a facilitator. + function settle(address receiver, address token) external nonReentrant { + ReceiverState storage rs = receivers[receiver][token]; + uint128 amount = rs.totalClaimed - rs.totalSettled; + if (amount == 0) return; + + rs.totalSettled = rs.totalClaimed; + + IERC20(token).safeTransfer(receiver, amount); + + emit Settled(receiver, token, msg.sender, amount); + } + + // ========================================================================= + // Withdrawal Flow + // ========================================================================= + + /// @notice Starts the payer-side timed withdrawal window for unclaimed escrow. + /// + /// @param config The channel configuration. + /// @param amount The gross amount requested; may be capped on finalization. + /// + /// @dev Only `config.payer` or `config.payerAuthorizer` may call. Reverts if a withdrawal is already pending. + /// + /// Available liquidity is `ch.balance - ch.totalClaimed` using **only** on-chain `totalClaimed`. Payer-signed + /// vouchers that are not yet claimed do **not** reserve funds; receivers relying on voucher value must treat + /// inclusion of `claim` during the withdrawal window as an operational requirement (RPC outages, censorship, + /// or relay failure can interact badly with `finalizeWithdraw`). + function initiateWithdraw(ChannelConfig calldata config, uint128 amount) external nonReentrant { + if (msg.sender != config.payer && msg.sender != config.payerAuthorizer) { + revert InvalidChannel(); + } + if (amount == 0) revert NothingToWithdraw(); + + bytes32 channelId = getChannelId(config); + + WithdrawalState storage ws = pendingWithdrawals[channelId]; + if (ws.initiatedAt != 0) revert WithdrawalAlreadyPending(); + + ChannelState storage ch = channels[channelId]; + uint128 available = ch.balance - ch.totalClaimed; + if (amount > available) revert WithdrawAmountExceedsAvailable(); + + ws.amount = amount; + ws.initiatedAt = uint40(block.timestamp); + + uint40 finalizeAfter = uint40(block.timestamp) + config.withdrawDelay; + emit WithdrawInitiated(channelId, amount, finalizeAfter); + } + + /// @notice Finalizes a pending withdrawal after `withdrawDelay` and sends tokens to the payer. + /// + /// @param config The channel configuration. + /// + /// @dev Only `config.payer` or `config.payerAuthorizer` may call. Amount may be capped by available escrow. + /// + /// Uses **current** `ch.balance` and `ch.totalClaimed` at execution time. If this transaction executes before a + /// competing `claim`, it can reduce escrow such that the claim later reverts with `ClaimExceedsBalance`. + function finalizeWithdraw( + ChannelConfig calldata config + ) external nonReentrant { + if (msg.sender != config.payer && msg.sender != config.payerAuthorizer) { + revert NotAuthorizedToFinalizeWithdraw(); + } + + bytes32 channelId = getChannelId(config); + WithdrawalState storage ws = pendingWithdrawals[channelId]; + + if (ws.initiatedAt == 0) revert WithdrawalNotPending(); + if (block.timestamp < uint256(ws.initiatedAt) + uint256(config.withdrawDelay)) { + revert WithdrawDelayNotElapsed(); + } + + ChannelState storage ch = channels[channelId]; + uint128 available = ch.balance - ch.totalClaimed; + uint128 withdrawAmount = ws.amount > available ? available : ws.amount; + + ws.amount = 0; + ws.initiatedAt = 0; + ch.balance -= withdrawAmount; + + emit WithdrawFinalized(channelId, msg.sender, withdrawAmount); + + if (ch.balance == 0 && ch.totalClaimed == 0) { + emit ChannelClosed(channelId, config); + } + + if (withdrawAmount > 0) { + IERC20(config.token).safeTransfer(config.payer, withdrawAmount); + } + } + + /// @notice Cooperative refund of unclaimed escrow to the payer. + /// + /// @param config The channel configuration. + /// @param amount Requested refund; capped to `balance - totalClaimed`. Reverts with `ZeroRefund` if `amount == 0`. + /// If the capped amount is zero but `amount > 0`, no token transfer occurs (no `Refunded` event) + /// yet `refundNonce` still advances (see `_executeRefund`). + /// + /// @dev Only `receiverAuthorizer` or `receiver` may call. Shares `_executeRefund` with `refundWithSignature`, so + /// every successful call advances `refundNonce` for the channel. Any in-flight EIP-712 `Refund` signature + /// produced for the previous nonce becomes unusable; relayers and authorizers should expect re-signing if + /// direct refunds race ahead of relayed `refundWithSignature` submissions. + function refund(ChannelConfig calldata config, uint128 amount) external nonReentrant { + if (!_isReceiverSide(msg.sender, config)) { + revert NotAuthorizedToRefund(); + } + _executeRefund(config, amount); + } + + /// @notice Refunds unclaimed escrow to the payer using an EIP-712 `Refund` signature from `receiverAuthorizer`. + /// + /// @param config The channel configuration. + /// @param amount The amount in the signature; capped like `refund`. If the capped amount is zero, no transfer + /// and no `Refunded` event, but `refundNonce` still advances once `_executeRefund` runs. + /// @param nonce Must equal `refundNonce(channelId)` at entry; `_executeRefund` increments it (same as `refund`). + /// @param receiverAuthorizerSignature EIP-712 signature from `receiverAuthorizer` over the refund digest. + /// + /// @dev Callable by anyone (relay-friendly). A prior direct `refund` on the same channel can advance the nonce + /// and cause `InvalidRefundNonce` here until the authorizer signs at the new nonce. + function refundWithSignature( + ChannelConfig calldata config, + uint128 amount, + uint256 nonce, + bytes calldata receiverAuthorizerSignature + ) external nonReentrant { + bytes32 channelId = getChannelId(config); + if (nonce != refundNonce[channelId]) revert InvalidRefundNonce(); + bytes32 digest = getRefundDigest(channelId, nonce, amount); + if (!SignatureChecker.isValidSignatureNow(config.receiverAuthorizer, digest, receiverAuthorizerSignature)) { + revert InvalidSignature(); + } + _executeRefund(config, amount); + } + + // ========================================================================= + // View Functions + // ========================================================================= + + /// @notice Returns the canonical `channelId` for a channel configuration. + /// + /// @param config The channel configuration to hash. + /// + /// @return The EIP-712 typed-data hash of `ChannelConfig`, bound to this contract's domain + /// (chainId + verifyingContract). Identical configs on different chains or different + /// deployments produce different ids, preventing cross-chain channel-state collision. + function getChannelId( + ChannelConfig calldata config + ) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(CHANNEL_CONFIG_TYPEHASH, config))); + } + + /// @notice EIP-712 digest for a `Voucher` with the given `channelId` and `maxClaimableAmount`. + /// + /// @param channelId The channel identifier. + /// @param maxClaimableAmount The ceiling encoded in the voucher. + /// + /// @return The typed data hash signers use for payer authorization. + function getVoucherDigest(bytes32 channelId, uint128 maxClaimableAmount) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(VOUCHER_TYPEHASH, channelId, maxClaimableAmount))); + } + + /// @notice EIP-712 digest for a cooperative `Refund` authorization. + /// + /// @param channelId The channel identifier. + /// @param nonce The refund nonce; must equal on-chain `refundNonce(channelId)` when `refundWithSignature` runs. + /// @param amount The signed refund amount. + /// + /// @return The typed data hash for `receiverAuthorizer` to sign. + /// + /// @dev Off-chain signers should use the current `refundNonce(channelId)` as `nonce`. A successful direct `refund` + /// on the same channel advances that nonce and invalidates digests signed for the old value until re-signed. + function getRefundDigest(bytes32 channelId, uint256 nonce, uint128 amount) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(REFUND_TYPEHASH, channelId, nonce, amount))); + } + + /// @notice EIP-712 digest for the signed `ClaimBatch` (used by `claimWithSignature` and off-chain signing). + /// + /// @param voucherClaims The claim rows hashed into the batch. + /// + /// @return The typed data hash `receiverAuthorizer` signs for the batch. + /// + /// @dev `claim` and `claimWithSignature` revert with `EmptyBatch()` when `voucherClaims.length == 0`, so they + /// never call this with an empty array. The digest for `length == 0` is still defined (`entriesRoot = keccak256("")`) + /// for `eth_call` and off-chain tools to match this contract’s EIP-712 encoding without duplicating hashing logic. + function getClaimBatchDigest( + VoucherClaim[] calldata voucherClaims + ) public view returns (bytes32) { + uint256 n = voucherClaims.length; + bytes32 entriesRoot; + if (n == 0) { + entriesRoot = keccak256(""); + } else { + bytes32[] memory entryHashes = new bytes32[](n); + for (uint256 i = 0; i < n; ++i) { + bytes32 cid = getChannelId(voucherClaims[i].voucher.channel); + entryHashes[i] = keccak256( + abi.encode( + CLAIM_ENTRY_TYPEHASH, + cid, + voucherClaims[i].voucher.maxClaimableAmount, + voucherClaims[i].totalClaimed + ) + ); + } + entriesRoot = keccak256(abi.encodePacked(entryHashes)); + } + return _hashTypedDataV4(keccak256(abi.encode(CLAIM_BATCH_TYPEHASH, entriesRoot))); + } + + // ========================================================================= + // Internal Helpers + // ========================================================================= + + /// @dev True if `sender` may submit a claim for this channel (`receiver` or `receiverAuthorizer`). + function _isReceiverSide(address sender, ChannelConfig calldata config) internal pure returns (bool) { + return sender == config.receiverAuthorizer || sender == config.receiver; + } + + /// @dev Validates the payer signature, updates `channels` and `receivers`, and emits `Claimed`. + function _processVoucherClaim( + VoucherClaim calldata vc + ) internal { + bytes32 channelId = getChannelId(vc.voucher.channel); + ChannelState storage ch = channels[channelId]; + + if (vc.totalClaimed <= ch.totalClaimed) return; + if (vc.totalClaimed > vc.voucher.maxClaimableAmount) { + revert ClaimExceedsCeiling(); + } + if (vc.totalClaimed > ch.balance) revert ClaimExceedsBalance(); + + bytes32 digest = getVoucherDigest(channelId, vc.voucher.maxClaimableAmount); + + address payerAuth = vc.voucher.channel.payerAuthorizer; + if (payerAuth != address(0)) { + address recovered = ECDSA.recoverCalldata(digest, vc.signature); + if (recovered != payerAuth) revert InvalidSignature(); + } else { + if (!SignatureChecker.isValidSignatureNow(vc.voucher.channel.payer, digest, vc.signature)) { + revert InvalidSignature(); + } + } + + uint128 claimDelta = vc.totalClaimed - ch.totalClaimed; + ch.totalClaimed = vc.totalClaimed; + receivers[vc.voucher.channel.receiver][vc.voucher.channel.token].totalClaimed += claimDelta; + + emit Claimed(channelId, msg.sender, claimDelta, vc.totalClaimed); + } + + /// @dev Caps refund to available unclaimed escrow, reduces or clears any pending withdrawal, transfers to payer. + /// + /// @dev Increments `refundNonce[channelId]` **first** (before computing the capped refund). That applies to both + /// `refund` and `refundWithSignature`, including when `amount > 0` but available unclaimed escrow is zero + /// (no transfer, no `Refunded` event). This ties replay protection for signed refunds to the same counter + /// used whenever the receiver side executes a cooperative refund through this helper. `amount == 0` reverts + /// with `ZeroRefund` and does not bump the nonce. + function _executeRefund(ChannelConfig calldata config, uint128 amount) internal { + if (amount == 0) revert ZeroRefund(); + + bytes32 channelId = getChannelId(config); + unchecked { + refundNonce[channelId]++; + } + + ChannelState storage ch = channels[channelId]; + uint128 available = ch.balance - ch.totalClaimed; + uint128 refundAmount = amount > available ? available : amount; + if (refundAmount == 0) return; + + ch.balance -= refundAmount; + + WithdrawalState storage pws = pendingWithdrawals[channelId]; + if (pws.initiatedAt != 0) { + if (refundAmount >= pws.amount) { + pws.amount = 0; + pws.initiatedAt = 0; + } else { + pws.amount -= refundAmount; + } + } + + emit Refunded(channelId, msg.sender, refundAmount); + + if (ch.balance == 0 && ch.totalClaimed == 0) { + emit ChannelClosed(channelId, config); + } + + IERC20(config.token).safeTransfer(config.payer, refundAmount); + } +} diff --git a/contracts/x402ExactPermit2Proxy.sol b/contracts/x402ExactPermit2Proxy.sol new file mode 100644 index 0000000..56e0e5a --- /dev/null +++ b/contracts/x402ExactPermit2Proxy.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {x402BasePermit2Proxy} from "./x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol"; + +/** + * @title x402ExactPermit2Proxy + * @notice Trustless proxy for x402 payments using Permit2 with exact amount transfers + * + * @dev This contract acts as the authorized spender in Permit2 signatures. + * It uses the "witness" pattern to cryptographically bind the payment destination, + * preventing facilitators from redirecting funds. + * + * This contract always transfers the EXACT permitted amount, similar to + * EIP-3009's transferWithAuthorization behavior. + * + * @author x402 Protocol + */ +contract x402ExactPermit2Proxy is x402BasePermit2Proxy { + /// @notice EIP-712 type string for witness data + string public constant WITNESS_TYPE_STRING = + "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)"; + + /// @notice EIP-712 typehash for witness struct + bytes32 public constant WITNESS_TYPEHASH = keccak256("Witness(address to,uint256 validAfter)"); + + /** + * @notice Witness data structure for payment authorization + * @param to Destination address (immutable once signed) + * @param validAfter Earliest timestamp when payment can be settled + */ + struct Witness { + address to; + uint256 validAfter; + } + + constructor( + address _permit2 + ) x402BasePermit2Proxy(_permit2) {} + + /** + * @notice Settles a payment using a Permit2 signature + * @dev This is the standard settlement path when user has already approved Permit2. + * Always transfers the exact permitted amount. + * @param permit The Permit2 transfer authorization + * @param owner The token owner (payer) + * @param witness The witness data containing destination and validity window + * @param signature The payer's signature over the permit and witness + */ + function settle( + ISignatureTransfer.PermitTransferFrom calldata permit, + address owner, + Witness calldata witness, + bytes calldata signature + ) external nonReentrant { + bytes32 witnessHash = keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.validAfter)); + _settle( + permit, + permit.permitted.amount, + owner, + witness.to, + witness.validAfter, + witnessHash, + WITNESS_TYPE_STRING, + signature + ); + emit Settled(); + } + + /** + * @notice Settles a payment using both EIP-2612 permit and Permit2 signature + * @dev Enables fully gasless flow for tokens supporting EIP-2612. + * First submits the EIP-2612 permit to approve Permit2, then settles. + * Always transfers the exact permitted amount. + * @param permit2612 The EIP-2612 permit parameters + * @param permit The Permit2 transfer authorization + * @param owner The token owner (payer) + * @param witness The witness data containing destination and validity window + * @param signature The payer's signature over the permit and witness + * + * @dev This function will succeed even if the EIP-2612 permit fails, + * as long as the Permit2 approval already exists + */ + function settleWithPermit( + EIP2612Permit calldata permit2612, + ISignatureTransfer.PermitTransferFrom calldata permit, + address owner, + Witness calldata witness, + bytes calldata signature + ) external nonReentrant { + _executePermit(permit.permitted.token, owner, permit2612, permit.permitted.amount); + bytes32 witnessHash = keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.validAfter)); + _settle( + permit, + permit.permitted.amount, + owner, + witness.to, + witness.validAfter, + witnessHash, + WITNESS_TYPE_STRING, + signature + ); + emit SettledWithPermit(); + } +} diff --git a/contracts/x402UptoPermit2Proxy.sol b/contracts/x402UptoPermit2Proxy.sol new file mode 100644 index 0000000..236e9e8 --- /dev/null +++ b/contracts/x402UptoPermit2Proxy.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {x402BasePermit2Proxy} from "./x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol"; + +/** + * @title x402UptoPermit2Proxy + * @notice Trustless proxy for x402 payments using Permit2 with flexible amount transfers + * + * @dev This contract acts as the authorized spender in Permit2 signatures. + * It uses the "witness" pattern to cryptographically bind the payment destination, + * preventing facilitators from redirecting funds. + * + * This contract allows the facilitator to transfer up to the permitted amount. + * The witness includes a facilitator field so only the authorized caller can + * choose the final settlement amount. + * + * @author x402 Protocol + */ +contract x402UptoPermit2Proxy is x402BasePermit2Proxy { + /// @notice EIP-712 type string for witness data + string public constant WITNESS_TYPE_STRING = + "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)"; + + /// @notice EIP-712 typehash for witness struct + bytes32 public constant WITNESS_TYPEHASH = keccak256("Witness(address to,address facilitator,uint256 validAfter)"); + + /// @notice Thrown when msg.sender does not match the facilitator in the witness + error UnauthorizedFacilitator(); + + /// @notice Thrown when requested amount exceeds permitted amount + error AmountExceedsPermitted(); + + /** + * @notice Witness data structure for payment authorization + * @param to Destination address (immutable once signed) + * @param facilitator Address authorized to settle this payment (must be msg.sender) + * @param validAfter Earliest timestamp when payment can be settled + */ + struct Witness { + address to; + address facilitator; + uint256 validAfter; + } + + constructor( + address _permit2 + ) x402BasePermit2Proxy(_permit2) {} + + /** + * @notice Settles a payment using a Permit2 signature + * @dev This is the standard settlement path when user has already approved Permit2 + * @param permit The Permit2 transfer authorization + * @param amount The amount to transfer (must be <= permit.permitted.amount) + * @param owner The token owner (payer) + * @param witness The witness data containing destination and validity window + * @param signature The payer's signature over the permit and witness + */ + function settle( + ISignatureTransfer.PermitTransferFrom calldata permit, + uint256 amount, + address owner, + Witness calldata witness, + bytes calldata signature + ) external nonReentrant { + if (amount > permit.permitted.amount) revert AmountExceedsPermitted(); + if (msg.sender != witness.facilitator) revert UnauthorizedFacilitator(); + bytes32 witnessHash = + keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.facilitator, witness.validAfter)); + _settle(permit, amount, owner, witness.to, witness.validAfter, witnessHash, WITNESS_TYPE_STRING, signature); + emit Settled(); + } + + /** + * @notice Settles a payment using both EIP-2612 permit and Permit2 signature + * @dev Enables fully gasless flow for tokens supporting EIP-2612. + * First submits the EIP-2612 permit to approve Permit2, then settles. + * @param permit2612 The EIP-2612 permit parameters + * @param permit The Permit2 transfer authorization + * @param amount The amount to transfer (must be <= permit.permitted.amount) + * @param owner The token owner (payer) + * @param witness The witness data containing destination and validity window + * @param signature The payer's signature over the permit and witness + * + * @dev This function will succeed even if the EIP-2612 permit fails, + * as long as the Permit2 approval already exists + */ + function settleWithPermit( + EIP2612Permit calldata permit2612, + ISignatureTransfer.PermitTransferFrom calldata permit, + uint256 amount, + address owner, + Witness calldata witness, + bytes calldata signature + ) external nonReentrant { + if (amount > permit.permitted.amount) revert AmountExceedsPermitted(); + if (msg.sender != witness.facilitator) revert UnauthorizedFacilitator(); + _executePermit(permit.permitted.token, owner, permit2612, permit.permitted.amount); + bytes32 witnessHash = + keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.facilitator, witness.validAfter)); + _settle(permit, amount, owner, witness.to, witness.validAfter, witnessHash, WITNESS_TYPE_STRING, signature); + emit SettledWithPermit(); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index d49c522..7e5e66f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,6 +8,7 @@ import * as dotenv from 'dotenv' dotenv.config() const settings = { + evmVersion: 'cancun', optimizer: { enabled: true, // enabled for optimizer runs: 999999, // runs time for optimizer run diff --git a/package-lock.json b/package-lock.json index deb5329..f1db76e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,16 @@ { - "name": "my-sunhat-project", + "name": "x402-contracts", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "my-sunhat-project", + "name": "x402-contracts", "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "@openzeppelin/contracts": "^5.6.1" + }, "devDependencies": { "@nomicfoundation/hardhat-foundry": "^1.1.3", "@nomicfoundation/hardhat-toolbox": "^5.0.0", @@ -39,6 +43,7 @@ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -892,6 +897,7 @@ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -901,7 +907,8 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -909,6 +916,7 @@ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -959,6 +967,7 @@ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -973,6 +982,7 @@ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 8" } @@ -983,6 +993,7 @@ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1234,6 +1245,7 @@ "integrity": "sha512-JdKFxYknTfOYtFXMN6iFJ1vALJPednuB+9p9OwGIRdoI6HYSh4ZBzyRURgyXtHFyaJ/SF9lBpsYV9/1zEpcYwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/address": "5.6.1", "@nomicfoundation/solidity-analyzer": "^0.1.1", @@ -1262,6 +1274,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/bignumber": "^5.6.2", "@ethersproject/bytes": "^5.6.1", @@ -1276,6 +1289,7 @@ "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "nofilter": "^3.1.0" }, @@ -1288,7 +1302,8 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-ui/-/ignition-ui-0.15.13.tgz", "integrity": "sha512-HbTszdN1iDHCkUS9hLeooqnLEW2U45FaqFwFEYT8nIno2prFZhG+n68JEERjmfFCB5u0WgbuJwk3CgLoqtSL7Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.2", @@ -1438,6 +1453,12 @@ "node": ">= 4.0.0" } }, + "node_modules/@openzeppelin/contracts": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.6.1.tgz", + "integrity": "sha512-Ly6SlsVJ3mj+b18W3R8gNufB7dTICT105fJhodGAGgyC2oqnBAhqSiNDJ8V8DLY05cCz81GLI0CU5vNYA1EC/w==", + "license": "MIT" + }, "node_modules/@scure/base": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", @@ -1693,6 +1714,7 @@ "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "antlr4ts": "^0.5.0-alpha.4" } @@ -1803,28 +1825,32 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@typechain/ethers-v6": { "version": "0.5.1", @@ -1849,6 +1875,7 @@ "integrity": "sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fs-extra": "^9.1.0" }, @@ -1865,6 +1892,7 @@ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -1881,6 +1909,7 @@ "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1899,6 +1928,7 @@ "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "*" } @@ -1909,6 +1939,7 @@ "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1919,6 +1950,7 @@ "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1939,7 +1971,8 @@ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/mocha": { "version": "10.0.10", @@ -1966,6 +1999,7 @@ "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1975,7 +2009,8 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/qs": { "version": "6.14.0", @@ -1990,6 +2025,7 @@ "integrity": "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1999,7 +2035,8 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/acorn": { "version": "8.15.0", @@ -2007,6 +2044,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2020,6 +2058,7 @@ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "acorn": "^8.11.0" }, @@ -2077,6 +2116,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2095,6 +2135,7 @@ "dev": true, "license": "BSD-3-Clause OR MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.4.2" } @@ -2166,7 +2207,8 @@ "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/anymatch": { "version": "3.1.3", @@ -2187,7 +2229,8 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/argparse": { "version": "2.0.1", @@ -2202,6 +2245,7 @@ "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -2212,6 +2256,7 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2222,6 +2267,7 @@ "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2231,7 +2277,8 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/assertion-error": { "version": "1.1.0", @@ -2239,6 +2286,7 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -2249,6 +2297,7 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2258,7 +2307,8 @@ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/asynckit": { "version": "0.4.0", @@ -2273,6 +2323,7 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -2283,6 +2334,7 @@ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -2316,6 +2368,7 @@ "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -2355,7 +2408,8 @@ "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/bn.js": { "version": "5.2.2", @@ -2406,6 +2460,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2444,6 +2499,7 @@ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -2459,6 +2515,7 @@ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "base-x": "^3.0.2" } @@ -2469,6 +2526,7 @@ "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", @@ -2487,7 +2545,8 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/bytes": { "version": "3.1.2", @@ -2505,6 +2564,7 @@ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -2567,7 +2627,8 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/cbor": { "version": "8.1.0", @@ -2575,6 +2636,7 @@ "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "nofilter": "^3.1.0" }, @@ -2608,6 +2670,7 @@ "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", "dev": true, "license": "WTFPL", + "peer": true, "dependencies": { "check-error": "^1.0.2" }, @@ -2638,6 +2701,7 @@ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -2648,6 +2712,7 @@ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-func-name": "^2.0.2" }, @@ -2693,6 +2758,7 @@ "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", @@ -2731,6 +2797,7 @@ "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "object-assign": "^4.1.0", "string-width": "^2.1.1" @@ -2748,6 +2815,7 @@ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -2758,6 +2826,7 @@ "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -2768,6 +2837,7 @@ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -2782,6 +2852,7 @@ "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^3.0.0" }, @@ -2857,6 +2928,7 @@ "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", @@ -2873,6 +2945,7 @@ "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-back": "^4.0.2", "chalk": "^2.4.2", @@ -2889,6 +2962,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -2902,6 +2976,7 @@ "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2912,6 +2987,7 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2927,6 +3003,7 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -2936,7 +3013,8 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/command-line-usage/node_modules/escape-string-regexp": { "version": "1.0.5", @@ -2944,6 +3022,7 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -2954,6 +3033,7 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -2964,6 +3044,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -2977,6 +3058,7 @@ "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2996,7 +3078,8 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/concat-stream": { "version": "1.6.2", @@ -3007,6 +3090,7 @@ "node >= 0.8" ], "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -3019,7 +3103,8 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/concat-stream/node_modules/readable-stream": { "version": "2.3.8", @@ -3027,6 +3112,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3042,7 +3128,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/concat-stream/node_modules/string_decoder": { "version": "1.1.1", @@ -3050,6 +3137,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -3069,7 +3157,8 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/create-hash": { "version": "1.2.0", @@ -3077,6 +3166,7 @@ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -3091,6 +3181,7 @@ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -3105,7 +3196,8 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/crypt": { "version": "0.0.2", @@ -3113,6 +3205,7 @@ "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -3131,7 +3224,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/death/-/death-1.1.0.tgz", "integrity": "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -3170,6 +3264,7 @@ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-detect": "^4.0.0" }, @@ -3183,6 +3278,7 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4.0.0" } @@ -3192,7 +3288,8 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/define-data-property": { "version": "1.1.4", @@ -3200,6 +3297,7 @@ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3247,6 +3345,7 @@ "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", "dev": true, + "peer": true, "dependencies": { "heap": ">= 0.2.0" }, @@ -3260,6 +3359,7 @@ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "path-type": "^4.0.0" }, @@ -3434,6 +3534,7 @@ "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esprima": "^2.7.1", "estraverse": "^1.9.1", @@ -3457,6 +3558,7 @@ "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -3470,6 +3572,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3480,6 +3583,7 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3490,6 +3594,7 @@ "integrity": "sha512-femhvoAM7wL0GcI8ozTdxfuBtBFJ9qsyIAsmKVjlWAHUbdnnXHt+lKzz/kmldM5lA9jLuNHGwuIxorNpLbR1Zw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@solidity-parser/parser": "^0.14.0", "axios": "^1.5.1", @@ -3525,7 +3630,8 @@ "url": "https://paulmillr.com/funding/" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eth-gas-reporter/node_modules/@scure/base": { "version": "1.1.9", @@ -3533,6 +3639,7 @@ "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://paulmillr.com/funding/" } @@ -3549,6 +3656,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "~1.2.0", "@noble/secp256k1": "~1.7.0", @@ -3567,6 +3675,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "~1.2.0", "@scure/base": "~1.1.0" @@ -3578,6 +3687,7 @@ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -3590,6 +3700,7 @@ "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "1.2.0", "@noble/secp256k1": "1.7.1", @@ -3613,6 +3724,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -3652,6 +3764,7 @@ "integrity": "sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "^1.4.0" } @@ -3662,6 +3775,7 @@ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3675,6 +3789,7 @@ "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", @@ -3699,6 +3814,7 @@ "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", "dev": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "@types/bn.js": "^5.1.0", "bn.js": "^5.1.2", @@ -3726,7 +3842,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", @@ -3792,6 +3907,7 @@ "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bn.js": "4.11.6", "number-to-bn": "1.7.0" @@ -3806,7 +3922,8 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eventemitter3": { "version": "3.1.2", @@ -3821,6 +3938,7 @@ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -3831,7 +3949,8 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-glob": { "version": "3.3.3", @@ -3839,6 +3958,7 @@ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3855,7 +3975,8 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-uri": { "version": "3.1.0", @@ -3872,7 +3993,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.20.1", @@ -3880,6 +4002,7 @@ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "reusify": "^1.0.4" } @@ -3927,6 +4050,7 @@ "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-back": "^3.0.1" }, @@ -3998,6 +4122,7 @@ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-callable": "^1.2.7" }, @@ -4065,7 +4190,8 @@ "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fs.realpath": { "version": "1.0.0", @@ -4115,6 +4241,7 @@ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -4150,6 +4277,7 @@ "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -4174,6 +4302,7 @@ "integrity": "sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "chalk": "^2.4.2", "node-emoji": "^1.10.0" @@ -4188,6 +4317,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -4201,6 +4331,7 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4216,6 +4347,7 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -4225,7 +4357,8 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { "version": "1.0.5", @@ -4233,6 +4366,7 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -4243,6 +4377,7 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -4253,6 +4388,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -4297,6 +4433,7 @@ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "global-prefix": "^3.0.0" }, @@ -4310,6 +4447,7 @@ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", @@ -4325,6 +4463,7 @@ "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/glob": "^7.1.1", "array-union": "^2.1.0", @@ -4345,6 +4484,7 @@ "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/minimatch": "*", "@types/node": "*" @@ -4357,6 +4497,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4378,6 +4519,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4411,6 +4553,7 @@ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -4433,6 +4576,7 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4443,7 +4587,6 @@ "integrity": "sha512-CPaMFgCU5+sLO0Kos82xWLGC9YldRRBRydj5JT4v00+ShAg4C6Up2jAgP9+dTPVkMOMTfQc05mOo2JreMX5z3A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ethereumjs/util": "^9.1.0", "@ethersproject/abi": "^5.1.2", @@ -4691,6 +4834,7 @@ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -4733,6 +4877,7 @@ "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", @@ -4748,7 +4893,8 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hash-base/node_modules/readable-stream": { "version": "2.3.8", @@ -4756,6 +4902,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4771,7 +4918,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hash-base/node_modules/string_decoder": { "version": "1.1.1", @@ -4779,6 +4927,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -4788,7 +4937,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hash.js": { "version": "1.1.7", @@ -4829,7 +4979,8 @@ "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -4849,6 +5000,7 @@ "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "caseless": "^0.12.0", "concat-stream": "^1.6.2", @@ -4886,6 +5038,7 @@ "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "^10.0.3" } @@ -4895,7 +5048,8 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/https-proxy-agent": { "version": "5.0.1", @@ -4930,6 +5084,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -4940,6 +5095,7 @@ "integrity": "sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -4996,7 +5152,8 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/injectpromise": { "version": "1.0.0", @@ -5011,6 +5168,7 @@ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -5044,6 +5202,7 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -5090,6 +5249,7 @@ "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.5.0", "npm": ">=3" @@ -5121,6 +5281,7 @@ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "which-typed-array": "^1.1.16" }, @@ -5149,14 +5310,16 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/js-sha3": { "version": "0.8.0", @@ -5183,7 +5346,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-stream-stringify": { "version": "3.1.6", @@ -5200,7 +5364,8 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -5208,6 +5373,7 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "json5": "lib/cli.js" }, @@ -5234,6 +5400,7 @@ "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -5260,6 +5427,7 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5270,6 +5438,7 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -5280,6 +5449,7 @@ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -5316,14 +5486,16 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -5331,14 +5503,16 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -5363,6 +5537,7 @@ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-func-name": "^2.0.1" } @@ -5389,14 +5564,16 @@ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/markdown-table": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/match-all": { "version": "1.2.7", @@ -5421,6 +5598,7 @@ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -5442,6 +5620,7 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 8" } @@ -5492,7 +5671,8 @@ "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/micro-packed": { "version": "0.7.3", @@ -5513,6 +5693,7 @@ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -5580,6 +5761,7 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5600,6 +5782,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -5738,6 +5921,7 @@ "integrity": "sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "json-stringify-safe": "^5.0.1", "minimist": "^1.2.5", @@ -5757,7 +5941,8 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-addon-api": { "version": "2.0.2", @@ -5793,6 +5978,7 @@ "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lodash": "^4.17.21" } @@ -5834,6 +6020,7 @@ "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12.19" } @@ -5844,6 +6031,7 @@ "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "abbrev": "1" }, @@ -5867,6 +6055,7 @@ "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bn.js": "4.11.6", "strip-hex-prefix": "1.0.0" @@ -5881,7 +6070,8 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -5889,6 +6079,7 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5951,6 +6142,7 @@ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", @@ -5968,7 +6160,8 @@ "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/os-tmpdir": { "version": "1.0.2", @@ -6032,7 +6225,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/path-exists": { "version": "4.0.0", @@ -6050,6 +6244,7 @@ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6084,6 +6279,7 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6094,6 +6290,7 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -6104,6 +6301,7 @@ "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", @@ -6142,6 +6340,7 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -6169,6 +6368,7 @@ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" } @@ -6178,6 +6378,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true, + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -6188,6 +6389,7 @@ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -6203,7 +6405,8 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/promise": { "version": "8.3.0", @@ -6211,6 +6414,7 @@ "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "asap": "~2.0.6" } @@ -6221,6 +6425,7 @@ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -6280,7 +6485,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/randombytes": { "version": "2.1.0", @@ -6341,6 +6547,7 @@ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dev": true, + "peer": true, "dependencies": { "resolve": "^1.1.6" }, @@ -6354,6 +6561,7 @@ "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimatch": "^3.0.5" }, @@ -6367,6 +6575,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6380,6 +6589,7 @@ "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -6390,6 +6600,7 @@ "integrity": "sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "req-from": "^2.0.0" }, @@ -6403,6 +6614,7 @@ "integrity": "sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "resolve-from": "^3.0.0" }, @@ -6426,6 +6638,7 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6449,6 +6662,7 @@ "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -6459,6 +6673,7 @@ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6470,6 +6685,7 @@ "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" @@ -6484,6 +6700,7 @@ "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", "dev": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "bn.js": "^5.2.0" }, @@ -6511,6 +6728,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "queue-microtask": "^1.2.2" } @@ -6549,6 +6767,7 @@ "integrity": "sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "abbrev": "1.0.x", "async": "1.x", @@ -6575,6 +6794,7 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -6586,6 +6806,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "inflight": "^1.0.4", "inherits": "2", @@ -6603,6 +6824,7 @@ "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6613,6 +6835,7 @@ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -6627,6 +6850,7 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -6641,6 +6865,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6653,7 +6878,8 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/sc-istanbul/node_modules/supports-color": { "version": "3.2.3", @@ -6661,6 +6887,7 @@ "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^1.0.0" }, @@ -6682,6 +6909,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "elliptic": "^6.5.7", "node-addon-api": "^5.0.0", @@ -6696,7 +6924,8 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "6.3.1", @@ -6724,6 +6953,7 @@ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -6741,7 +6971,8 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -6756,6 +6987,7 @@ "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dev": true, "license": "(MIT AND BSD-3-Clause)", + "peer": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", @@ -6777,6 +7009,7 @@ "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "charenc": ">= 0.0.1", "crypt": ">= 0.0.1" @@ -6791,6 +7024,7 @@ "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", @@ -6810,6 +7044,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6831,6 +7066,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6919,7 +7155,8 @@ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/slash": { "version": "3.0.0", @@ -6927,6 +7164,7 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6937,6 +7175,7 @@ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -6987,6 +7226,7 @@ "integrity": "sha512-5P8vnB6qVX9tt1MfuONtCTEaEGO/O4WuEidPHIAJjx4sktHHKhO3rFvnE0q8L30nWJPTrcqGQMT7jpE29B2qow==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "@ethersproject/abi": "^5.0.9", "@solidity-parser/parser": "^0.20.1", @@ -7020,7 +7260,8 @@ "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/solidity-coverage/node_modules/ansi-styles": { "version": "3.2.1", @@ -7028,6 +7269,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -7041,6 +7283,7 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -7056,6 +7299,7 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -7065,7 +7309,8 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/solidity-coverage/node_modules/escape-string-regexp": { "version": "1.0.5", @@ -7073,6 +7318,7 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.0" } @@ -7083,6 +7329,7 @@ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -7098,6 +7345,7 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -7108,6 +7356,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -7118,6 +7367,7 @@ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -7131,6 +7381,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -7144,6 +7395,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7154,6 +7406,7 @@ "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", "dev": true, "optional": true, + "peer": true, "dependencies": { "amdefine": ">=0.0.4" }, @@ -7188,6 +7441,7 @@ "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "readable-stream": "^3.0.0" } @@ -7197,7 +7451,8 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/stacktrace-parser": { "version": "0.1.11", @@ -7247,7 +7502,8 @@ "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", "dev": true, - "license": "WTFPL OR MIT" + "license": "WTFPL OR MIT", + "peer": true }, "node_modules/string-width": { "version": "4.2.3", @@ -7283,6 +7539,7 @@ "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-hex-prefixed": "1.0.0" }, @@ -7323,6 +7580,7 @@ "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "http-response-object": "^3.0.1", "sync-rpc": "^1.2.1", @@ -7338,6 +7596,7 @@ "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-port": "^3.1.0" } @@ -7348,6 +7607,7 @@ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", @@ -7365,6 +7625,7 @@ "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-back": "^4.0.1", "deep-extend": "~0.6.0", @@ -7381,6 +7642,7 @@ "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7391,6 +7653,7 @@ "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7401,6 +7664,7 @@ "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/concat-stream": "^1.6.0", "@types/form-data": "0.0.33", @@ -7423,7 +7687,8 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/then-request/node_modules/form-data": { "version": "2.5.5", @@ -7431,6 +7696,7 @@ "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7449,6 +7715,7 @@ "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readable-stream": "3" } @@ -7494,7 +7761,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7521,6 +7787,7 @@ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", @@ -7642,6 +7909,7 @@ "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "chalk": "^4.1.0", "command-line-args": "^5.1.1", @@ -7658,6 +7926,7 @@ "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "typescript": ">=3.7.0" } @@ -7713,6 +7982,7 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.3.1" } @@ -7737,6 +8007,7 @@ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "~1.1.2" }, @@ -7750,6 +8021,7 @@ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -7799,6 +8071,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -7815,6 +8088,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7836,6 +8110,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -7846,6 +8121,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7859,6 +8135,7 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -7872,6 +8149,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7882,6 +8160,7 @@ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -7896,7 +8175,8 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/typescript": { "version": "5.9.3", @@ -7919,6 +8199,7 @@ "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7930,6 +8211,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -7955,7 +8237,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/universalify": { "version": "2.0.1", @@ -7982,7 +8265,8 @@ "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -8006,7 +8290,8 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/validator": { "version": "13.15.26", @@ -8034,6 +8319,7 @@ "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", "dev": true, "license": "LGPL-3.0", + "peer": true, "dependencies": { "@ethereumjs/util": "^8.1.0", "bn.js": "^5.2.1", @@ -8054,6 +8340,7 @@ "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", "dev": true, "license": "MPL-2.0", + "peer": true, "bin": { "rlp": "bin/rlp" }, @@ -8067,6 +8354,7 @@ "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", "dev": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "@ethereumjs/rlp": "^4.0.1", "ethereum-cryptography": "^2.0.0", @@ -8082,6 +8370,7 @@ "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "1.4.0" }, @@ -8095,6 +8384,7 @@ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 16" }, @@ -8108,6 +8398,7 @@ "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", @@ -8121,6 +8412,7 @@ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8134,6 +8426,7 @@ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -8169,6 +8462,7 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8178,7 +8472,8 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/wordwrapjs": { "version": "4.0.1", @@ -8186,6 +8481,7 @@ "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "reduce-flatten": "^2.0.0", "typical": "^5.2.0" @@ -8200,6 +8496,7 @@ "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -8319,6 +8616,7 @@ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index ddd9163..35739b8 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,8 @@ "deploy-nile": "hardhat deploy --network nile", "deploy-shasta": "hardhat deploy --network shasta", "postinstall": "sh scripts/postinstall.sh && echo 'Post-installation script executed successfully.'" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.6.1" } } diff --git a/test/MockGenericERC20.t.sol b/test/MockGenericERC20.t.sol new file mode 100644 index 0000000..877dde7 --- /dev/null +++ b/test/MockGenericERC20.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {MockGenericERC20} from "../contracts/mocks/MockGenericERC20.sol"; + +contract MockGenericERC20Test is Test { + function test_metadata_andMint() public { + MockGenericERC20 token = new MockGenericERC20(); + assertEq(token.name(), "Mock Generic ERC20"); + assertEq(token.symbol(), "MOCK"); + assertEq(token.decimals(), 6); + + address user = makeAddr("user"); + token.mint(user, 1000e6); + assertEq(token.balanceOf(user), 1000e6); + } +} diff --git a/test/invariants/X402ExactInvariants.t.sol b/test/invariants/X402ExactInvariants.t.sol new file mode 100644 index 0000000..700ff12 --- /dev/null +++ b/test/invariants/X402ExactInvariants.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {x402ExactPermit2Proxy} from "../../contracts/x402ExactPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../../contracts/x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "../../contracts/interfaces/ISignatureTransfer.sol"; +import {MockPermit2} from "../mocks/MockPermit2.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; + +contract X402ExactHandler is Test { + x402ExactPermit2Proxy public proxy; + MockPermit2 public mockPermit2; + MockERC20 public token; + + address public payer; + address public recipient; + + uint256 public totalSettled; + uint256 public settleCallCount; + + constructor( + x402ExactPermit2Proxy _proxy, + MockPermit2 _mockPermit2, + MockERC20 _token, + address _payer, + address _recipient + ) { + proxy = _proxy; + mockPermit2 = _mockPermit2; + token = _token; + payer = _payer; + recipient = _recipient; + } + + function settle(uint256 amount, uint256 nonce) external { + amount = bound(amount, 0, token.balanceOf(payer)); + if (amount == 0) return; + + uint256 t = block.timestamp; + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: amount}), + nonce: nonce, + deadline: t + 3600 + }); + + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t > 60 ? t - 60 : 0}); + + bytes memory sig = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + + try proxy.settle(permit, payer, witness, sig) { + totalSettled += amount; + settleCallCount++; + } catch {} + } +} + +contract X402ExactInvariantsTest is Test { + x402ExactPermit2Proxy public proxy; + MockPermit2 public mockPermit2; + MockERC20 public token; + X402ExactHandler public handler; + + address public payer; + address public recipient; + + uint256 constant MINT_AMOUNT = 1_000_000e6; + + function setUp() public { + payer = makeAddr("payer"); + recipient = makeAddr("recipient"); + + mockPermit2 = new MockPermit2(); + proxy = new x402ExactPermit2Proxy(address(mockPermit2)); + token = new MockERC20("USDC", "USDC", 6); + + token.mint(payer, MINT_AMOUNT); + vm.prank(payer); + token.approve(address(mockPermit2), type(uint256).max); + mockPermit2.setShouldActuallyTransfer(true); + + handler = new X402ExactHandler(proxy, mockPermit2, token, payer, recipient); + targetContract(address(handler)); + } + + /// @notice Proxy must never hold tokens - all transfers go directly to recipient + function invariant_proxyNeverHoldsTokens() public view { + assertEq(token.balanceOf(address(proxy)), 0); + } + + /// @notice Total settled cannot exceed minted supply + function invariant_settledNeverExceedsMinted() public view { + assertLe(handler.totalSettled(), MINT_AMOUNT); + } + + /// @notice Token conservation: sum of all balances equals minted amount + function invariant_tokenConservation() public view { + uint256 total = token.balanceOf(payer) + token.balanceOf(recipient) + token.balanceOf(address(proxy)) + + token.balanceOf(address(mockPermit2)); + assertEq(total, MINT_AMOUNT); + } +} diff --git a/test/invariants/X402UptoInvariants.t.sol b/test/invariants/X402UptoInvariants.t.sol new file mode 100644 index 0000000..b097fe2 --- /dev/null +++ b/test/invariants/X402UptoInvariants.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {x402UptoPermit2Proxy} from "../../contracts/x402UptoPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../../contracts/x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "../../contracts/interfaces/ISignatureTransfer.sol"; +import {MockPermit2} from "../mocks/MockPermit2.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; + +contract X402UptoHandler is Test { + x402UptoPermit2Proxy public proxy; + MockPermit2 public mockPermit2; + MockERC20 public token; + + address public payer; + address public recipient; + + uint256 public totalSettled; + uint256 public settleCallCount; + + constructor( + x402UptoPermit2Proxy _proxy, + MockPermit2 _mockPermit2, + MockERC20 _token, + address _payer, + address _recipient + ) { + proxy = _proxy; + mockPermit2 = _mockPermit2; + token = _token; + payer = _payer; + recipient = _recipient; + } + + function settle(uint256 amount, uint256 nonce) external { + amount = bound(amount, 0, token.balanceOf(payer)); + if (amount == 0) return; + + uint256 t = block.timestamp; + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: amount}), + nonce: nonce, + deadline: t + 3600 + }); + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t > 60 ? t - 60 : 0}); + + bytes memory sig = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + + // Upto variant includes the amount parameter + try proxy.settle(permit, amount, payer, witness, sig) { + totalSettled += amount; + settleCallCount++; + } catch {} + } +} + +contract X402UptoInvariantsTest is Test { + x402UptoPermit2Proxy public proxy; + MockPermit2 public mockPermit2; + MockERC20 public token; + X402UptoHandler public handler; + + address public payer; + address public recipient; + + uint256 constant MINT_AMOUNT = 1_000_000e6; + + function setUp() public { + payer = makeAddr("payer"); + recipient = makeAddr("recipient"); + + mockPermit2 = new MockPermit2(); + proxy = new x402UptoPermit2Proxy(address(mockPermit2)); + token = new MockERC20("USDC", "USDC", 6); + + token.mint(payer, MINT_AMOUNT); + vm.prank(payer); + token.approve(address(mockPermit2), type(uint256).max); + mockPermit2.setShouldActuallyTransfer(true); + + handler = new X402UptoHandler(proxy, mockPermit2, token, payer, recipient); + targetContract(address(handler)); + } + + /// @notice Proxy must never hold tokens - all transfers go directly to recipient + function invariant_proxyNeverHoldsTokens() public view { + assertEq(token.balanceOf(address(proxy)), 0); + } + + /// @notice Total settled cannot exceed minted supply + function invariant_settledNeverExceedsMinted() public view { + assertLe(handler.totalSettled(), MINT_AMOUNT); + } + + /// @notice Token conservation: sum of all balances equals minted amount + function invariant_tokenConservation() public view { + uint256 total = token.balanceOf(payer) + token.balanceOf(recipient) + token.balanceOf(address(proxy)) + + token.balanceOf(address(mockPermit2)); + assertEq(total, MINT_AMOUNT); + } +} diff --git a/test/MockERC20.sol b/test/legacy/MockERC20.sol similarity index 100% rename from test/MockERC20.sol rename to test/legacy/MockERC20.sol diff --git a/test/PaymentPermit.t.sol b/test/legacy/PaymentPermit.t.sol similarity index 98% rename from test/PaymentPermit.t.sol rename to test/legacy/PaymentPermit.t.sol index e9188c2..51c9d80 100644 --- a/test/PaymentPermit.t.sol +++ b/test/legacy/PaymentPermit.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "../contracts/PaymentPermit.sol"; +import "../../contracts/legacy/PaymentPermit.sol"; import "./MockERC20.sol"; -import "../contracts/interface/IPaymentPermit.sol"; -import "../contracts/libraries/PermitHash.sol"; +import "../../contracts/legacy/interface/IPaymentPermit.sol"; +import "../../contracts/legacy/libraries/PermitHash.sol"; contract PaymentPermitTest is Test { using PermitHash for IPaymentPermit.PaymentPermitDetails; diff --git a/test/mocks/GasMockDepositCollector.sol b/test/mocks/GasMockDepositCollector.sol new file mode 100644 index 0000000..04b86e9 --- /dev/null +++ b/test/mocks/GasMockDepositCollector.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IDepositCollector} from "../../contracts/interfaces/IDepositCollector.sol"; + +/// @notice Gas harness collector for `x402BatchSettlement.gas.t.sol` only (kept separate so forge coverage +/// does not merge its bytecode with `MockDepositCollector` in `MockBatchDepositCollectors.sol`). +contract GasMockDepositCollector is IDepositCollector { + function collect(address payer, address token, uint256 amount, bytes32, bytes calldata) external override { + IERC20(token).transferFrom(payer, msg.sender, amount); + } +} diff --git a/test/mocks/MaliciousReentrantExact.sol b/test/mocks/MaliciousReentrantExact.sol new file mode 100644 index 0000000..1ab577f --- /dev/null +++ b/test/mocks/MaliciousReentrantExact.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ISignatureTransfer} from "../../contracts/interfaces/ISignatureTransfer.sol"; +import {x402ExactPermit2Proxy} from "../../contracts/x402ExactPermit2Proxy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MaliciousReentrantExact is ISignatureTransfer { + x402ExactPermit2Proxy public target; + bool public attemptReentry; + + ISignatureTransfer.PermitTransferFrom public storedPermit; + address public storedOwner; + x402ExactPermit2Proxy.Witness public storedWitness; + bytes public storedSignature; + + mapping(address => mapping(uint256 => uint256)) public nonceBitmapStorage; + + function setTarget( + address _target + ) external { + target = x402ExactPermit2Proxy(_target); + } + + function setAttemptReentry( + bool _attempt + ) external { + attemptReentry = _attempt; + } + + function setAttackParams( + ISignatureTransfer.PermitTransferFrom calldata permit, + address owner, + x402ExactPermit2Proxy.Witness calldata witness, + bytes calldata signature + ) external { + storedPermit = permit; + storedOwner = owner; + storedWitness = witness; + storedSignature = signature; + } + + function nonceBitmap(address owner, uint256 wordPos) external view override returns (uint256) { + return nonceBitmapStorage[owner][wordPos]; + } + + function permitTransferFrom( + PermitTransferFrom memory, + SignatureTransferDetails calldata, + address, + bytes calldata + ) external pure override { + revert("Use permitWitnessTransferFrom"); + } + + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32, + string calldata, + bytes calldata + ) external override { + uint256 wordPos = permit.nonce >> 8; + uint256 bitPos = permit.nonce & 0xff; + nonceBitmapStorage[owner][wordPos] |= (1 << bitPos); + + if (attemptReentry && address(target) != address(0)) { + target.settle(storedPermit, storedOwner, storedWitness, storedSignature); + } + + IERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); + } +} diff --git a/test/mocks/MaliciousReentrantUpto.sol b/test/mocks/MaliciousReentrantUpto.sol new file mode 100644 index 0000000..219fccd --- /dev/null +++ b/test/mocks/MaliciousReentrantUpto.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ISignatureTransfer} from "../../contracts/interfaces/ISignatureTransfer.sol"; +import {x402UptoPermit2Proxy} from "../../contracts/x402UptoPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../../contracts/x402BasePermit2Proxy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MaliciousReentrantUpto is ISignatureTransfer { + x402UptoPermit2Proxy public target; + bool public attemptReentry; + + ISignatureTransfer.PermitTransferFrom public storedPermit; + uint256 public storedAmount; + address public storedOwner; + x402UptoPermit2Proxy.Witness public storedWitness; + bytes public storedSignature; + + mapping(address => mapping(uint256 => uint256)) public nonceBitmapStorage; + + function setTarget( + address _target + ) external { + target = x402UptoPermit2Proxy(_target); + } + + function setAttemptReentry( + bool _attempt + ) external { + attemptReentry = _attempt; + } + + function setAttackParams( + ISignatureTransfer.PermitTransferFrom calldata permit, + uint256 amount, + address owner, + x402UptoPermit2Proxy.Witness calldata witness, + bytes calldata signature + ) external { + storedPermit = permit; + storedAmount = amount; + storedOwner = owner; + storedWitness = witness; + storedSignature = signature; + } + + function nonceBitmap(address owner, uint256 wordPos) external view override returns (uint256) { + return nonceBitmapStorage[owner][wordPos]; + } + + function permitTransferFrom( + PermitTransferFrom memory, + SignatureTransferDetails calldata, + address, + bytes calldata + ) external pure override { + revert("Use permitWitnessTransferFrom"); + } + + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32, + string calldata, + bytes calldata + ) external override { + uint256 wordPos = permit.nonce >> 8; + uint256 bitPos = permit.nonce & 0xff; + nonceBitmapStorage[owner][wordPos] |= (1 << bitPos); + + if (attemptReentry && address(target) != address(0)) { + // Upto variant includes the amount parameter + target.settle(storedPermit, storedAmount, storedOwner, storedWitness, storedSignature); + } + + IERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); + } +} diff --git a/test/mocks/MockBatchDepositCollectors.sol b/test/mocks/MockBatchDepositCollectors.sol new file mode 100644 index 0000000..181572d --- /dev/null +++ b/test/mocks/MockBatchDepositCollectors.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IDepositCollector} from "../../contracts/interfaces/IDepositCollector.sol"; + +/// @notice Full-amount pull mock used by `x402BatchSettlement.t.sol`. +contract MockDepositCollector is IDepositCollector { + function collect(address payer, address token, uint256 amount, bytes32, bytes calldata) external override { + // Use a local for destination — identical gas semantics to GasMockDepositCollector but distinct bytecode + // so `forge coverage` does not attribute all hits to the gas harness contract only. + address to = msg.sender; + IERC20(token).transferFrom(payer, to, amount); + } +} + +/// @notice Pulls half the requested amount — used to trigger `DepositCollectionFailed` in tests. +contract MockShortCollector is IDepositCollector { + function collect(address payer, address token, uint256 amount, bytes32, bytes calldata) external override { + IERC20(token).transferFrom(payer, msg.sender, amount / 2); + } +} diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol new file mode 100644 index 0000000..99708e0 --- /dev/null +++ b/test/mocks/MockERC20.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + uint8 private _decimals; + + constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) { + _decimals = decimals_; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/test/mocks/MockERC20Permit.sol b/test/mocks/MockERC20Permit.sol new file mode 100644 index 0000000..b7835d9 --- /dev/null +++ b/test/mocks/MockERC20Permit.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +contract MockERC20Permit is ERC20, ERC20Permit { + uint8 private _decimals; + + enum RevertMode { + None, + RevertWithReason, + Panic, + CustomError + } + + RevertMode public revertMode; + string public permitRevertMessage; + + error MockCustomError(address sender); + + constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) ERC20Permit(name) { + _decimals = decimals_; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function permit(address owner, address spender, uint256 value, uint256, uint8, bytes32, bytes32) public override { + if (revertMode == RevertMode.RevertWithReason) { + revert(permitRevertMessage); + } else if (revertMode == RevertMode.Panic) { + uint256 zero = 0; + uint256 x = 1 / zero; + _approve(owner, spender, x); + return; + } else if (revertMode == RevertMode.CustomError) { + revert MockCustomError(msg.sender); + } + _approve(owner, spender, value); + } + + function setPermitRevert(bool _shouldRevert, string memory _message) external { + revertMode = _shouldRevert ? RevertMode.RevertWithReason : RevertMode.None; + permitRevertMessage = _message; + } + + function setRevertMode( + RevertMode _mode + ) external { + revertMode = _mode; + } +} diff --git a/test/mocks/MockERC3009Token.sol b/test/mocks/MockERC3009Token.sol new file mode 100644 index 0000000..1127fd0 --- /dev/null +++ b/test/mocks/MockERC3009Token.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @dev Mock ERC-20 with EIP-3009 receiveWithAuthorization support for testing. + * Does not verify the signature — simply transfers `value` from `from` to `to`. + */ +contract MockERC3009Token is ERC20 { + uint8 private _decimals; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) { + _decimals = decimals_; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256, + uint256, + bytes32, + bytes memory + ) external { + _transfer(from, to, value); + } +} diff --git a/test/mocks/MockPermit2.sol b/test/mocks/MockPermit2.sol new file mode 100644 index 0000000..4a1c825 --- /dev/null +++ b/test/mocks/MockPermit2.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ISignatureTransfer} from "../../contracts/interfaces/ISignatureTransfer.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockPermit2 is ISignatureTransfer { + mapping(address => mapping(uint256 => uint256)) public nonceBitmapStorage; + bool public shouldActuallyTransfer; + + function nonceBitmap(address owner, uint256 wordPos) external view override returns (uint256) { + return nonceBitmapStorage[owner][wordPos]; + } + + function permitTransferFrom( + PermitTransferFrom memory, + SignatureTransferDetails calldata, + address, + bytes calldata + ) external pure override { + revert("Use permitWitnessTransferFrom"); + } + + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32, + string calldata, + bytes calldata + ) external override { + uint256 wordPos = permit.nonce >> 8; + uint256 bitPos = permit.nonce & 0xff; + nonceBitmapStorage[owner][wordPos] |= (1 << bitPos); + + if (shouldActuallyTransfer) { + IERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); + } + } + + function setShouldActuallyTransfer( + bool _should + ) external { + shouldActuallyTransfer = _should; + } +} diff --git a/test/periphery/ERC3009DepositCollector.t.sol b/test/periphery/ERC3009DepositCollector.t.sol new file mode 100644 index 0000000..e2861a5 --- /dev/null +++ b/test/periphery/ERC3009DepositCollector.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ERC3009DepositCollector} from "../../contracts/periphery/ERC3009DepositCollector.sol"; +import {DepositCollector} from "../../contracts/periphery/DepositCollector.sol"; +import {MockERC3009Token} from "../mocks/MockERC3009Token.sol"; + +contract ERC3009DepositCollectorTest is Test { + ERC3009DepositCollector public collector; + MockERC3009Token public token; + + address public payer; + + uint256 constant AMOUNT = 1000e6; + + function setUp() public { + collector = new ERC3009DepositCollector(address(this)); + token = new MockERC3009Token("USDC3009", "USDC3009", 6); + + payer = makeAddr("payer"); + + token.mint(payer, 100_000e6); + } + + function test_collect_success() public { + uint256 validAfter = 0; + uint256 validBefore = block.timestamp + 3600; + uint256 salt = 1; + bytes memory signature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + + bytes memory collectorData = abi.encode(validAfter, validBefore, salt, signature); + + collector.collect(payer, address(token), AMOUNT, bytes32(0), collectorData); + + assertEq(token.balanceOf(address(this)), AMOUNT); + assertEq(token.balanceOf(payer), 100_000e6 - AMOUNT); + } + + function test_collect_transfersViaCollector() public { + bytes memory collectorData = abi.encode( + uint256(0), + uint256(block.timestamp + 3600), + uint256(42), + abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)) + ); + + uint256 collectorBalBefore = token.balanceOf(address(collector)); + collector.collect(payer, address(token), AMOUNT, bytes32(0), collectorData); + uint256 collectorBalAfter = token.balanceOf(address(collector)); + + assertEq(collectorBalAfter, collectorBalBefore); + assertEq(token.balanceOf(address(this)), AMOUNT); + } + + function test_collect_differentAmounts() public { + uint128 smallAmount = 1e6; + bytes memory collectorData = abi.encode( + uint256(0), + uint256(block.timestamp + 3600), + uint256(100), + abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)) + ); + + collector.collect(payer, address(token), smallAmount, bytes32(0), collectorData); + assertEq(token.balanceOf(address(this)), smallAmount); + } + + function test_collect_revert_onlyX402BatchSettlement() public { + bytes memory collectorData = abi.encode(uint256(0), uint256(block.timestamp + 3600), uint256(1), hex"dead"); + + vm.prank(makeAddr("attacker")); + vm.expectRevert(DepositCollector.OnlyX402BatchSettlement.selector); + collector.collect(payer, address(token), AMOUNT, bytes32(0), collectorData); + } +} diff --git a/test/periphery/Permit2DepositCollector.t.sol b/test/periphery/Permit2DepositCollector.t.sol new file mode 100644 index 0000000..d262597 --- /dev/null +++ b/test/periphery/Permit2DepositCollector.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Permit2DepositCollector} from "../../contracts/periphery/Permit2DepositCollector.sol"; +import {DepositCollector} from "../../contracts/periphery/DepositCollector.sol"; +import {MockPermit2} from "../mocks/MockPermit2.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; + +contract Permit2DepositCollectorTest is Test { + Permit2DepositCollector public collector; + MockPermit2 public mockPermit2; + MockERC20 public token; + + address public payer; + + uint256 constant AMOUNT = 1000e6; + + function setUp() public { + mockPermit2 = new MockPermit2(); + collector = new Permit2DepositCollector(address(this), address(mockPermit2)); + token = new MockERC20("USDC", "USDC", 6); + + payer = makeAddr("payer"); + + token.mint(payer, 100_000e6); + + vm.prank(payer); + token.approve(address(mockPermit2), type(uint256).max); + mockPermit2.setShouldActuallyTransfer(true); + } + + function _collectorData( + uint256 nonce, + uint256 deadline, + bytes memory signature + ) internal pure returns (bytes memory) { + return abi.encode(nonce, deadline, signature, bytes("")); + } + + function test_constructor_setsX402BatchSettlementAndPermit2() public view { + assertEq(collector.x402BatchSettlement(), address(this)); + assertEq(address(collector.PERMIT2()), address(mockPermit2)); + } + + function test_constructor_revert_zeroPermit2() public { + vm.expectRevert(Permit2DepositCollector.InvalidPermit2Address.selector); + new Permit2DepositCollector(address(this), address(0)); + } + + function test_constructor_revert_zeroX402BatchSettlement() public { + vm.expectRevert(DepositCollector.InvalidX402BatchSettlementAddress.selector); + new Permit2DepositCollector(address(0), address(mockPermit2)); + } + + function test_witnessConstants() public view { + assertEq(collector.DEPOSIT_WITNESS_TYPEHASH(), keccak256("DepositWitness(bytes32 channelId)")); + assertEq( + keccak256(bytes(collector.DEPOSIT_WITNESS_TYPE_STRING())), + keccak256( + "DepositWitness witness)DepositWitness(bytes32 channelId)TokenPermissions(address token,uint256 amount)" + ) + ); + } + + function test_collect_success() public { + bytes memory signature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + bytes memory collectorData = _collectorData(0, block.timestamp + 3600, signature); + + bytes32 channelId = keccak256("test-channel"); + collector.collect(payer, address(token), AMOUNT, channelId, collectorData); + + assertEq(token.balanceOf(address(this)), AMOUNT); + assertEq(token.balanceOf(payer), 100_000e6 - AMOUNT); + } + + function test_collect_directTransfer_noHop() public { + bytes memory signature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + bytes memory collectorData = _collectorData(0, block.timestamp + 3600, signature); + + bytes32 channelId = keccak256("test-channel"); + collector.collect(payer, address(token), AMOUNT, channelId, collectorData); + + assertEq(token.balanceOf(address(collector)), 0); + } + + function test_collect_consumesNonce() public { + bytes memory signature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + bytes memory collectorData = _collectorData(42, block.timestamp + 3600, signature); + + uint256 bitmapBefore = mockPermit2.nonceBitmap(payer, 0); + assertEq(bitmapBefore, 0); + + collector.collect(payer, address(token), AMOUNT, keccak256("ch"), collectorData); + + uint256 bitmapAfter = mockPermit2.nonceBitmap(payer, 0); + assertGt(bitmapAfter, 0); + } + + function test_collect_revert_onlyX402BatchSettlement() public { + bytes memory collectorData = _collectorData(0, block.timestamp + 3600, hex"dead"); + + vm.prank(makeAddr("attacker")); + vm.expectRevert(DepositCollector.OnlyX402BatchSettlement.selector); + collector.collect(payer, address(token), AMOUNT, keccak256("ch"), collectorData); + } +} diff --git a/test/periphery/Permit2WithERC2612DepositCollector.t.sol b/test/periphery/Permit2WithERC2612DepositCollector.t.sol new file mode 100644 index 0000000..c26bff5 --- /dev/null +++ b/test/periphery/Permit2WithERC2612DepositCollector.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Permit2DepositCollector} from "../../contracts/periphery/Permit2DepositCollector.sol"; +import {DepositCollector} from "../../contracts/periphery/DepositCollector.sol"; +import {MockPermit2} from "../mocks/MockPermit2.sol"; +import {MockERC20Permit} from "../mocks/MockERC20Permit.sol"; + +/// @dev Optional EIP-2612 segment on merged `Permit2DepositCollector`. +contract Permit2WithERC2612DepositCollectorTest is Test { + Permit2DepositCollector public collector; + MockPermit2 public mockPermit2; + MockERC20Permit public token; + + address public payer; + + uint256 constant AMOUNT = 1000e6; + + event EIP2612PermitFailedWithReason(address indexed token, address indexed owner, string reason); + event EIP2612PermitFailedWithPanic(address indexed token, address indexed owner, uint256 errorCode); + event EIP2612PermitFailedWithData(address indexed token, address indexed owner, bytes data); + + function setUp() public { + mockPermit2 = new MockPermit2(); + collector = new Permit2DepositCollector(address(this), address(mockPermit2)); + token = new MockERC20Permit("PermitUSDC", "pUSDC", 6); + + payer = makeAddr("payer"); + + token.mint(payer, 100_000e6); + + vm.prank(payer); + token.approve(address(mockPermit2), type(uint256).max); + mockPermit2.setShouldActuallyTransfer(true); + } + + function _makeCollectorData( + uint256 amount + ) internal view returns (bytes memory) { + bytes memory eip2612 = abi.encode(amount, block.timestamp + 3600, uint8(27), bytes32(0), bytes32(0)); + bytes memory signature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + return abi.encode(uint256(0), block.timestamp + 3600, signature, eip2612); + } + + function test_constructor_setsPermit2() public view { + assertEq(address(collector.PERMIT2()), address(mockPermit2)); + } + + function test_constructor_revert_zeroPermit2() public { + vm.expectRevert(Permit2DepositCollector.InvalidPermit2Address.selector); + new Permit2DepositCollector(address(this), address(0)); + } + + function test_collect_success() public { + bytes memory collectorData = _makeCollectorData(AMOUNT); + bytes32 channelId = keccak256("test-channel"); + + collector.collect(payer, address(token), AMOUNT, channelId, collectorData); + + assertEq(token.balanceOf(address(this)), AMOUNT); + assertEq(token.balanceOf(payer), 100_000e6 - AMOUNT); + } + + function test_collect_softFail_revertWithReason() public { + token.setPermitRevert(true, "ERC20Permit: invalid signature"); + + bytes memory collectorData = _makeCollectorData(AMOUNT); + bytes32 channelId = keccak256("test-channel"); + + vm.expectEmit(true, true, false, true); + emit EIP2612PermitFailedWithReason(address(token), payer, "ERC20Permit: invalid signature"); + + collector.collect(payer, address(token), AMOUNT, channelId, collectorData); + + assertEq(token.balanceOf(address(this)), AMOUNT); + } + + function test_collect_softFail_panic() public { + token.setRevertMode(MockERC20Permit.RevertMode.Panic); + + bytes memory collectorData = _makeCollectorData(AMOUNT); + bytes32 channelId = keccak256("test-channel"); + + vm.expectEmit(true, true, false, true); + emit EIP2612PermitFailedWithPanic(address(token), payer, 0x12); + + collector.collect(payer, address(token), AMOUNT, channelId, collectorData); + + assertEq(token.balanceOf(address(this)), AMOUNT); + } + + function test_collect_softFail_customError() public { + token.setRevertMode(MockERC20Permit.RevertMode.CustomError); + + bytes memory collectorData = _makeCollectorData(AMOUNT); + bytes32 channelId = keccak256("test-channel"); + + collector.collect(payer, address(token), AMOUNT, channelId, collectorData); + + assertEq(token.balanceOf(address(this)), AMOUNT); + } + + function test_collect_revert_amountMismatch() public { + bytes memory eip2612 = abi.encode(AMOUNT + 1, block.timestamp + 3600, uint8(27), bytes32(0), bytes32(0)); + bytes memory signature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + bytes memory collectorData = abi.encode(uint256(0), block.timestamp + 3600, signature, eip2612); + + vm.expectRevert(Permit2DepositCollector.Permit2612AmountMismatch.selector); + collector.collect(payer, address(token), AMOUNT, keccak256("ch"), collectorData); + } + + function test_collect_revert_onlyX402BatchSettlement() public { + bytes memory collectorData = _makeCollectorData(AMOUNT); + + vm.prank(makeAddr("attacker")); + vm.expectRevert(DepositCollector.OnlyX402BatchSettlement.selector); + collector.collect(payer, address(token), AMOUNT, keccak256("ch"), collectorData); + } +} diff --git a/test/x402BatchSettlement.fork.t.sol b/test/x402BatchSettlement.fork.t.sol new file mode 100644 index 0000000..a356a0e --- /dev/null +++ b/test/x402BatchSettlement.fork.t.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {x402BatchSettlement} from "../contracts/x402BatchSettlement.sol"; +import {Permit2DepositCollector} from "../contracts/periphery/Permit2DepositCollector.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; + +/// @title X402BatchSettlementForkTest +/// @notice Fork tests against real Permit2 deployment for the dual-authorizer channel model +/// @dev Run with: `forge test --match-contract X402BatchSettlementForkTest --fork-url $BASE_RPC_URL` +/// @dev Gas benchmarks (canonical Permit2): `forge test --match-test test_gas_fork --fork-url $BASE_RPC_URL -vv` +contract X402BatchSettlementForkTest is Test { + address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + bytes32 constant PERMIT2_DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + bytes32 constant PERMIT_WITNESS_TYPEHASH = keccak256( + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,DepositWitness witness)DepositWitness(bytes32 channelId)TokenPermissions(address token,uint256 amount)" + ); + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + bytes32 constant DEPOSIT_WITNESS_TYPEHASH = keccak256("DepositWitness(bytes32 channelId)"); + + x402BatchSettlement public settlement; + Permit2DepositCollector public permit2Collector; + MockERC20 public token; + + uint256 public payerKey; + address public payer; + uint256 public payerAuthKey; + address public payerAuthAddr; + uint256 public receiverKey; + address public receiverAddr; + uint256 public receiverAuthKey; + address public receiverAuthAddr; + + uint40 constant WITHDRAW_DELAY = 3600; + uint128 constant DEPOSIT_AMOUNT = 1000e6; + uint128 constant CLAIM_AMOUNT = 100e6; + + function setUp() public { + if (block.chainid == 31_337) return; + require(PERMIT2.code.length > 0, "Permit2 not deployed"); + + payerKey = uint256(keccak256("x402-batch-test-payer")); + payer = vm.addr(payerKey); + payerAuthKey = uint256(keccak256("x402-batch-test-payerAuth")); + payerAuthAddr = vm.addr(payerAuthKey); + receiverKey = uint256(keccak256("x402-batch-test-receiver")); + receiverAddr = vm.addr(receiverKey); + receiverAuthKey = uint256(keccak256("x402-batch-test-receiverAuth")); + receiverAuthAddr = vm.addr(receiverAuthKey); + + settlement = new x402BatchSettlement(); + permit2Collector = new Permit2DepositCollector(address(settlement), PERMIT2); + token = new MockERC20("USDC", "USDC", 6); + token.mint(payer, 100_000e6); + + vm.prank(payer); + token.approve(PERMIT2, type(uint256).max); + } + + modifier onlyFork() { + if (block.chainid == 31_337) return; + _; + } + + // ========================================================================= + // Helpers + // ========================================================================= + + function _makeConfig() internal view returns (x402BatchSettlement.ChannelConfig memory) { + return x402BatchSettlement.ChannelConfig({ + payer: payer, + payerAuthorizer: payerAuthAddr, + receiver: receiverAddr, + receiverAuthorizer: receiverAuthAddr, + token: address(token), + withdrawDelay: WITHDRAW_DELAY, + salt: bytes32(0) + }); + } + + function _channelId( + x402BatchSettlement.ChannelConfig memory config + ) internal view returns (bytes32) { + return settlement.getChannelId(config); + } + + function _getChannel( + bytes32 id + ) internal view returns (x402BatchSettlement.ChannelState memory ch) { + (ch.balance, ch.totalClaimed) = settlement.channels(id); + } + + function _permit2DomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(PERMIT2_DOMAIN_TYPEHASH, keccak256("Permit2"), block.chainid, PERMIT2)); + } + + function _nonce( + uint256 salt + ) internal view returns (uint256) { + return uint256(keccak256(abi.encodePacked(block.timestamp, block.number, salt))); + } + + function _signPermit2Deposit( + x402BatchSettlement.ChannelConfig memory config, + uint256 amount, + uint256 nonce, + uint256 deadline + ) internal view returns (bytes memory) { + bytes32 channelId = _channelId(config); + bytes32 witnessHash = keccak256(abi.encode(DEPOSIT_WITNESS_TYPEHASH, channelId)); + bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, address(token), amount)); + bytes32 structHash = keccak256( + abi.encode(PERMIT_WITNESS_TYPEHASH, tokenHash, address(permit2Collector), nonce, deadline, witnessHash) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _permit2DomainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerKey, digest); + return abi.encodePacked(r, s, v); + } + + function _domainSeparator() internal view returns (bytes32) { + (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = + settlement.eip712Domain(); + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + verifyingContract + ) + ); + } + + function _signVoucher(bytes32 channelId, uint128 maxClaimableAmount) internal view returns (bytes memory) { + bytes32 structHash = keccak256(abi.encode(settlement.VOUCHER_TYPEHASH(), channelId, maxClaimableAmount)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerAuthKey, digest); + return abi.encodePacked(r, s, v); + } + + function _signRefund(bytes32 channelId, uint256 refundNonce, uint128 amount) internal view returns (bytes memory) { + bytes32 structHash = keccak256(abi.encode(settlement.REFUND_TYPEHASH(), channelId, refundNonce, amount)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(receiverAuthKey, digest); + return abi.encodePacked(r, s, v); + } + + function _depositViaPermit2( + x402BatchSettlement.ChannelConfig memory config, + uint128 amount, + uint256 permitNonce, + uint256 deadline + ) internal { + bytes memory depositSig = _signPermit2Deposit(config, amount, permitNonce, deadline); + bytes memory collectorData = abi.encode(permitNonce, deadline, depositSig, bytes("")); + settlement.deposit(config, amount, address(permit2Collector), collectorData); + } + + // ========================================================================= + // Fork Tests + // ========================================================================= + + function test_fork_fullLifecycle_permit2() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + uint256 nonce = _nonce(0); + uint256 deadline = block.timestamp + 3600; + _depositViaPermit2(config, DEPOSIT_AMOUNT, nonce, deadline); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, DEPOSIT_AMOUNT); + + bytes memory voucherSig = _signVoucher(channelId, CLAIM_AMOUNT); + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: voucherSig, + totalClaimed: CLAIM_AMOUNT + }); + vm.prank(receiverAuthAddr); + settlement.claim(claims); + + uint256 balBefore = token.balanceOf(receiverAddr); + settlement.settle(receiverAddr, address(token)); + assertEq(token.balanceOf(receiverAddr), balBefore + CLAIM_AMOUNT); + } + + function test_fork_refund_afterPartialClaim() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + uint256 nonce = _nonce(0); + uint256 deadline = block.timestamp + 3600; + _depositViaPermit2(config, DEPOSIT_AMOUNT, nonce, deadline); + + bytes memory voucherSig = _signVoucher(channelId, CLAIM_AMOUNT); + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: voucherSig, + totalClaimed: CLAIM_AMOUNT + }); + vm.prank(receiverAuthAddr); + settlement.claim(claims); + + uint128 refundAmt = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + bytes memory refundSig = _signRefund(channelId, 0, refundAmt); + uint256 payerBalBefore = token.balanceOf(payer); + settlement.refundWithSignature(config, refundAmt, 0, refundSig); + + assertEq(token.balanceOf(payer), payerBalBefore + refundAmt); + } + + function test_fork_tamperedWitness_reverts() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + x402BatchSettlement.ChannelConfig memory tamperedConfig = _makeConfig(); + tamperedConfig.salt = bytes32(uint256(999)); + + uint256 nonce = _nonce(0); + uint256 deadline = block.timestamp + 3600; + bytes memory depositSig = _signPermit2Deposit(config, DEPOSIT_AMOUNT, nonce, deadline); + + bytes memory collectorData = abi.encode(nonce, deadline, depositSig, bytes("")); + + vm.expectRevert(); + settlement.deposit(tamperedConfig, DEPOSIT_AMOUNT, address(permit2Collector), collectorData); + } + + // ========================================================================= + // Gas benchmarks (canonical Permit2; requires `--fork-url`, e.g. Base or Ethereum mainnet) + // ========================================================================= + + function test_gas_fork_permit2_deposit_first() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + uint256 nonce = _nonce(10); + uint256 deadline = block.timestamp + 3600; + bytes memory depositSig = _signPermit2Deposit(config, DEPOSIT_AMOUNT, nonce, deadline); + bytes memory collectorData = abi.encode(nonce, deadline, depositSig, bytes("")); + + vm.prank(payer); + settlement.deposit(config, DEPOSIT_AMOUNT, address(permit2Collector), collectorData); + uint256 g = vm.snapshotGasLastCall("fork_deposit_permit2_first"); + console2.log("fork_deposit_permit2_first", g); + } + + function test_gas_fork_claim_one_voucher() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + uint256 nonce = _nonce(11); + uint256 deadline = block.timestamp + 3600; + _depositViaPermit2(config, DEPOSIT_AMOUNT, nonce, deadline); + + bytes memory voucherSig = _signVoucher(channelId, CLAIM_AMOUNT); + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: voucherSig, + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthAddr); + settlement.claim(claims); + uint256 g = vm.snapshotGasLastCall("fork_claim_1_voucher"); + console2.log("fork_claim_1_voucher", g); + } + + function test_gas_fork_settle() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + uint256 nonce = _nonce(12); + uint256 deadline = block.timestamp + 3600; + _depositViaPermit2(config, DEPOSIT_AMOUNT, nonce, deadline); + + bytes memory voucherSig = _signVoucher(channelId, CLAIM_AMOUNT); + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: voucherSig, + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthAddr); + settlement.claim(claims); + + settlement.settle(receiverAddr, address(token)); + uint256 g = vm.snapshotGasLastCall("fork_settle"); + console2.log("fork_settle", g); + } + + function test_gas_fork_refund_with_signature() public onlyFork { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + uint256 nonce = _nonce(13); + uint256 deadline = block.timestamp + 3600; + _depositViaPermit2(config, DEPOSIT_AMOUNT, nonce, deadline); + + bytes memory voucherSig = _signVoucher(channelId, CLAIM_AMOUNT); + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: voucherSig, + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthAddr); + settlement.claim(claims); + + uint128 refundAmt = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + bytes memory refundSig = _signRefund(channelId, 0, refundAmt); + settlement.refundWithSignature(config, refundAmt, 0, refundSig); + uint256 g = vm.snapshotGasLastCall("fork_refund_with_signature"); + console2.log("fork_refund_with_signature", g); + } +} diff --git a/test/x402BatchSettlement.gas.t.sol b/test/x402BatchSettlement.gas.t.sol new file mode 100644 index 0000000..3cbcc43 --- /dev/null +++ b/test/x402BatchSettlement.gas.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title x402BatchSettlement gas benchmarks +/// @notice Foundry gas snapshots for deposit / claim / settle / refund paths. +/// +/// **Run locally (mock collectors + mock token):** +/// `forge test --match-contract X402BatchSettlementGasTest -vv` +/// +/// **Gas report table:** +/// `forge test --match-contract X402BatchSettlementGasTest --gas-report` +/// +/// **Interpretation:** +/// - **ERC-3009 deposit:** `MockERC3009Token` does not verify signatures; real USDC ERC-3009 may cost more. +/// - **Permit2 deposit:** `MockPermit2` omits canonical Permit2 crypto; see fork tests for real Permit2 gas. +/// - **First deposit** on a channel includes `ChannelCreated`; top-ups are cheaper (not measured here unless noted). +/// - **Refund** measured via `refund` (EOA receiverAuthorizer). `refundWithSignature` adds payer-side digest verification. +/// +/// **Production-adjacent Permit2:** `test/x402BatchSettlement.fork.t.sol` defines `test_gas_fork_*`; run with `--fork-url $BASE_RPC_URL` (or mainnet) so Permit2 bytecode exists. +/// +/// **Break-even sketch** (one channel, same receiver/token): compare summed snapshots to `baseline_erc20_transfer`. +/// Rough parity when N naive ERC-20 transfers (gas each ~= baseline) exceeds deposit + claim + settle (+ refund if applicable). + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {x402BatchSettlement} from "../contracts/x402BatchSettlement.sol"; +import {ERC3009DepositCollector} from "../contracts/periphery/ERC3009DepositCollector.sol"; +import {Permit2DepositCollector} from "../contracts/periphery/Permit2DepositCollector.sol"; +import {GasMockDepositCollector} from "./mocks/GasMockDepositCollector.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {MockERC3009Token} from "./mocks/MockERC3009Token.sol"; +import {MockPermit2} from "./mocks/MockPermit2.sol"; + +contract X402BatchSettlementGasTest is Test { + x402BatchSettlement public settlement; + GasMockDepositCollector public mockCollector; + + MockERC20 public erc20Token; + MockERC3009Token public erc3009Token; + ERC3009DepositCollector public erc3009Collector; + + MockPermit2 public mockPermit2; + Permit2DepositCollector public permit2Collector; + + VmSafe.Wallet public payerWallet; + VmSafe.Wallet public payerAuthWallet; + VmSafe.Wallet public receiverWallet; + VmSafe.Wallet public receiverAuthWallet; + + uint40 internal constant WITHDRAW_DELAY = 3600; + uint128 internal constant DEPOSIT_AMOUNT = 1000e6; + uint128 internal constant CLAIM_AMOUNT = 100e6; + + function setUp() public { + vm.warp(1_000_000); + + payerWallet = vm.createWallet("gas_payer"); + payerAuthWallet = vm.createWallet("gas_payerAuth"); + receiverWallet = vm.createWallet("gas_receiver"); + receiverAuthWallet = vm.createWallet("gas_receiverAuth"); + + settlement = new x402BatchSettlement(); + mockCollector = new GasMockDepositCollector(); + + erc20Token = new MockERC20("USDC", "USDC", 6); + erc20Token.mint(payerWallet.addr, 10_000_000e6); + + vm.prank(payerWallet.addr); + erc20Token.approve(address(mockCollector), type(uint256).max); + + erc3009Token = new MockERC3009Token("USDC3009", "USDC", 6); + erc3009Token.mint(payerWallet.addr, 10_000_000e6); + + erc3009Collector = new ERC3009DepositCollector(address(settlement)); + + mockPermit2 = new MockPermit2(); + mockPermit2.setShouldActuallyTransfer(true); + permit2Collector = new Permit2DepositCollector(address(settlement), address(mockPermit2)); + + vm.prank(payerWallet.addr); + erc20Token.approve(address(mockPermit2), type(uint256).max); + } + + function _makeConfig( + address token, + bytes32 salt + ) internal view returns (x402BatchSettlement.ChannelConfig memory) { + return x402BatchSettlement.ChannelConfig({ + payer: payerWallet.addr, + payerAuthorizer: payerAuthWallet.addr, + receiver: receiverWallet.addr, + receiverAuthorizer: receiverAuthWallet.addr, + token: token, + withdrawDelay: WITHDRAW_DELAY, + salt: salt + }); + } + + function _domainSeparator() internal view returns (bytes32) { + (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = + settlement.eip712Domain(); + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + verifyingContract + ) + ); + } + + function _signTypedData(VmSafe.Wallet memory wallet, bytes32 structHash) internal returns (bytes memory) { + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet, digest); + return abi.encodePacked(r, s, v); + } + + function _signVoucher( + VmSafe.Wallet memory wallet, + bytes32 channelId, + uint128 maxClaimableAmount + ) internal returns (bytes memory) { + bytes32 structHash = keccak256(abi.encode(settlement.VOUCHER_TYPEHASH(), channelId, maxClaimableAmount)); + return _signTypedData(wallet, structHash); + } + + function _depositErc20(x402BatchSettlement.ChannelConfig memory config, uint128 amount) internal { + settlement.deposit(config, amount, address(mockCollector), ""); + } + + /// @notice Baseline: simple ERC-20 transfer (break-even denominator). + function test_gas_baseline_erc20_transfer() public { + address recipient = receiverWallet.addr; + erc20Token.mint(address(this), DEPOSIT_AMOUNT); + erc20Token.transfer(recipient, DEPOSIT_AMOUNT); + uint256 g = vm.snapshotGasLastCall("baseline_erc20_transfer"); + console2.log("baseline_erc20_transfer", g); + } + + /// @notice First ERC-20 deposit via pull collector (MockDepositCollector), not ERC-3009 / Permit2. + function test_gas_deposit_erc20_pull_first() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc20Token), bytes32(0)); + vm.prank(payerWallet.addr); + _depositErc20(config, DEPOSIT_AMOUNT); + uint256 g = vm.snapshotGasLastCall("deposit_erc20_pull_first"); + console2.log("deposit_erc20_pull_first", g); + } + + /// @notice ERC-3009-style deposit: receiveWithAuthorization + forward to settlement (`MockERC3009Token`). + function test_gas_deposit_erc3009_first() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc3009Token), bytes32(0)); + bytes32 channelId = settlement.getChannelId(config); + uint256 salt = 7; + bytes32 expectedNonce = keccak256(abi.encode(channelId, salt)); + bytes memory dummySig = abi.encodePacked(expectedNonce, bytes32(0)); // unused by mock token + + bytes memory collectorData = abi.encode(uint256(0), type(uint256).max, salt, dummySig); + + vm.prank(payerWallet.addr); + settlement.deposit(config, DEPOSIT_AMOUNT, address(erc3009Collector), collectorData); + uint256 g = vm.snapshotGasLastCall("deposit_erc3009_first"); + console2.log("deposit_erc3009_first", g); + } + + /// @notice Permit2 witness transfer via `MockPermit2` + `Permit2DepositCollector`. + function test_gas_deposit_permit2_mock_first() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc20Token), bytes32(0)); + uint256 nonce = uint256(keccak256("gas_permit2_nonce")); + uint256 deadline = block.timestamp + 3600; + bytes memory permit2Sig = hex""; + bytes memory collectorData = abi.encode(nonce, deadline, permit2Sig, bytes("")); + + vm.prank(payerWallet.addr); + settlement.deposit(config, DEPOSIT_AMOUNT, address(permit2Collector), collectorData); + uint256 g = vm.snapshotGasLastCall("deposit_permit2_mock_first"); + console2.log("deposit_permit2_mock_first", g); + } + + /// @notice Single-voucher `claim` after funding via pull deposit. + function test_gas_claim_one_voucher() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc20Token), bytes32(0)); + bytes32 channelId = settlement.getChannelId(config); + + vm.prank(payerWallet.addr); + _depositErc20(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: _signVoucher(payerAuthWallet, channelId, CLAIM_AMOUNT), + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + uint256 g = vm.snapshotGasLastCall("claim_1_voucher"); + console2.log("claim_1_voucher", g); + } + + /// @notice Ten distinct channels in one `claim` batch (each channel pre-funded). + function test_gas_claim_ten_channels() public { + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](10); + + for (uint256 i = 0; i < 10; ++i) { + x402BatchSettlement.ChannelConfig memory cfg = _makeConfig(address(erc20Token), bytes32(uint256(i + 1))); + bytes32 cid = settlement.getChannelId(cfg); + + vm.prank(payerWallet.addr); + _depositErc20(cfg, DEPOSIT_AMOUNT); + + claims[i] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: cfg, maxClaimableAmount: CLAIM_AMOUNT}), + signature: _signVoucher(payerAuthWallet, cid, CLAIM_AMOUNT), + totalClaimed: CLAIM_AMOUNT + }); + } + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + uint256 g = vm.snapshotGasLastCall("claim_10_channels"); + console2.log("claim_10_channels", g); + } + + /// @notice Sweep claimed-but-not-settled balance to receiver (single token). + function test_gas_settle() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc20Token), bytes32(0)); + bytes32 channelId = settlement.getChannelId(config); + + vm.prank(payerWallet.addr); + _depositErc20(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: _signVoucher(payerAuthWallet, channelId, CLAIM_AMOUNT), + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + settlement.settle(receiverWallet.addr, address(erc20Token)); + uint256 g = vm.snapshotGasLastCall("settle_receiver_token"); + console2.log("settle_receiver_token", g); + } + + /// @notice Cooperative refund of available escrow (`receiverAuthorizer` caller). + function test_gas_refund_full() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc20Token), bytes32(0)); + + vm.prank(payerWallet.addr); + _depositErc20(config, DEPOSIT_AMOUNT); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, DEPOSIT_AMOUNT); + uint256 g = vm.snapshotGasLastCall("refund_full_receiverAuth"); + console2.log("refund_full_receiverAuth", g); + } + + /// @notice Relay-style refund with EIP-712 signature check on `receiverAuthorizer`. + function test_gas_refund_with_signature() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(address(erc20Token), bytes32(uint256(777))); + bytes32 channelId = settlement.getChannelId(config); + + vm.prank(payerWallet.addr); + _depositErc20(config, DEPOSIT_AMOUNT); + + uint256 nonce = settlement.refundNonce(channelId); + bytes32 structHash = keccak256(abi.encode(settlement.REFUND_TYPEHASH(), channelId, nonce, DEPOSIT_AMOUNT)); + bytes memory sig = _signTypedData(receiverAuthWallet, structHash); + + settlement.refundWithSignature(config, DEPOSIT_AMOUNT, nonce, sig); + uint256 g = vm.snapshotGasLastCall("refund_with_signature_relay"); + console2.log("refund_with_signature_relay", g); + } +} diff --git a/test/x402BatchSettlement.t.sol b/test/x402BatchSettlement.t.sol new file mode 100644 index 0000000..238caca --- /dev/null +++ b/test/x402BatchSettlement.t.sol @@ -0,0 +1,1535 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {x402BatchSettlement} from "../contracts/x402BatchSettlement.sol"; +import {MockDepositCollector, MockShortCollector} from "./mocks/MockBatchDepositCollectors.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; + +contract X402BatchSettlementTest is Test { + x402BatchSettlement public settlement; + MockDepositCollector public mockCollector; + MockShortCollector public shortCollector; + MockERC20 public token; + + VmSafe.Wallet public payerWallet; + VmSafe.Wallet public payerAuthWallet; + VmSafe.Wallet public receiverWallet; + VmSafe.Wallet public receiverAuthWallet; + VmSafe.Wallet public otherWallet; + + uint40 constant WITHDRAW_DELAY = 3600; // 1 hour + uint128 constant DEPOSIT_AMOUNT = 1000e6; + uint128 constant CLAIM_AMOUNT = 100e6; + + event ChannelCreated(bytes32 indexed channelId, x402BatchSettlement.ChannelConfig config); + event ChannelClosed(bytes32 indexed channelId, x402BatchSettlement.ChannelConfig config); + event Deposited(bytes32 indexed channelId, address indexed sender, uint128 amount, uint128 newBalance); + event Claimed(bytes32 indexed channelId, address indexed sender, uint128 claimAmount, uint128 newTotalClaimed); + event Settled(address indexed receiver, address indexed token, address indexed sender, uint128 amount); + event Refunded(bytes32 indexed channelId, address indexed sender, uint128 amount); + event WithdrawInitiated(bytes32 indexed channelId, uint128 amount, uint40 finalizeAfter); + event WithdrawFinalized(bytes32 indexed channelId, address indexed sender, uint128 amount); + + function setUp() public { + vm.warp(1_000_000); + + payerWallet = vm.createWallet("payer"); + payerAuthWallet = vm.createWallet("payerAuth"); + receiverWallet = vm.createWallet("receiver"); + receiverAuthWallet = vm.createWallet("receiverAuth"); + otherWallet = vm.createWallet("other"); + + settlement = new x402BatchSettlement(); + + mockCollector = new MockDepositCollector(); + shortCollector = new MockShortCollector(); + + token = new MockERC20("USDC", "USDC", 6); + token.mint(payerWallet.addr, 100_000e6); + + vm.prank(payerWallet.addr); + token.approve(address(mockCollector), type(uint256).max); + vm.prank(payerWallet.addr); + token.approve(address(shortCollector), type(uint256).max); + } + + // ========================================================================= + // Helpers + // ========================================================================= + + function _makeConfig() internal view returns (x402BatchSettlement.ChannelConfig memory) { + return x402BatchSettlement.ChannelConfig({ + payer: payerWallet.addr, + payerAuthorizer: payerAuthWallet.addr, + receiver: receiverWallet.addr, + receiverAuthorizer: receiverAuthWallet.addr, + token: address(token), + withdrawDelay: WITHDRAW_DELAY, + salt: bytes32(0) + }); + } + + function _makeStatefulConfig() internal view returns (x402BatchSettlement.ChannelConfig memory) { + return x402BatchSettlement.ChannelConfig({ + payer: payerWallet.addr, + payerAuthorizer: address(0), + receiver: receiverWallet.addr, + receiverAuthorizer: receiverAuthWallet.addr, + token: address(token), + withdrawDelay: WITHDRAW_DELAY, + salt: bytes32(uint256(42)) + }); + } + + function _channelId( + x402BatchSettlement.ChannelConfig memory config + ) internal view returns (bytes32) { + bytes32 structHash = keccak256(abi.encode(settlement.CHANNEL_CONFIG_TYPEHASH(), config)); + return keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + } + + function _domainSeparator() internal view returns (bytes32) { + (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = + settlement.eip712Domain(); + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + verifyingContract + ) + ); + } + + function _signTypedData(VmSafe.Wallet memory wallet, bytes32 structHash) internal returns (bytes memory) { + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet, digest); + return abi.encodePacked(r, s, v); + } + + function _signVoucher( + VmSafe.Wallet memory wallet, + bytes32 channelId, + uint128 maxClaimableAmount + ) internal returns (bytes memory) { + bytes32 structHash = keccak256(abi.encode(settlement.VOUCHER_TYPEHASH(), channelId, maxClaimableAmount)); + return _signTypedData(wallet, structHash); + } + + function _signRefund( + VmSafe.Wallet memory wallet, + bytes32 channelId, + uint256 nonce, + uint128 amount + ) internal returns (bytes memory) { + bytes32 structHash = keccak256(abi.encode(settlement.REFUND_TYPEHASH(), channelId, nonce, amount)); + return _signTypedData(wallet, structHash); + } + + function _claimEntriesRootHashMemory( + x402BatchSettlement.VoucherClaim[] memory claims + ) internal view returns (bytes32) { + uint256 n = claims.length; + if (n == 0) { + return keccak256(""); + } + bytes32[] memory entryHashes = new bytes32[](n); + for (uint256 i = 0; i < n; ++i) { + bytes32 cid = settlement.getChannelId(claims[i].voucher.channel); + entryHashes[i] = keccak256( + abi.encode( + settlement.CLAIM_ENTRY_TYPEHASH(), cid, claims[i].voucher.maxClaimableAmount, claims[i].totalClaimed + ) + ); + } + return keccak256(abi.encodePacked(entryHashes)); + } + + function _claimBatchStructHashMemory( + x402BatchSettlement.VoucherClaim[] memory claims + ) internal view returns (bytes32) { + return keccak256(abi.encode(settlement.CLAIM_BATCH_TYPEHASH(), _claimEntriesRootHashMemory(claims))); + } + + function _signClaimBatch( + VmSafe.Wallet memory wallet, + x402BatchSettlement.VoucherClaim[] memory claims + ) internal returns (bytes memory) { + bytes32 structHash = _claimBatchStructHashMemory(claims); + return _signTypedData(wallet, structHash); + } + + function _deposit(x402BatchSettlement.ChannelConfig memory config, uint128 amount) internal { + settlement.deposit(config, amount, address(mockCollector), ""); + } + + function _getChannel( + bytes32 id + ) internal view returns (x402BatchSettlement.ChannelState memory ch) { + (ch.balance, ch.totalClaimed) = settlement.channels(id); + } + + function _getPendingWithdrawal( + bytes32 id + ) internal view returns (x402BatchSettlement.WithdrawalState memory ws) { + (ws.amount, ws.initiatedAt) = settlement.pendingWithdrawals(id); + } + + function _getReceiver( + address receiver, + address tkn + ) internal view returns (x402BatchSettlement.ReceiverState memory rs) { + (rs.totalClaimed, rs.totalSettled) = settlement.receivers(receiver, tkn); + } + + function _makeVoucherClaim( + x402BatchSettlement.ChannelConfig memory config, + uint128 maxClaimableAmount, + uint128 claimAmount + ) internal returns (x402BatchSettlement.VoucherClaim memory) { + bytes32 channelId = _channelId(config); + bytes memory sig = _signVoucher(payerAuthWallet, channelId, maxClaimableAmount); + return x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: maxClaimableAmount}), + signature: sig, + totalClaimed: claimAmount + }); + } + + function _makeVoucherClaimWithSigner( + x402BatchSettlement.ChannelConfig memory config, + uint128 maxClaimableAmount, + uint128 claimAmount, + VmSafe.Wallet memory signer + ) internal returns (x402BatchSettlement.VoucherClaim memory) { + bytes32 channelId = _channelId(config); + bytes memory sig = _signVoucher(signer, channelId, maxClaimableAmount); + return x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: maxClaimableAmount}), + signature: sig, + totalClaimed: claimAmount + }); + } + + // ========================================================================= + // Constructor Tests + // ========================================================================= + + function test_constructor_deploysSuccessfully() public view { + assertTrue(address(settlement) != address(0)); + } + + // ========================================================================= + // Deposit Tests + // ========================================================================= + + function test_deposit_success() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + vm.expectEmit(true, false, false, true); + emit ChannelCreated(channelId, config); + vm.expectEmit(true, true, false, true); + emit Deposited(channelId, address(this), DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, DEPOSIT_AMOUNT); + assertEq(ch.totalClaimed, 0); + } + + function test_deposit_topUp() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + _deposit(config, DEPOSIT_AMOUNT); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, DEPOSIT_AMOUNT * 2); + } + + function test_deposit_revert_zeroAmount() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + vm.expectRevert(x402BatchSettlement.ZeroDeposit.selector); + settlement.deposit(config, 0, address(mockCollector), ""); + } + + function test_deposit_revert_zeroCollector() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + vm.expectRevert(x402BatchSettlement.InvalidCollector.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(0), ""); + } + + function test_deposit_revert_collectionFailed() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + vm.expectRevert(x402BatchSettlement.DepositCollectionFailed.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(shortCollector), ""); + } + + function test_deposit_revert_withdrawDelayTooLow() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + config.withdrawDelay = 1; + vm.expectRevert(x402BatchSettlement.WithdrawDelayOutOfRange.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(mockCollector), ""); + } + + function test_deposit_revert_withdrawDelayTooHigh() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + config.withdrawDelay = uint40(31 days); + vm.expectRevert(x402BatchSettlement.WithdrawDelayOutOfRange.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(mockCollector), ""); + } + + function test_deposit_revert_zeroReceiver() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + config.receiver = address(0); + vm.expectRevert(x402BatchSettlement.InvalidChannel.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(mockCollector), ""); + } + + function test_deposit_revert_zeroReceiverAuthorizer() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + config.receiverAuthorizer = address(0); + vm.expectRevert(x402BatchSettlement.InvalidChannel.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(mockCollector), ""); + } + + function test_deposit_revert_zeroToken() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + config.token = address(0); + vm.expectRevert(x402BatchSettlement.InvalidChannel.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(mockCollector), ""); + } + + function test_deposit_revert_zeroPayer() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + config.payer = address(0); + vm.expectRevert(x402BatchSettlement.InvalidChannel.selector); + settlement.deposit(config, DEPOSIT_AMOUNT, address(mockCollector), ""); + } + + function test_deposit_revert_overflow() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + token.mint(payerWallet.addr, type(uint128).max); + _deposit(config, type(uint128).max); + + vm.expectRevert(x402BatchSettlement.DepositOverflow.selector); + settlement.deposit(config, 1, address(mockCollector), ""); + } + + /// @dev Deposits increase `balance` only; cooperative refund (not deposit) clears a pending withdrawal. + function test_deposit_doesNotCancelPendingWithdrawal() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + uint40 initiatedBefore = ws.initiatedAt; + assertGt(initiatedBefore, 0); + + _deposit(config, DEPOSIT_AMOUNT); + + ws = _getPendingWithdrawal(channelId); + assertEq(ws.initiatedAt, initiatedBefore); + assertEq(ws.amount, DEPOSIT_AMOUNT); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, DEPOSIT_AMOUNT * 2); + } + + // ========================================================================= + // Claim Tests (direct call) + // ========================================================================= + + function test_claim_single() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + vm.expectEmit(true, true, false, true); + emit Claimed(channelId, receiverAuthWallet.addr, CLAIM_AMOUNT, CLAIM_AMOUNT); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.totalClaimed, CLAIM_AMOUNT); + + x402BatchSettlement.ReceiverState memory rs = _getReceiver(receiverWallet.addr, address(token)); + assertEq(rs.totalClaimed, CLAIM_AMOUNT); + } + + function test_claim_batch() public { + x402BatchSettlement.ChannelConfig memory config1 = _makeConfig(); + x402BatchSettlement.ChannelConfig memory config2 = _makeConfig(); + config2.salt = bytes32(uint256(1)); + + _deposit(config1, DEPOSIT_AMOUNT); + _deposit(config2, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](2); + claims[0] = _makeVoucherClaim(config1, CLAIM_AMOUNT, CLAIM_AMOUNT); + claims[1] = _makeVoucherClaim(config2, CLAIM_AMOUNT * 2, CLAIM_AMOUNT * 2); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + x402BatchSettlement.ReceiverState memory rs = _getReceiver(receiverWallet.addr, address(token)); + assertEq(rs.totalClaimed, CLAIM_AMOUNT * 3); + } + + function test_claim_cumulative() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, 200e6, 100e6); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + claims[0] = _makeVoucherClaim(config, 200e6, 200e6); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + bytes32 channelId = _channelId(config); + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.totalClaimed, 200e6); + } + + function test_claim_revert_notAuthorized() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + vm.prank(otherWallet.addr); + vm.expectRevert(x402BatchSettlement.NotAuthorizedToClaim.selector); + settlement.claim(claims); + } + + function test_claim_success_asReceiver() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + vm.expectEmit(true, true, false, true); + emit Claimed(channelId, receiverWallet.addr, CLAIM_AMOUNT, CLAIM_AMOUNT); + + vm.prank(receiverWallet.addr); + settlement.claim(claims); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.totalClaimed, CLAIM_AMOUNT); + } + + function test_claim_revert_exceedsCeiling() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT + 1); + + vm.prank(receiverAuthWallet.addr); + vm.expectRevert(x402BatchSettlement.ClaimExceedsCeiling.selector); + settlement.claim(claims); + } + + function test_claim_revert_exceedsBalance() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, CLAIM_AMOUNT / 2); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + vm.prank(receiverAuthWallet.addr); + vm.expectRevert(x402BatchSettlement.ClaimExceedsBalance.selector); + settlement.claim(claims); + } + + function test_claim_revert_wrongSigner() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaimWithSigner(config, CLAIM_AMOUNT, CLAIM_AMOUNT, otherWallet); + + vm.prank(receiverAuthWallet.addr); + vm.expectRevert(x402BatchSettlement.InvalidSignature.selector); + settlement.claim(claims); + } + + function test_claim_revert_malformedSignature() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: hex"0000", + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthWallet.addr); + vm.expectRevert(); + settlement.claim(claims); + } + + // ========================================================================= + // Claim Tests — Stateful (payerAuthorizer == address(0), EIP-1271 path) + // ========================================================================= + + function test_claim_statefulMode_payerSigns() public { + x402BatchSettlement.ChannelConfig memory config = _makeStatefulConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + bytes32 channelId = _channelId(config); + bytes memory sig = _signVoucher(payerWallet, channelId, CLAIM_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: sig, + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.totalClaimed, CLAIM_AMOUNT); + } + + function test_claim_statefulMode_revert_wrongSigner() public { + x402BatchSettlement.ChannelConfig memory config = _makeStatefulConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + bytes32 channelId = _channelId(config); + bytes memory sig = _signVoucher(otherWallet, channelId, CLAIM_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = x402BatchSettlement.VoucherClaim({ + voucher: x402BatchSettlement.Voucher({channel: config, maxClaimableAmount: CLAIM_AMOUNT}), + signature: sig, + totalClaimed: CLAIM_AMOUNT + }); + + vm.prank(receiverAuthWallet.addr); + vm.expectRevert(x402BatchSettlement.InvalidSignature.selector); + settlement.claim(claims); + } + + // ========================================================================= + // claimWithSignature Tests + // ========================================================================= + + function test_claimWithSignature_success() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + bytes memory authSig = _signClaimBatch(receiverAuthWallet, claims); + + vm.prank(otherWallet.addr); + settlement.claimWithSignature(claims, authSig); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.totalClaimed, CLAIM_AMOUNT); + } + + function test_claimWithSignature_revert_emptyClaims() public { + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](0); + bytes memory sig = hex"dead"; + + vm.expectRevert(x402BatchSettlement.EmptyBatch.selector); + settlement.claimWithSignature(claims, sig); + } + + function test_claim_revert_emptyBatch() public { + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](0); + + vm.prank(receiverAuthWallet.addr); + vm.expectRevert(x402BatchSettlement.EmptyBatch.selector); + settlement.claim(claims); + } + + function test_claimWithSignature_revert_wrongAuthorizerSignature() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + bytes memory badSig = _signClaimBatch(otherWallet, claims); + + vm.expectRevert(x402BatchSettlement.InvalidSignature.selector); + settlement.claimWithSignature(claims, badSig); + } + + function test_claimWithSignature_revert_mixedReceiverAuthorizers() public { + x402BatchSettlement.ChannelConfig memory config1 = _makeConfig(); + x402BatchSettlement.ChannelConfig memory config2 = _makeConfig(); + config2.receiverAuthorizer = otherWallet.addr; + config2.salt = bytes32(uint256(2)); + + _deposit(config1, DEPOSIT_AMOUNT); + _deposit(config2, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](2); + claims[0] = _makeVoucherClaim(config1, CLAIM_AMOUNT, CLAIM_AMOUNT); + claims[1] = _makeVoucherClaim(config2, CLAIM_AMOUNT, CLAIM_AMOUNT); + + bytes memory authSig = _signClaimBatch(receiverAuthWallet, claims); + + vm.expectRevert(x402BatchSettlement.NotReceiverAuthorizer.selector); + settlement.claimWithSignature(claims, authSig); + } + + // ========================================================================= + // Settle Tests + // ========================================================================= + + function test_settle_success() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint256 balBefore = token.balanceOf(receiverWallet.addr); + + vm.expectEmit(true, true, true, true); + emit Settled(receiverWallet.addr, address(token), address(this), CLAIM_AMOUNT); + settlement.settle(receiverWallet.addr, address(token)); + + assertEq(token.balanceOf(receiverWallet.addr), balBefore + CLAIM_AMOUNT); + + x402BatchSettlement.ReceiverState memory rs = _getReceiver(receiverWallet.addr, address(token)); + assertEq(rs.totalSettled, CLAIM_AMOUNT); + } + + function test_settle_sweepsAcrossChannels() public { + x402BatchSettlement.ChannelConfig memory config1 = _makeConfig(); + x402BatchSettlement.ChannelConfig memory config2 = _makeConfig(); + config2.salt = bytes32(uint256(1)); + + _deposit(config1, DEPOSIT_AMOUNT); + _deposit(config2, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory v1 = new x402BatchSettlement.VoucherClaim[](1); + v1[0] = _makeVoucherClaim(config1, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(v1); + + x402BatchSettlement.VoucherClaim[] memory v2 = new x402BatchSettlement.VoucherClaim[](1); + v2[0] = _makeVoucherClaim(config2, CLAIM_AMOUNT * 2, CLAIM_AMOUNT * 2); + vm.prank(receiverAuthWallet.addr); + settlement.claim(v2); + + uint256 balBefore = token.balanceOf(receiverWallet.addr); + settlement.settle(receiverWallet.addr, address(token)); + assertEq(token.balanceOf(receiverWallet.addr), balBefore + CLAIM_AMOUNT * 3); + } + + function test_settle_idempotent_nothingToSettle() public { + uint256 balBefore = token.balanceOf(receiverWallet.addr); + settlement.settle(receiverWallet.addr, address(token)); + assertEq(token.balanceOf(receiverWallet.addr), balBefore); + } + + // ========================================================================= + // Timed Withdrawal Tests + // ========================================================================= + + function test_initiateWithdraw_success() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + vm.expectEmit(true, false, false, true); + emit WithdrawInitiated(channelId, DEPOSIT_AMOUNT, uint40(block.timestamp) + WITHDRAW_DELAY); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.amount, DEPOSIT_AMOUNT); + assertEq(ws.initiatedAt, uint40(block.timestamp)); + } + + function test_initiateWithdraw_success_asPayerAuthorizer() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerAuthWallet.addr); + vm.expectEmit(true, false, false, true); + emit WithdrawInitiated(channelId, DEPOSIT_AMOUNT, uint40(block.timestamp) + WITHDRAW_DELAY); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.amount, DEPOSIT_AMOUNT); + } + + function test_initiateWithdraw_revert_notPayerOrPayerAuthorizer() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(otherWallet.addr); + vm.expectRevert(x402BatchSettlement.InvalidChannel.selector); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + } + + function test_initiateWithdraw_revert_zeroAmount() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.NothingToWithdraw.selector); + settlement.initiateWithdraw(config, 0); + } + + function test_initiateWithdraw_revert_alreadyPending() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawalAlreadyPending.selector); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + } + + function test_initiateWithdraw_revert_amountExceedsAvailable_noChannel() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawAmountExceedsAvailable.selector); + settlement.initiateWithdraw(config, 1); + } + + function test_initiateWithdraw_revert_amountExceedsAvailable_afterClaim() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint128 available = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawAmountExceedsAvailable.selector); + settlement.initiateWithdraw(config, available + 1); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, available); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.amount, available); + } + + function test_initiateWithdraw_attackBypass_blocked() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawAmountExceedsAvailable.selector); + settlement.initiateWithdraw(config, type(uint128).max); + + vm.warp(block.timestamp + WITHDRAW_DELAY + 1); + + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawalNotPending.selector); + settlement.finalizeWithdraw(config); + } + + function test_finalizeWithdraw_success() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + vm.warp(block.timestamp + WITHDRAW_DELAY + 1); + + uint256 balBefore = token.balanceOf(payerWallet.addr); + + vm.expectEmit(true, true, false, true); + emit WithdrawFinalized(channelId, payerWallet.addr, DEPOSIT_AMOUNT); + vm.expectEmit(true, false, false, true); + emit ChannelClosed(channelId, config); + + vm.prank(payerWallet.addr); + settlement.finalizeWithdraw(config); + + assertEq(token.balanceOf(payerWallet.addr), balBefore + DEPOSIT_AMOUNT); + } + + function test_finalizeWithdraw_revert_notPayerOrPayerAuthorizer() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + vm.warp(block.timestamp + WITHDRAW_DELAY + 1); + + vm.prank(otherWallet.addr); + vm.expectRevert(x402BatchSettlement.NotAuthorizedToFinalizeWithdraw.selector); + settlement.finalizeWithdraw(config); + } + + function test_finalizeWithdraw_revert_notPending() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawalNotPending.selector); + settlement.finalizeWithdraw(config); + } + + function test_finalizeWithdraw_revert_delayNotElapsed() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + vm.expectRevert(x402BatchSettlement.WithdrawDelayNotElapsed.selector); + settlement.finalizeWithdraw(config); + } + + function test_finalizeWithdraw_zeroAmount_afterFullClaim() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + vm.warp(block.timestamp + WITHDRAW_DELAY + 1); + uint256 balBefore = token.balanceOf(payerWallet.addr); + vm.prank(payerWallet.addr); + settlement.finalizeWithdraw(config); + + assertEq(token.balanceOf(payerWallet.addr), balBefore); + } + + function test_finalizeWithdraw_capsIfClaimedDuringDelay() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, 500e6, 500e6); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + vm.warp(block.timestamp + WITHDRAW_DELAY + 1); + vm.prank(payerWallet.addr); + settlement.finalizeWithdraw(config); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, 500e6); + } + + // ========================================================================= + // Cooperative Withdrawal Tests + // ========================================================================= + + function test_refund_directCall() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint256 balBefore = token.balanceOf(payerWallet.addr); + + vm.expectEmit(true, true, false, true); + emit Refunded(channelId, receiverAuthWallet.addr, DEPOSIT_AMOUNT - CLAIM_AMOUNT); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, DEPOSIT_AMOUNT - CLAIM_AMOUNT); + + assertEq(token.balanceOf(payerWallet.addr), balBefore + DEPOSIT_AMOUNT - CLAIM_AMOUNT); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, ch.totalClaimed); + } + + function test_refund_revert_notAuthorized() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(otherWallet.addr); + vm.expectRevert(x402BatchSettlement.NotAuthorizedToRefund.selector); + settlement.refund(config, 1); + } + + function test_refund_directCall_asReceiver() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint256 balBefore = token.balanceOf(payerWallet.addr); + uint128 refundAmt = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + + vm.expectEmit(true, true, false, true); + emit Refunded(channelId, receiverWallet.addr, refundAmt); + + vm.prank(receiverWallet.addr); + settlement.refund(config, refundAmt); + + assertEq(token.balanceOf(payerWallet.addr), balBefore + refundAmt); + } + + function test_refundWithSignature_success() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint128 refundAmt = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + bytes memory sig = _signRefund(receiverAuthWallet, channelId, 0, refundAmt); + uint256 balBefore = token.balanceOf(payerWallet.addr); + + vm.prank(otherWallet.addr); + settlement.refundWithSignature(config, refundAmt, 0, sig); + + assertEq(token.balanceOf(payerWallet.addr), balBefore + refundAmt); + } + + function test_refundWithSignature_clearsPendingWithdrawal_whenRefundCoversIt() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + bytes memory sig = _signRefund(receiverAuthWallet, channelId, 0, DEPOSIT_AMOUNT); + settlement.refundWithSignature(config, DEPOSIT_AMOUNT, 0, sig); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.initiatedAt, 0); + assertEq(ws.amount, 0); + } + + function test_refund_partialDoesNotClearPendingWithdrawal() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + uint40 initiatedBefore = _getPendingWithdrawal(channelId).initiatedAt; + assertGt(initiatedBefore, 0); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, 1); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.initiatedAt, initiatedBefore); + assertEq(ws.amount, DEPOSIT_AMOUNT - 1); + } + + function test_refund_griefAttempt_finalizeStillSucceedsAfterDelay() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, DEPOSIT_AMOUNT); + + uint256 payerBalBefore = token.balanceOf(payerWallet.addr); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, 1); + + vm.warp(block.timestamp + WITHDRAW_DELAY); + + vm.prank(payerWallet.addr); + settlement.finalizeWithdraw(config); + + assertEq(token.balanceOf(payerWallet.addr), payerBalBefore + DEPOSIT_AMOUNT); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.initiatedAt, 0); + assertEq(ws.amount, 0); + } + + function test_refund_largerThanPending_clearsWithdrawal() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + uint128 partialWithdraw = DEPOSIT_AMOUNT / 4; + vm.prank(payerWallet.addr); + settlement.initiateWithdraw(config, partialWithdraw); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.WithdrawalState memory ws = _getPendingWithdrawal(channelId); + assertEq(ws.initiatedAt, 0); + assertEq(ws.amount, 0); + } + + function test_refundWithSignature_noop_whenNothingAvailable_afterFullClaim() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + bytes memory sig = _signRefund(receiverAuthWallet, channelId, 0, 1); + uint256 nonceBefore = settlement.refundNonce(channelId); + uint256 payerBal = token.balanceOf(payerWallet.addr); + + settlement.refundWithSignature(config, 1, 0, sig); + + assertEq(settlement.refundNonce(channelId), nonceBefore + 1); + assertEq(token.balanceOf(payerWallet.addr), payerBal); + } + + function test_refundWithSignature_replay_blockedAfterNoopThenDeposit() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + bytes memory sig = _signRefund(receiverAuthWallet, channelId, 0, DEPOSIT_AMOUNT); + settlement.refundWithSignature(config, DEPOSIT_AMOUNT, 0, sig); + + _deposit(config, DEPOSIT_AMOUNT); + + vm.expectRevert(x402BatchSettlement.InvalidRefundNonce.selector); + settlement.refundWithSignature(config, DEPOSIT_AMOUNT, 0, sig); + } + + function test_refund_directCall_bumpsNonce_evenOnNoop() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint256 nonceBefore = settlement.refundNonce(channelId); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, 1); + + assertEq(settlement.refundNonce(channelId), nonceBefore + 1); + } + + function test_refund_capsToAvailable() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + uint128 available = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + uint256 balBefore = token.balanceOf(payerWallet.addr); + + vm.expectEmit(true, true, false, true); + emit Refunded(channelId, receiverAuthWallet.addr, available); + + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, type(uint128).max); + + assertEq(token.balanceOf(payerWallet.addr), balBefore + available); + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, ch.totalClaimed); + } + + function test_refundWithSignature_revert_wrongSignature() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + bytes memory badSig = _signRefund(otherWallet, channelId, 0, DEPOSIT_AMOUNT); + vm.expectRevert(x402BatchSettlement.InvalidSignature.selector); + settlement.refundWithSignature(config, DEPOSIT_AMOUNT, 0, badSig); + } + + // ========================================================================= + // View Function Tests + // ========================================================================= + + function test_getChannelId_deterministic() public view { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 id1 = settlement.getChannelId(config); + bytes32 id2 = settlement.getChannelId(config); + assertEq(id1, id2); + assertEq(id1, _channelId(config)); + } + + function test_differentSalt_differentChannelId() public view { + x402BatchSettlement.ChannelConfig memory config1 = _makeConfig(); + x402BatchSettlement.ChannelConfig memory config2 = _makeConfig(); + config2.salt = bytes32(uint256(1)); + + assertNotEq(settlement.getChannelId(config1), settlement.getChannelId(config2)); + } + + function test_getVoucherDigest_matches() public view { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + bytes32 expected = keccak256( + abi.encodePacked( + "\x19\x01", + _domainSeparator(), + keccak256(abi.encode(settlement.VOUCHER_TYPEHASH(), channelId, uint128(100))) + ) + ); + assertEq(settlement.getVoucherDigest(channelId, 100), expected); + } + + function test_getRefundDigest_matches() public view { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + uint256 nonce = 7; + uint128 amount = 999e6; + bytes32 expected = keccak256( + abi.encodePacked( + "\x19\x01", + _domainSeparator(), + keccak256(abi.encode(settlement.REFUND_TYPEHASH(), channelId, nonce, amount)) + ) + ); + assertEq(settlement.getRefundDigest(channelId, nonce, amount), expected); + } + + function test_getClaimBatchDigest_matches() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config, CLAIM_AMOUNT, CLAIM_AMOUNT); + + bytes32 digest = settlement.getClaimBatchDigest(claims); + assertTrue(digest != bytes32(0)); + } + + function test_getClaimBatchDigest_emptyClaims_matches() public view { + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](0); + bytes32 expected = keccak256( + abi.encodePacked( + "\x19\x01", _domainSeparator(), keccak256(abi.encode(settlement.CLAIM_BATCH_TYPEHASH(), keccak256(""))) + ) + ); + assertEq(settlement.getClaimBatchDigest(claims), expected); + } + + function test_refundWithSignature_revert_invalidNonce() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + uint128 refundAmt = 50e6; + bytes memory sig = _signRefund(receiverAuthWallet, channelId, 1, refundAmt); + + vm.expectRevert(x402BatchSettlement.InvalidRefundNonce.selector); + settlement.refundWithSignature(config, refundAmt, 1, sig); + } + + function test_refundWithSignature_revert_zeroRefund() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + bytes memory sig = _signRefund(receiverAuthWallet, channelId, 0, 0); + + vm.expectRevert(x402BatchSettlement.ZeroRefund.selector); + settlement.refundWithSignature(config, 0, 0, sig); + } + + // ========================================================================= + // Edge Case Tests + // ========================================================================= + + function test_redeposit_afterFullWithdraw() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + + _deposit(config, DEPOSIT_AMOUNT); + + vm.expectEmit(true, true, false, true); + emit Refunded(channelId, receiverAuthWallet.addr, DEPOSIT_AMOUNT); + vm.expectEmit(true, false, false, true); + emit ChannelClosed(channelId, config); + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, DEPOSIT_AMOUNT); + + vm.expectEmit(true, false, false, true); + emit ChannelCreated(channelId, config); + vm.expectEmit(true, true, false, true); + emit Deposited(channelId, address(this), DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + _deposit(config, DEPOSIT_AMOUNT); + + x402BatchSettlement.ChannelState memory ch = _getChannel(channelId); + assertEq(ch.balance, DEPOSIT_AMOUNT); + } + + function test_channelClosed_onFullRefund_noClaims() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + bytes32 channelId = _channelId(config); + _deposit(config, DEPOSIT_AMOUNT); + + vm.expectEmit(true, true, false, true); + emit Refunded(channelId, receiverAuthWallet.addr, DEPOSIT_AMOUNT); + vm.expectEmit(true, false, false, true); + emit ChannelClosed(channelId, config); + vm.prank(receiverAuthWallet.addr); + settlement.refund(config, DEPOSIT_AMOUNT); + } + + function test_crossChannel_isolation() public { + x402BatchSettlement.ChannelConfig memory config1 = _makeConfig(); + x402BatchSettlement.ChannelConfig memory config2 = _makeConfig(); + config2.salt = bytes32(uint256(99)); + + _deposit(config1, DEPOSIT_AMOUNT); + _deposit(config2, DEPOSIT_AMOUNT / 2); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(config1, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + bytes32 channelId2 = _channelId(config2); + x402BatchSettlement.ChannelState memory ch2 = _getChannel(channelId2); + assertEq(ch2.totalClaimed, 0); + } + + function test_payerAuthorizer_zeroAllowed_inConfig() public view { + x402BatchSettlement.ChannelConfig memory config = _makeStatefulConfig(); + assertEq(config.payerAuthorizer, address(0)); + bytes32 id = settlement.getChannelId(config); + assertTrue(id != bytes32(0)); + } + + // ========================================================================= + // Multicall Tests + // ========================================================================= + + function test_multicall_migration_refundAndDeposit() public { + x402BatchSettlement.ChannelConfig memory oldConfig = _makeConfig(); + bytes32 oldChannelId = _channelId(oldConfig); + _deposit(oldConfig, DEPOSIT_AMOUNT); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(oldConfig, CLAIM_AMOUNT, CLAIM_AMOUNT); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + x402BatchSettlement.ChannelConfig memory newConfig = _makeConfig(); + newConfig.salt = bytes32(uint256(77)); + bytes32 newChannelId = _channelId(newConfig); + + uint128 refundAmt = DEPOSIT_AMOUNT - CLAIM_AMOUNT; + bytes memory refundSig = _signRefund(receiverAuthWallet, oldChannelId, 0, refundAmt); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(settlement.refundWithSignature, (oldConfig, refundAmt, uint256(0), refundSig)); + calls[1] = + abi.encodeCall(settlement.deposit, (newConfig, DEPOSIT_AMOUNT - CLAIM_AMOUNT, address(mockCollector), "")); + + uint256 payerBalBefore = token.balanceOf(payerWallet.addr); + + settlement.multicall(calls); + + x402BatchSettlement.ChannelState memory oldCh = _getChannel(oldChannelId); + assertEq(oldCh.balance, oldCh.totalClaimed); + + x402BatchSettlement.ChannelState memory newCh = _getChannel(newChannelId); + assertEq(newCh.balance, DEPOSIT_AMOUNT - CLAIM_AMOUNT); + + assertEq(token.balanceOf(payerWallet.addr), payerBalBefore); + } + + function test_multicall_batchDeposits() public { + x402BatchSettlement.ChannelConfig memory config1 = _makeConfig(); + x402BatchSettlement.ChannelConfig memory config2 = _makeConfig(); + config2.salt = bytes32(uint256(1)); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(settlement.deposit, (config1, DEPOSIT_AMOUNT, address(mockCollector), "")); + calls[1] = abi.encodeCall(settlement.deposit, (config2, DEPOSIT_AMOUNT / 2, address(mockCollector), "")); + + settlement.multicall(calls); + + assertEq(_getChannel(_channelId(config1)).balance, DEPOSIT_AMOUNT); + assertEq(_getChannel(_channelId(config2)).balance, DEPOSIT_AMOUNT / 2); + } + + function test_multicall_singleCall() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeCall(settlement.deposit, (config, DEPOSIT_AMOUNT, address(mockCollector), "")); + + settlement.multicall(calls); + + assertEq(_getChannel(_channelId(config)).balance, DEPOSIT_AMOUNT); + } + + function test_multicall_revert_propagates() public { + x402BatchSettlement.ChannelConfig memory config = _makeConfig(); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(settlement.deposit, (config, DEPOSIT_AMOUNT, address(mockCollector), "")); + calls[1] = abi.encodeCall(settlement.deposit, (config, 0, address(mockCollector), "")); + + vm.expectRevert(); + settlement.multicall(calls); + + assertEq(_getChannel(_channelId(config)).balance, 0); + } + + // ========================================================================= + // Complex Scenario Tests + // ========================================================================= + + /// @dev Full double-migration lifecycle: + /// Round 1: deposit A → claim A → migrate(refund A + deposit B) → settle + /// Round 2: deposit A → claim A → migrate(refund A + deposit B) → settle + /// Asserts channel A is fully drained after each round, channel B accumulates + /// both migrations, and the receiver settles both claims. + function test_scenario_doubleMigrationLifecycle() public { + x402BatchSettlement.ChannelConfig memory configA = _makeConfig(); + configA.salt = bytes32(uint256(0xA)); + + x402BatchSettlement.ChannelConfig memory configB = _makeConfig(); + configB.salt = bytes32(uint256(0xB)); + + uint256 payerStart = token.balanceOf(payerWallet.addr); + + // ── Round 1: deposit A, claim A, migrate A→B, settle ───────────────── + _roundDepositClaimMigrate(configA, configB, 1000e6, 100e6, 100e6); + settlement.settle(receiverWallet.addr, address(token)); + + _assertRound1(configA, configB, payerStart); + + // ── Round 2: deposit A again, claim A, migrate A→B, settle ─────────── + _roundDepositClaimMigrate(configA, configB, 1000e6, 200e6, 200e6); + settlement.settle(receiverWallet.addr, address(token)); + + _assertRound2(configA, configB, payerStart); + } + + function _roundDepositClaimMigrate( + x402BatchSettlement.ChannelConfig memory configA, + x402BatchSettlement.ChannelConfig memory configB, + uint128 depositAmt, + uint128 maxClaimable, + uint128 claimAmt + ) internal { + bytes32 channelA = _channelId(configA); + + _deposit(configA, depositAmt); + + x402BatchSettlement.VoucherClaim[] memory claims = new x402BatchSettlement.VoucherClaim[](1); + claims[0] = _makeVoucherClaim(configA, maxClaimable, claimAmt); + vm.prank(receiverAuthWallet.addr); + settlement.claim(claims); + + x402BatchSettlement.ChannelState memory chA = _getChannel(channelA); + uint128 migrateAmt = chA.balance - chA.totalClaimed; + + uint256 refundNonce = settlement.refundNonce(channelA); + bytes memory refundSig = _signRefund(receiverAuthWallet, channelA, refundNonce, migrateAmt); + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(settlement.refundWithSignature, (configA, migrateAmt, refundNonce, refundSig)); + calls[1] = abi.encodeCall(settlement.deposit, (configB, migrateAmt, address(mockCollector), "")); + settlement.multicall(calls); + } + + function _assertRound1( + x402BatchSettlement.ChannelConfig memory configA, + x402BatchSettlement.ChannelConfig memory configB, + uint256 payerStart + ) internal view { + x402BatchSettlement.ChannelState memory chA = _getChannel(_channelId(configA)); + assertEq(chA.balance, chA.totalClaimed, "R1: A fully drained"); + assertEq(chA.totalClaimed, 100e6); + + x402BatchSettlement.ChannelState memory chB = _getChannel(_channelId(configB)); + assertEq(chB.balance, 900e6, "R1: B holds migrated funds"); + + assertEq(token.balanceOf(receiverWallet.addr), 100e6, "R1: receiver settled"); + assertEq(token.balanceOf(payerWallet.addr), payerStart - 1000e6, "R1: payer spent one deposit"); + } + + function _assertRound2( + x402BatchSettlement.ChannelConfig memory configA, + x402BatchSettlement.ChannelConfig memory configB, + uint256 payerStart + ) internal view { + x402BatchSettlement.ChannelState memory chA = _getChannel(_channelId(configA)); + assertEq(chA.balance, chA.totalClaimed, "R2: A fully drained again"); + assertEq(chA.totalClaimed, 200e6, "R2: A accumulated two claims"); + + x402BatchSettlement.ChannelState memory chB = _getChannel(_channelId(configB)); + assertEq(chB.balance, 1800e6, "R2: B holds both migrations"); + + assertEq(token.balanceOf(receiverWallet.addr), 200e6, "R2: receiver settled both claims"); + + x402BatchSettlement.ReceiverState memory rs = _getReceiver(receiverWallet.addr, address(token)); + assertEq(rs.totalSettled, 200e6, "R2: totalSettled matches"); + assertEq(rs.totalClaimed, rs.totalSettled, "R2: nothing unsettled"); + + assertEq(token.balanceOf(payerWallet.addr), payerStart - 2000e6, "R2: payer funded both deposit rounds"); + } + + /// @dev Multiple individual claims across 3 channels, then a batched + /// claimWithSignature across all 3, followed by a single settle. + function test_scenario_multiChannelClaimsThenBatchedClaimAndSettle() public { + x402BatchSettlement.ChannelConfig memory c1 = _makeConfig(); + c1.salt = bytes32(uint256(1)); + x402BatchSettlement.ChannelConfig memory c2 = _makeConfig(); + c2.salt = bytes32(uint256(2)); + x402BatchSettlement.ChannelConfig memory c3 = _makeConfig(); + c3.salt = bytes32(uint256(3)); + + _deposit(c1, 1000e6); + _deposit(c2, 2000e6); + _deposit(c3, 500e6); + + // ── Individual claims ──────────────────────────────────────────────── + + // C1: claim 50, then claim to total 150 + x402BatchSettlement.VoucherClaim[] memory v = new x402BatchSettlement.VoucherClaim[](1); + v[0] = _makeVoucherClaim(c1, 50e6, 50e6); + vm.prank(receiverAuthWallet.addr); + settlement.claim(v); + + v[0] = _makeVoucherClaim(c1, 150e6, 150e6); + vm.prank(receiverAuthWallet.addr); + settlement.claim(v); + + // C2: claim 200 + v[0] = _makeVoucherClaim(c2, 200e6, 200e6); + vm.prank(receiverAuthWallet.addr); + settlement.claim(v); + + // C3: claim 50 + v[0] = _makeVoucherClaim(c3, 50e6, 50e6); + vm.prank(receiverAuthWallet.addr); + settlement.claim(v); + + // Verify intermediate state + assertEq(_getChannel(_channelId(c1)).totalClaimed, 150e6); + assertEq(_getChannel(_channelId(c2)).totalClaimed, 200e6); + assertEq(_getChannel(_channelId(c3)).totalClaimed, 50e6); + + x402BatchSettlement.ReceiverState memory rsMid = _getReceiver(receiverWallet.addr, address(token)); + assertEq(rsMid.totalClaimed, 400e6, "Mid: total claimed across all channels"); + assertEq(rsMid.totalSettled, 0, "Mid: nothing settled yet"); + + // ── Batched claimWithSignature across all 3 channels ───────────────── + + x402BatchSettlement.VoucherClaim[] memory batchClaims = new x402BatchSettlement.VoucherClaim[](3); + batchClaims[0] = _makeVoucherClaim(c1, 250e6, 250e6); // C1: total 250 (+100 delta) + batchClaims[1] = _makeVoucherClaim(c2, 500e6, 500e6); // C2: total 500 (+300 delta) + batchClaims[2] = _makeVoucherClaim(c3, 200e6, 200e6); // C3: total 200 (+150 delta) + + bytes memory authSig = _signClaimBatch(receiverAuthWallet, batchClaims); + vm.prank(otherWallet.addr); + settlement.claimWithSignature(batchClaims, authSig); + + // Verify post-batch state + assertEq(_getChannel(_channelId(c1)).totalClaimed, 250e6); + assertEq(_getChannel(_channelId(c2)).totalClaimed, 500e6); + assertEq(_getChannel(_channelId(c3)).totalClaimed, 200e6); + + x402BatchSettlement.ReceiverState memory rsPost = _getReceiver(receiverWallet.addr, address(token)); + uint128 expectedTotal = 250e6 + 500e6 + 200e6; + assertEq(rsPost.totalClaimed, expectedTotal, "Post: accumulated across individual + batch"); + assertEq(rsPost.totalSettled, 0, "Post: still nothing settled"); + + // ── Single settle sweeps everything ────────────────────────────────── + + uint256 receiverBefore = token.balanceOf(receiverWallet.addr); + settlement.settle(receiverWallet.addr, address(token)); + assertEq( + token.balanceOf(receiverWallet.addr), + receiverBefore + expectedTotal, + "Settle: receiver received all claimed funds" + ); + + x402BatchSettlement.ReceiverState memory rsFinal = _getReceiver(receiverWallet.addr, address(token)); + assertEq(rsFinal.totalSettled, expectedTotal); + assertEq(rsFinal.totalClaimed, rsFinal.totalSettled, "Final: fully settled"); + + // Channel balances unchanged (claims don't reduce balance) + assertEq(_getChannel(_channelId(c1)).balance, 1000e6); + assertEq(_getChannel(_channelId(c2)).balance, 2000e6); + assertEq(_getChannel(_channelId(c3)).balance, 500e6); + } +} diff --git a/test/x402ExactPermit2Proxy.fork.t.sol b/test/x402ExactPermit2Proxy.fork.t.sol new file mode 100644 index 0000000..4e89b07 --- /dev/null +++ b/test/x402ExactPermit2Proxy.fork.t.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {x402ExactPermit2Proxy} from "../contracts/x402ExactPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../contracts/x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "../contracts/interfaces/ISignatureTransfer.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; + +/// @title X402ExactPermit2ProxyForkTest +/// @notice Fork tests against real Permit2 deployment for exact amount transfers +/// @dev Run with: forge test --match-contract X402ExactPermit2ProxyForkTest --fork-url $RPC_URL +contract X402ExactPermit2ProxyForkTest is Test { + address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + bytes32 constant PERMIT_TYPEHASH = keccak256( + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)" + ); + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + + x402ExactPermit2Proxy public proxy; + MockERC20 public token; + + uint256 public payerKey; + address public payer; + address public recipient; + + uint256 constant MINT_AMOUNT = 10_000e6; + uint256 constant TRANSFER_AMOUNT = 100e6; + + event Settled(); + + function setUp() public { + if (block.chainid == 31_337) return; + require(PERMIT2.code.length > 0, "Permit2 not deployed"); + + payerKey = uint256(keccak256("x402-test-payer")); + payer = vm.addr(payerKey); + recipient = makeAddr("recipient"); + + proxy = new x402ExactPermit2Proxy(PERMIT2); + token = new MockERC20("USDC", "USDC", 6); + token.mint(payer, MINT_AMOUNT); + + vm.prank(payer); + token.approve(PERMIT2, type(uint256).max); + } + + modifier onlyFork() { + if (block.chainid == 31_337) return; + _; + } + + function _domainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256("Permit2"), block.chainid, PERMIT2)); + } + + function _nonce( + uint256 salt + ) internal view returns (uint256) { + return uint256(keccak256(abi.encodePacked(block.timestamp, block.number, salt))); + } + + function _sign( + address tokenAddr, + uint256 amount, + uint256 nonce, + uint256 deadline, + x402ExactPermit2Proxy.Witness memory witness + ) internal view returns (bytes memory) { + bytes32 witnessHash = keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.validAfter)); + + bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, tokenAddr, amount)); + + bytes32 structHash = + keccak256(abi.encode(PERMIT_TYPEHASH, tokenHash, address(proxy), nonce, deadline, witnessHash)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerKey, digest); + return abi.encodePacked(r, s, v); + } + + function test_fork_settleWithRealPermit2() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(1); + uint256 deadline = t + 3600; + + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + uint256 balanceBefore = token.balanceOf(recipient); + + vm.expectEmit(false, false, false, false); + emit Settled(); + + proxy.settle(permit, payer, witness, sig); + + assertEq(token.balanceOf(recipient) - balanceBefore, TRANSFER_AMOUNT); + } + + function test_fork_rejectsInvalidSignature() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(2); + + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: t + 3600 + }); + + bytes memory badSig = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + + vm.expectRevert(); + proxy.settle(permit, payer, witness, badSig); + } + + function test_fork_rejectsWrongSigner() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(3); + uint256 deadline = t + 3600; + + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); + + uint256 wrongKey = 0xdeadbeef; + bytes32 witnessHash = keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.validAfter)); + bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, address(token), TRANSFER_AMOUNT)); + bytes32 structHash = + keccak256(abi.encode(PERMIT_TYPEHASH, tokenHash, address(proxy), nonce, deadline, witnessHash)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongKey, digest); + bytes memory wrongSig = abi.encodePacked(r, s, v); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + vm.expectRevert(); + proxy.settle(permit, payer, witness, wrongSig); + } + + function test_fork_rejectsReplayedNonce() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(4); + uint256 deadline = t + 3600; + + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + proxy.settle(permit, payer, witness, sig); + + vm.expectRevert(); + proxy.settle(permit, payer, witness, sig); + } + + function test_fork_rejectsExpiredDeadline() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(5); + uint256 deadline = t - 60; // expired (Permit2's deadline enforces the upper bound) + + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 120}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + vm.expectRevert(); + proxy.settle(permit, payer, witness, sig); + } + + function test_fork_preventsDestinationTampering() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(6); + uint256 deadline = t + 3600; + + address attacker = makeAddr("attacker"); + + x402ExactPermit2Proxy.Witness memory signedWitness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, signedWitness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + x402ExactPermit2Proxy.Witness memory tamperedWitness = + x402ExactPermit2Proxy.Witness({to: attacker, validAfter: signedWitness.validAfter}); + + vm.expectRevert(); + proxy.settle(permit, payer, tamperedWitness, sig); + } +} diff --git a/test/x402ExactPermit2Proxy.t.sol b/test/x402ExactPermit2Proxy.t.sol new file mode 100644 index 0000000..8ab9437 --- /dev/null +++ b/test/x402ExactPermit2Proxy.t.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {x402ExactPermit2Proxy} from "../contracts/x402ExactPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../contracts/x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "../contracts/interfaces/ISignatureTransfer.sol"; +import {MockPermit2} from "./mocks/MockPermit2.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {MockERC20Permit} from "./mocks/MockERC20Permit.sol"; +import {MaliciousReentrantExact} from "./mocks/MaliciousReentrantExact.sol"; + +contract X402ExactPermit2ProxyTest is Test { + x402ExactPermit2Proxy public proxy; + MockPermit2 public mockPermit2; + MockERC20 public token; + + address public payer; + address public recipient; + + uint256 constant MINT_AMOUNT = 10_000e6; + uint256 constant TRANSFER_AMOUNT = 100e6; + + event Settled(); + event SettledWithPermit(); + event EIP2612PermitFailedWithReason(address indexed token, address indexed owner, string reason); + event EIP2612PermitFailedWithPanic(address indexed token, address indexed owner, uint256 errorCode); + event EIP2612PermitFailedWithData(address indexed token, address indexed owner, bytes data); + + function setUp() public { + vm.warp(1_000_000); + + payer = makeAddr("payer"); + recipient = makeAddr("recipient"); + + mockPermit2 = new MockPermit2(); + proxy = new x402ExactPermit2Proxy(address(mockPermit2)); + token = new MockERC20("USDC", "USDC", 6); + + token.mint(payer, MINT_AMOUNT); + vm.prank(payer); + token.approve(address(mockPermit2), type(uint256).max); + mockPermit2.setShouldActuallyTransfer(true); + } + + function _permit( + uint256 amount, + uint256 nonce, + uint256 deadline + ) internal view returns (ISignatureTransfer.PermitTransferFrom memory) { + return ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: amount}), + nonce: nonce, + deadline: deadline + }); + } + + function _witness(address to, uint256 validAfter) internal pure returns (x402ExactPermit2Proxy.Witness memory) { + return x402ExactPermit2Proxy.Witness({to: to, validAfter: validAfter}); + } + + function _sig() internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + } + + // --- Constructor --- + + function test_constructor_revertsOnZeroPermit2() public { + vm.expectRevert(x402BasePermit2Proxy.InvalidPermit2Address.selector); + new x402ExactPermit2Proxy(address(0)); + } + + function test_constructor_setsPermit2() public view { + assertEq(address(proxy.PERMIT2()), address(mockPermit2)); + } + + // --- settle() validation --- + + function test_settle_revertsOnZeroOwner() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidOwner.selector); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), address(0), _witness(recipient, t - 60), _sig()); + } + + function test_settle_revertsOnZeroDestination() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidDestination.selector); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(address(0), t - 60), _sig()); + } + + function test_settle_revertsBeforeValidAfter() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.PaymentTooEarly.selector); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t + 60), _sig()); + } + + function test_settle_revertsOnZeroAmount() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); + proxy.settle(_permit(0, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + } + + // Note: validBefore was removed - upper time bound is enforced by Permit2's deadline + + // --- settle() success paths --- + + function test_settle_transfersExactPermittedAmount() public { + uint256 t = block.timestamp; + uint256 balanceBefore = token.balanceOf(recipient); + + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + + assertEq(token.balanceOf(recipient) - balanceBefore, TRANSFER_AMOUNT); + } + + function test_settle_emitsSettled() public { + uint256 t = block.timestamp; + + vm.expectEmit(false, false, false, false); + emit Settled(); + + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + } + + function test_settle_atExactValidAfter() public { + uint256 t = block.timestamp; + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t), _sig()); + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); + } + + // --- Security: Permissionless settlement --- + + function test_settle_anyoneCanSettle() public { + uint256 t = block.timestamp; + address anyone = makeAddr("anyone"); + + vm.prank(anyone); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); + } + + // --- Security: Reentrancy --- + + function test_settle_blocksReentrancy() public { + MaliciousReentrantExact maliciousPermit2 = new MaliciousReentrantExact(); + x402ExactPermit2Proxy vulnerableProxy = new x402ExactPermit2Proxy(address(maliciousPermit2)); + maliciousPermit2.setTarget(address(vulnerableProxy)); + + MockERC20 testToken = new MockERC20("Test", "TST", 6); + testToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + testToken.approve(address(maliciousPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(testToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402ExactPermit2Proxy.Witness memory witness = _witness(recipient, t - 60); + + maliciousPermit2.setAttemptReentry(true); + maliciousPermit2.setAttackParams(permit, payer, witness, _sig()); + + vm.expectRevert(); + vulnerableProxy.settle(permit, payer, witness, _sig()); + } + + // --- Security: Proxy never holds funds --- + + function test_settle_proxyNeverHoldsTokens() public { + uint256 t = block.timestamp; + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + assertEq(token.balanceOf(address(proxy)), 0); + } + + // --- settleWithPermit() --- + + function test_settleWithPermit_transfersTokens() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(false, false, false, false); + emit SettledWithPermit(); + + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + + assertEq(permitToken.balanceOf(recipient), TRANSFER_AMOUNT); + } + + function test_settleWithPermit_succeedsWhenPermitFails() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setPermitRevert(true, "Permit failed"); + + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + + assertEq(permitToken.balanceOf(recipient), TRANSFER_AMOUNT); + } + + function test_settleWithPermit_emitsPermitFailedWithReason() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setPermitRevert(true, "Permit failed"); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(true, true, false, true); + emit EIP2612PermitFailedWithReason(address(permitToken), payer, "Permit failed"); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + } + + function test_settleWithPermit_emitsPermitFailedWithPanic() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setRevertMode(MockERC20Permit.RevertMode.Panic); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(true, true, false, true); + emit EIP2612PermitFailedWithPanic(address(permitToken), payer, 0x12); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + } + + function test_settleWithPermit_emitsPermitFailedWithData() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setRevertMode(MockERC20Permit.RevertMode.CustomError); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(true, true, false, false); + emit EIP2612PermitFailedWithData(address(permitToken), payer, ""); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + } + + function test_settleWithPermit_doesNotEmitPermitFailedOnSuccess() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.recordLogs(); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + bytes32 reasonSig = keccak256("EIP2612PermitFailedWithReason(address,address,string)"); + bytes32 panicSig = keccak256("EIP2612PermitFailedWithPanic(address,address,uint256)"); + bytes32 dataSig = keccak256("EIP2612PermitFailedWithData(address,address,bytes)"); + for (uint256 i = 0; i < entries.length; i++) { + bytes32 topic = entries[i].topics[0]; + assertTrue( + topic != reasonSig && topic != panicSig && topic != dataSig, + "No permit failure event should be emitted on success" + ); + } + } + + function test_settleWithPermit_revertsOnZeroAmount() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: 0}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: 0, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + } + + function test_settleWithPermit_revertsWhenPermit2612ValueTooSmall() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT - 1, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402BasePermit2Proxy.Permit2612AmountMismatch.selector); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + } + + function test_settleWithPermit_revertsWhenPermit2612ValueTooLarge() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT + 1, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402BasePermit2Proxy.Permit2612AmountMismatch.selector); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); + } + + // --- Fuzz: Time window --- + + function testFuzz_settle_afterValidAfter(uint256 validAfter, uint256 currentTime) public { + validAfter = bound(validAfter, 0, type(uint64).max - 3601); + currentTime = bound(currentTime, validAfter, type(uint64).max - 3601); + + vm.warp(currentTime); + + proxy.settle(_permit(TRANSFER_AMOUNT, 0, currentTime + 3600), payer, _witness(recipient, validAfter), _sig()); + + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); + } + + function testFuzz_settle_revertsBeforeValidAfter(uint256 validAfter, uint256 currentTime) public { + validAfter = bound(validAfter, 1000, type(uint64).max - 1000); + currentTime = bound(currentTime, 0, validAfter - 1); + + vm.warp(currentTime); + + vm.expectRevert(); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, currentTime + 3600), payer, _witness(recipient, validAfter), _sig()); + } + + // --- Fuzz: Amount (exact always transfers full permitted amount) --- + + function testFuzz_settle_alwaysTransfersExactPermittedAmount( + uint256 permitted + ) public { + permitted = bound(permitted, 1, MINT_AMOUNT); + + uint256 t = block.timestamp; + + proxy.settle(_permit(permitted, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + + assertEq(token.balanceOf(recipient), permitted); + } +} diff --git a/test/x402UptoPermit2Proxy.fork.t.sol b/test/x402UptoPermit2Proxy.fork.t.sol new file mode 100644 index 0000000..6ca5d9a --- /dev/null +++ b/test/x402UptoPermit2Proxy.fork.t.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {x402UptoPermit2Proxy} from "../contracts/x402UptoPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../contracts/x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "../contracts/interfaces/ISignatureTransfer.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; + +/// @title X402UptoPermit2ProxyForkTest +/// @notice Fork tests against real Permit2 deployment for flexible amount transfers +/// @dev Run with: forge test --match-contract X402UptoPermit2ProxyForkTest --fork-url $RPC_URL +contract X402UptoPermit2ProxyForkTest is Test { + address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + bytes32 constant PERMIT_TYPEHASH = keccak256( + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)" + ); + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + + x402UptoPermit2Proxy public proxy; + MockERC20 public token; + + uint256 public payerKey; + address public payer; + address public recipient; + + uint256 constant MINT_AMOUNT = 10_000e6; + uint256 constant TRANSFER_AMOUNT = 100e6; + + event Settled(); + + function setUp() public { + if (block.chainid == 31_337) return; + require(PERMIT2.code.length > 0, "Permit2 not deployed"); + + // Use a key that produces an EOA (not a deployed contract) on the fork + payerKey = uint256(keccak256("x402-test-payer")); + payer = vm.addr(payerKey); + recipient = makeAddr("recipient"); + + proxy = new x402UptoPermit2Proxy(PERMIT2); + token = new MockERC20("USDC", "USDC", 6); + token.mint(payer, MINT_AMOUNT); + + vm.prank(payer); + token.approve(PERMIT2, type(uint256).max); + } + + modifier onlyFork() { + if (block.chainid == 31_337) return; + _; + } + + function _domainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256("Permit2"), block.chainid, PERMIT2)); + } + + function _nonce( + uint256 salt + ) internal view returns (uint256) { + return uint256(keccak256(abi.encodePacked(block.timestamp, block.number, salt))); + } + + function _sign( + address tokenAddr, + uint256 amount, + uint256 nonce, + uint256 deadline, + x402UptoPermit2Proxy.Witness memory witness + ) internal view returns (bytes memory) { + // Must match contract's witness hash computation order + bytes32 witnessHash = + keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.facilitator, witness.validAfter)); + + bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, tokenAddr, amount)); + + bytes32 structHash = + keccak256(abi.encode(PERMIT_TYPEHASH, tokenHash, address(proxy), nonce, deadline, witnessHash)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerKey, digest); + return abi.encodePacked(r, s, v); + } + + function test_fork_settleWithRealPermit2() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(1); + uint256 deadline = t + 3600; + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + uint256 balanceBefore = token.balanceOf(recipient); + + vm.expectEmit(false, false, false, false); + emit Settled(); + + proxy.settle(permit, TRANSFER_AMOUNT, payer, witness, sig); + + assertEq(token.balanceOf(recipient) - balanceBefore, TRANSFER_AMOUNT); + } + + function test_fork_rejectsInvalidSignature() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(2); + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: t + 3600 + }); + + bytes memory badSig = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + + vm.expectRevert(); + proxy.settle(permit, TRANSFER_AMOUNT, payer, witness, badSig); + } + + function test_fork_rejectsWrongSigner() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(3); + uint256 deadline = t + 3600; + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + + uint256 wrongKey = 0xdeadbeef; + bytes32 witnessHash = + keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.facilitator, witness.validAfter)); + bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, address(token), TRANSFER_AMOUNT)); + bytes32 structHash = + keccak256(abi.encode(PERMIT_TYPEHASH, tokenHash, address(proxy), nonce, deadline, witnessHash)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongKey, digest); + bytes memory wrongSig = abi.encodePacked(r, s, v); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + vm.expectRevert(); + proxy.settle(permit, TRANSFER_AMOUNT, payer, witness, wrongSig); + } + + function test_fork_rejectsReplayedNonce() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(4); + uint256 deadline = t + 3600; + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + proxy.settle(permit, TRANSFER_AMOUNT, payer, witness, sig); + + vm.expectRevert(); + proxy.settle(permit, TRANSFER_AMOUNT, payer, witness, sig); + } + + function test_fork_rejectsExpiredDeadline() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(5); + uint256 deadline = t - 60; // expired (Permit2's deadline enforces the upper bound) + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 120}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + vm.expectRevert(); + proxy.settle(permit, TRANSFER_AMOUNT, payer, witness, sig); + } + + function test_fork_preventsDestinationTampering() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(6); + uint256 deadline = t + 3600; + + address attacker = makeAddr("attacker"); + + x402UptoPermit2Proxy.Witness memory signedWitness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + + bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, signedWitness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), + nonce: nonce, + deadline: deadline + }); + + x402UptoPermit2Proxy.Witness memory tamperedWitness = x402UptoPermit2Proxy.Witness({ + to: attacker, + facilitator: signedWitness.facilitator, + validAfter: signedWitness.validAfter + }); + + vm.expectRevert(); + proxy.settle(permit, TRANSFER_AMOUNT, payer, tamperedWitness, sig); + } + + function test_fork_partialAmounts() public onlyFork { + uint256 t = block.timestamp; + uint256 nonce = _nonce(7); + uint256 deadline = t + 3600; + uint256 permitted = TRANSFER_AMOUNT; + uint256 requested = permitted / 2; + + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + + bytes memory sig = _sign(address(token), permitted, nonce, deadline, witness); + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: permitted}), + nonce: nonce, + deadline: deadline + }); + + uint256 balanceBefore = token.balanceOf(recipient); + proxy.settle(permit, requested, payer, witness, sig); + assertEq(token.balanceOf(recipient) - balanceBefore, requested); + } +} diff --git a/test/x402UptoPermit2Proxy.t.sol b/test/x402UptoPermit2Proxy.t.sol new file mode 100644 index 0000000..194f2ca --- /dev/null +++ b/test/x402UptoPermit2Proxy.t.sol @@ -0,0 +1,651 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {x402UptoPermit2Proxy} from "../contracts/x402UptoPermit2Proxy.sol"; +import {x402BasePermit2Proxy} from "../contracts/x402BasePermit2Proxy.sol"; +import {ISignatureTransfer} from "../contracts/interfaces/ISignatureTransfer.sol"; +import {MockPermit2} from "./mocks/MockPermit2.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {MockERC20Permit} from "./mocks/MockERC20Permit.sol"; +import {MaliciousReentrantUpto} from "./mocks/MaliciousReentrantUpto.sol"; + +contract X402UptoPermit2ProxyTest is Test { + x402UptoPermit2Proxy public proxy; + MockPermit2 public mockPermit2; + MockERC20 public token; + + address public payer; + address public recipient; + + uint256 constant MINT_AMOUNT = 10_000e6; + uint256 constant TRANSFER_AMOUNT = 100e6; + + event Settled(); + event SettledWithPermit(); + event EIP2612PermitFailedWithReason(address indexed token, address indexed owner, string reason); + event EIP2612PermitFailedWithPanic(address indexed token, address indexed owner, uint256 errorCode); + event EIP2612PermitFailedWithData(address indexed token, address indexed owner, bytes data); + + function setUp() public { + vm.warp(1_000_000); + + payer = makeAddr("payer"); + recipient = makeAddr("recipient"); + + mockPermit2 = new MockPermit2(); + proxy = new x402UptoPermit2Proxy(address(mockPermit2)); + token = new MockERC20("USDC", "USDC", 6); + + token.mint(payer, MINT_AMOUNT); + vm.prank(payer); + token.approve(address(mockPermit2), type(uint256).max); + mockPermit2.setShouldActuallyTransfer(true); + } + + function _permit( + uint256 amount, + uint256 nonce, + uint256 deadline + ) internal view returns (ISignatureTransfer.PermitTransferFrom memory) { + return ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: amount}), + nonce: nonce, + deadline: deadline + }); + } + + function _witness( + address to, + address facilitator, + uint256 validAfter + ) internal pure returns (x402UptoPermit2Proxy.Witness memory) { + return x402UptoPermit2Proxy.Witness({to: to, facilitator: facilitator, validAfter: validAfter}); + } + + function _sig() internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + } + + // --- Constructor --- + + function test_constructor_revertsOnZeroPermit2() public { + vm.expectRevert(x402BasePermit2Proxy.InvalidPermit2Address.selector); + new x402UptoPermit2Proxy(address(0)); + } + + function test_constructor_setsPermit2() public view { + assertEq(address(proxy.PERMIT2()), address(mockPermit2)); + } + + // --- settle() validation --- + + function test_settle_revertsOnZeroOwner() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidOwner.selector); + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + address(0), + _witness(recipient, address(this), t - 60), + _sig() + ); + } + + function test_settle_revertsOnZeroDestination() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidDestination.selector); + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + payer, + _witness(address(0), address(this), t - 60), + _sig() + ); + } + + function test_settle_revertsBeforeValidAfter() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.PaymentTooEarly.selector); + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), t + 60), + _sig() + ); + } + + // Note: validBefore was removed - upper time bound is enforced by Permit2's deadline + + function test_settle_revertsOnZeroAmount() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), 0, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settle_revertsOnUnauthorizedFacilitator() public { + uint256 t = block.timestamp; + address attacker = makeAddr("attacker"); + vm.prank(attacker); + vm.expectRevert(x402UptoPermit2Proxy.UnauthorizedFacilitator.selector); + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), t - 60), + _sig() + ); + } + + function test_settle_revertsOnZeroPermittedAmount() public { + uint256 t = block.timestamp; + vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); + proxy.settle(_permit(0, 0, t + 3600), 0, payer, _witness(recipient, address(this), t - 60), _sig()); + } + + function test_settle_revertsWhenAmountExceedsPermitted() public { + uint256 t = block.timestamp; + vm.expectRevert(x402UptoPermit2Proxy.AmountExceedsPermitted.selector); + proxy.settle(_permit(100e6, 0, t + 3600), 150e6, payer, _witness(recipient, address(this), t - 60), _sig()); + } + + // --- settle() success paths --- + + function test_settle_transfersTokens() public { + uint256 t = block.timestamp; + uint256 balanceBefore = token.balanceOf(recipient); + + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), t - 60), + _sig() + ); + + assertEq(token.balanceOf(recipient) - balanceBefore, TRANSFER_AMOUNT); + } + + function test_settle_emitsSettled() public { + uint256 t = block.timestamp; + + vm.expectEmit(false, false, false, false); + emit Settled(); + + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), t - 60), + _sig() + ); + } + + function test_settle_allowsPartialAmount() public { + uint256 t = block.timestamp; + uint256 permitted = 100e6; + uint256 requested = 50e6; + + proxy.settle( + _permit(permitted, 0, t + 3600), requested, payer, _witness(recipient, address(this), t - 60), _sig() + ); + + assertEq(token.balanceOf(recipient), requested); + } + + function test_settle_atExactValidAfter() public { + uint256 t = block.timestamp; + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t), _sig() + ); + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); + } + + // --- Security: Reentrancy --- + + function test_settle_blocksReentrancy() public { + MaliciousReentrantUpto maliciousPermit2 = new MaliciousReentrantUpto(); + x402UptoPermit2Proxy vulnerableProxy = new x402UptoPermit2Proxy(address(maliciousPermit2)); + maliciousPermit2.setTarget(address(vulnerableProxy)); + + MockERC20 testToken = new MockERC20("Test", "TST", 6); + testToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + testToken.approve(address(maliciousPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(testToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402UptoPermit2Proxy.Witness memory witness = _witness(recipient, address(this), t - 60); + + maliciousPermit2.setAttemptReentry(true); + maliciousPermit2.setAttackParams(permit, TRANSFER_AMOUNT, payer, witness, _sig()); + + vm.expectRevert(); + vulnerableProxy.settle(permit, TRANSFER_AMOUNT, payer, witness, _sig()); + } + + // --- Security: Proxy never holds funds --- + + function test_settle_proxyNeverHoldsTokens() public { + uint256 t = block.timestamp; + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, t + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), t - 60), + _sig() + ); + assertEq(token.balanceOf(address(proxy)), 0); + } + + // --- settleWithPermit() --- + + function test_settleWithPermit_revertsOnUnauthorizedFacilitator() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + address attacker = makeAddr("attacker"); + vm.prank(attacker); + vm.expectRevert(x402UptoPermit2Proxy.UnauthorizedFacilitator.selector); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settleWithPermit_transfersTokens() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(false, false, false, false); + emit SettledWithPermit(); + + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + + assertEq(permitToken.balanceOf(recipient), TRANSFER_AMOUNT); + } + + function test_settleWithPermit_succeedsWhenPermitFails() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setPermitRevert(true, "Permit failed"); + + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + + assertEq(permitToken.balanceOf(recipient), TRANSFER_AMOUNT); + } + + function test_settleWithPermit_emitsPermitFailedWithReason() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setPermitRevert(true, "Permit failed"); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(true, true, false, true); + emit EIP2612PermitFailedWithReason(address(permitToken), payer, "Permit failed"); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settleWithPermit_emitsPermitFailedWithPanic() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setRevertMode(MockERC20Permit.RevertMode.Panic); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(true, true, false, true); + emit EIP2612PermitFailedWithPanic(address(permitToken), payer, 0x12); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settleWithPermit_emitsPermitFailedWithData() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + permitToken.setRevertMode(MockERC20Permit.RevertMode.CustomError); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectEmit(true, true, false, false); + emit EIP2612PermitFailedWithData(address(permitToken), payer, ""); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settleWithPermit_doesNotEmitPermitFailedOnSuccess() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.recordLogs(); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + bytes32 reasonSig = keccak256("EIP2612PermitFailedWithReason(address,address,string)"); + bytes32 panicSig = keccak256("EIP2612PermitFailedWithPanic(address,address,uint256)"); + bytes32 dataSig = keccak256("EIP2612PermitFailedWithData(address,address,bytes)"); + for (uint256 i = 0; i < entries.length; i++) { + bytes32 topic = entries[i].topics[0]; + assertTrue( + topic != reasonSig && topic != panicSig && topic != dataSig, + "No permit failure event should be emitted on success" + ); + } + } + + function test_settleWithPermit_revertsOnZeroAmount() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); + proxy.settleWithPermit(permit2612, permit, 0, payer, _witness(recipient, address(this), t - 60), _sig()); + } + + function test_settleWithPermit_revertsWhenAmountExceedsPermitted() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: 100e6}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: 100e6, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402UptoPermit2Proxy.AmountExceedsPermitted.selector); + proxy.settleWithPermit(permit2612, permit, 150e6, payer, _witness(recipient, address(this), t - 60), _sig()); + } + + function test_settleWithPermit_revertsWhenPermit2612ValueTooSmall() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT - 1, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402BasePermit2Proxy.Permit2612AmountMismatch.selector); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settleWithPermit_revertsWhenPermit2612ValueTooLarge() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + + uint256 t = block.timestamp; + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: TRANSFER_AMOUNT}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: TRANSFER_AMOUNT + 1, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + vm.expectRevert(x402BasePermit2Proxy.Permit2612AmountMismatch.selector); + proxy.settleWithPermit( + permit2612, permit, TRANSFER_AMOUNT, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function test_settleWithPermit_partialAmountWithMatchingPermit() public { + MockERC20Permit permitToken = new MockERC20Permit("USDC", "USDC", 6); + permitToken.mint(payer, MINT_AMOUNT); + vm.prank(payer); + permitToken.approve(address(mockPermit2), type(uint256).max); + + uint256 t = block.timestamp; + uint256 permitted = TRANSFER_AMOUNT; // 100e6 + uint256 requested = TRANSFER_AMOUNT / 2; // 50e6 + + ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: address(permitToken), amount: permitted}), + nonce: 0, + deadline: t + 3600 + }); + + x402BasePermit2Proxy.EIP2612Permit memory permit2612 = x402BasePermit2Proxy.EIP2612Permit({ + value: permitted, + deadline: t + 3600, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + proxy.settleWithPermit(permit2612, permit, requested, payer, _witness(recipient, address(this), t - 60), _sig()); + + assertEq(permitToken.balanceOf(recipient), requested); + } + + // --- Fuzz: Time window --- + + function testFuzz_settle_afterValidAfter(uint256 validAfter, uint256 currentTime) public { + validAfter = bound(validAfter, 0, type(uint64).max - 3601); + currentTime = bound(currentTime, validAfter, type(uint64).max - 3601); + + vm.warp(currentTime); + + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, currentTime + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), validAfter), + _sig() + ); + + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); + } + + function testFuzz_settle_revertsBeforeValidAfter(uint256 validAfter, uint256 currentTime) public { + validAfter = bound(validAfter, 1000, type(uint64).max - 1000); + currentTime = bound(currentTime, 0, validAfter - 1); + + vm.warp(currentTime); + + vm.expectRevert(); + proxy.settle( + _permit(TRANSFER_AMOUNT, 0, currentTime + 3600), + TRANSFER_AMOUNT, + payer, + _witness(recipient, address(this), validAfter), + _sig() + ); + } + + // --- Fuzz: Amount --- + + function testFuzz_settle_amountNeverExceedsPermitted(uint256 permitted, uint256 requested) public { + permitted = bound(permitted, 1, type(uint128).max); + requested = bound(requested, permitted + 1, type(uint256).max); + + uint256 t = block.timestamp; + + vm.expectRevert(x402UptoPermit2Proxy.AmountExceedsPermitted.selector); + proxy.settle( + _permit(permitted, 0, t + 3600), requested, payer, _witness(recipient, address(this), t - 60), _sig() + ); + } + + function testFuzz_settle_partialAmountsSucceed(uint256 permitted, uint256 requested) public { + permitted = bound(permitted, 1, MINT_AMOUNT); + requested = bound(requested, 1, permitted); + + uint256 t = block.timestamp; + + proxy.settle( + _permit(permitted, 0, t + 3600), requested, payer, _witness(recipient, address(this), t - 60), _sig() + ); + + assertEq(token.balanceOf(recipient), requested); + } +} From fda541eac4eaa2688c6a7baf082ae8d510ca0c96 Mon Sep 17 00:00:00 2001 From: app Date: Tue, 16 Jun 2026 10:44:28 +0800 Subject: [PATCH 02/12] delete 02/03 --- deploy/02_deploy_MerchantAgent.ts | 16 ---------------- deploy/03_deploy_Merchant.ts | 16 ---------------- 2 files changed, 32 deletions(-) delete mode 100644 deploy/02_deploy_MerchantAgent.ts delete mode 100644 deploy/03_deploy_Merchant.ts diff --git a/deploy/02_deploy_MerchantAgent.ts b/deploy/02_deploy_MerchantAgent.ts deleted file mode 100644 index ea59a73..0000000 --- a/deploy/02_deploy_MerchantAgent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { deployments, getNamedAccounts } = hre; - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - await deploy('MerchantAgent', { - from: deployer, - args: [], - log: true, - }); -}; -export default func; -func.tags = ['MerchantAgent']; diff --git a/deploy/03_deploy_Merchant.ts b/deploy/03_deploy_Merchant.ts deleted file mode 100644 index d365010..0000000 --- a/deploy/03_deploy_Merchant.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { deployments, getNamedAccounts } = hre; - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - await deploy('Merchant', { - from: deployer, - args: ['0x1DB6990CFAD265EFE4A0BB986488C04CC49FEE53','0xA3A3F5684FA066D9E5520FD5E592D87C322A58C2'], // '0x0997AEB2FB2E15E532B972C145E140B278510143', '0x55DC789DC6D58C596214F10D4A7717E9EC0A8CBB' - log: true, - }); -}; -export default func; -func.tags = ['Merchant']; From 9b68896c0a65be1141ad719a67fddb67e6ef075c Mon Sep 17 00:00:00 2001 From: app Date: Tue, 16 Jun 2026 11:41:15 +0800 Subject: [PATCH 03/12] add deployed address --- README.md | 10 ++++----- deploy/02_deploy_x402ExactPermit2Proxy.ts | 21 ++++++++++++++++++ deploy/03_deploy_x402BatchSettlement.ts | 16 ++++++++++++++ deploy/04_deploy_x402UptoPermit2Proxy.ts | 21 ++++++++++++++++++ deployTron/02_deploy_x402ExactPermit2Proxy.ts | 22 +++++++++++++++++++ deployTron/03_deploy_x402BatchSettlement.ts | 16 ++++++++++++++ deployTron/04_deploy_x402UptoPermit2Proxy.ts | 22 +++++++++++++++++++ hardhat.config.ts | 4 ++-- 8 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 deploy/02_deploy_x402ExactPermit2Proxy.ts create mode 100644 deploy/03_deploy_x402BatchSettlement.ts create mode 100644 deploy/04_deploy_x402UptoPermit2Proxy.ts create mode 100644 deployTron/02_deploy_x402ExactPermit2Proxy.ts create mode 100644 deployTron/03_deploy_x402BatchSettlement.ts create mode 100644 deployTron/04_deploy_x402UptoPermit2Proxy.ts diff --git a/README.md b/README.md index 0814e6b..05d777b 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → ## Deployed Addresses -| Network | Chain / Environment | PaymentPermit Address | -|-----------|---------------------|------------------------| -| **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | -| **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | -| **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | +| Network | Chain / Environment | PaymentPermit Address | x402ExactPermit2Proxy | x402BatchSettlement | x402UptoPermit2Proxy | +|-----------|---------------------|------------------------|------------------------|------------------------|------------------------| +| **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | — | — | — | +| **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TZ4xFKCDxiSMQsqrWWKBdXbD4a17nQaeT1`](https://nile.tronscan.org/#/contract/TZ4xFKCDxiSMQsqrWWKBdXbD4a17nQaeT1) | [`TKLsFK9B3iqtgQSqqfFkv4UEM81YKdKY3m`](https://nile.tronscan.org/#/contract/TKLsFK9B3iqtgQSqqfFkv4UEM81YKdKY3m) | [`TMaz5vj4zGWwjzQE6wqboRsypuxY23tcBX`](https://nile.tronscan.org/#/contract/TMaz5vj4zGWwjzQE6wqboRsypuxY23tcBX) | +| **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | | Network | Chain / Environment | PaymentPermit Address | |-----------|---------------------|------------------------| diff --git a/deploy/02_deploy_x402ExactPermit2Proxy.ts b/deploy/02_deploy_x402ExactPermit2Proxy.ts new file mode 100644 index 0000000..3de3c8c --- /dev/null +++ b/deploy/02_deploy_x402ExactPermit2Proxy.ts @@ -0,0 +1,21 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). +const CANONICAL_PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + + await deploy('x402ExactPermit2Proxy', { + from: deployer, + args: [permit2], + log: true, + }); +}; +export default func; +func.tags = ['x402ExactPermit2Proxy']; diff --git a/deploy/03_deploy_x402BatchSettlement.ts b/deploy/03_deploy_x402BatchSettlement.ts new file mode 100644 index 0000000..687912a --- /dev/null +++ b/deploy/03_deploy_x402BatchSettlement.ts @@ -0,0 +1,16 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + await deploy('x402BatchSettlement', { + from: deployer, + args: [], + log: true, + }); +}; +export default func; +func.tags = ['x402BatchSettlement']; diff --git a/deploy/04_deploy_x402UptoPermit2Proxy.ts b/deploy/04_deploy_x402UptoPermit2Proxy.ts new file mode 100644 index 0000000..af228e2 --- /dev/null +++ b/deploy/04_deploy_x402UptoPermit2Proxy.ts @@ -0,0 +1,21 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). +const CANONICAL_PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + + await deploy('x402UptoPermit2Proxy', { + from: deployer, + args: [permit2], + log: true, + }); +}; +export default func; +func.tags = ['x402UptoPermit2Proxy']; diff --git a/deployTron/02_deploy_x402ExactPermit2Proxy.ts b/deployTron/02_deploy_x402ExactPermit2Proxy.ts new file mode 100644 index 0000000..7a8b7ea --- /dev/null +++ b/deployTron/02_deploy_x402ExactPermit2Proxy.ts @@ -0,0 +1,22 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). +// Override with PERMIT2_ADDRESS if the target Tron network uses a different one. +const CANONICAL_PERMIT2 = '0xBE365314f2E77FD1257d60C346Bb32DbDa369403'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + + await deploy('x402ExactPermit2Proxy', { + from: deployer, + args: [permit2], + log: true, + }); +}; +export default func; +func.tags = ['x402ExactPermit2Proxy']; diff --git a/deployTron/03_deploy_x402BatchSettlement.ts b/deployTron/03_deploy_x402BatchSettlement.ts new file mode 100644 index 0000000..687912a --- /dev/null +++ b/deployTron/03_deploy_x402BatchSettlement.ts @@ -0,0 +1,16 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + await deploy('x402BatchSettlement', { + from: deployer, + args: [], + log: true, + }); +}; +export default func; +func.tags = ['x402BatchSettlement']; diff --git a/deployTron/04_deploy_x402UptoPermit2Proxy.ts b/deployTron/04_deploy_x402UptoPermit2Proxy.ts new file mode 100644 index 0000000..3f727ce --- /dev/null +++ b/deployTron/04_deploy_x402UptoPermit2Proxy.ts @@ -0,0 +1,22 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). +// Override with PERMIT2_ADDRESS if the target Tron network uses a different one. +const CANONICAL_PERMIT2 = '0xBE365314f2E77FD1257d60C346Bb32DbDa369403'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + + const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + + await deploy('x402UptoPermit2Proxy', { + from: deployer, + args: [permit2], + log: true, + }); +}; +export default func; +func.tags = ['x402UptoPermit2Proxy']; diff --git a/hardhat.config.ts b/hardhat.config.ts index 7e5e66f..18de982 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -37,13 +37,13 @@ const config: HardhatUserConfig = { nile: { url: process.env.TRON_RPC_URL || 'https://nile.trongrid.io/jsonrpc', // nile rpc url tron: true, // enable nile network - deploy: ['deploy/'], // folder for nile deploy scripts + deploy: ['deployTron/'], // folder for nile deploy scripts accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy }, shasta: { url: process.env.TRON_RPC_URL || 'https://api.shasta.trongrid.io/jsonrpc', // shasta rpc url tron: true, // enable shasta network - deploy: ['deploy/'], // folder for shasta deploy scripts + deploy: ['deployTron/'], // folder for shasta deploy scripts accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy }, }, From e678842e41fe049dd2a844f385573d2c82244a01 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Tue, 16 Jun 2026 18:35:34 +0800 Subject: [PATCH 04/12] Set Permit2 to TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h and update Nile deploy addresses Point x402ExactPermit2Proxy and x402UptoPermit2Proxy deploy scripts at the Tron Permit2 address TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h (0xf62f506D1FaA02e2354a9886B4A200496EE96F4b), and update the README with the freshly deployed Nile addresses for the three contracts. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- deployTron/02_deploy_x402ExactPermit2Proxy.ts | 5 +++-- deployTron/04_deploy_x402UptoPermit2Proxy.ts | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 05d777b..eee9198 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | Network | Chain / Environment | PaymentPermit Address | x402ExactPermit2Proxy | x402BatchSettlement | x402UptoPermit2Proxy | |-----------|---------------------|------------------------|------------------------|------------------------|------------------------| | **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | — | — | — | -| **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TZ4xFKCDxiSMQsqrWWKBdXbD4a17nQaeT1`](https://nile.tronscan.org/#/contract/TZ4xFKCDxiSMQsqrWWKBdXbD4a17nQaeT1) | [`TKLsFK9B3iqtgQSqqfFkv4UEM81YKdKY3m`](https://nile.tronscan.org/#/contract/TKLsFK9B3iqtgQSqqfFkv4UEM81YKdKY3m) | [`TMaz5vj4zGWwjzQE6wqboRsypuxY23tcBX`](https://nile.tronscan.org/#/contract/TMaz5vj4zGWwjzQE6wqboRsypuxY23tcBX) | +| **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TQGL1oGBPqX7WnYSmjyJQwHHqf5zQikPgR`](https://nile.tronscan.org/#/contract/TQGL1oGBPqX7WnYSmjyJQwHHqf5zQikPgR) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | | Network | Chain / Environment | PaymentPermit Address | diff --git a/deployTron/02_deploy_x402ExactPermit2Proxy.ts b/deployTron/02_deploy_x402ExactPermit2Proxy.ts index 7a8b7ea..9ee5ef9 100644 --- a/deployTron/02_deploy_x402ExactPermit2Proxy.ts +++ b/deployTron/02_deploy_x402ExactPermit2Proxy.ts @@ -1,9 +1,10 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; -// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). +// Permit2 address on the target Tron network. +// base58: TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h // Override with PERMIT2_ADDRESS if the target Tron network uses a different one. -const CANONICAL_PERMIT2 = '0xBE365314f2E77FD1257d60C346Bb32DbDa369403'; +const CANONICAL_PERMIT2 = '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; diff --git a/deployTron/04_deploy_x402UptoPermit2Proxy.ts b/deployTron/04_deploy_x402UptoPermit2Proxy.ts index 3f727ce..3916279 100644 --- a/deployTron/04_deploy_x402UptoPermit2Proxy.ts +++ b/deployTron/04_deploy_x402UptoPermit2Proxy.ts @@ -1,9 +1,10 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; -// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). +// Permit2 address on the target Tron network. +// base58: TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h // Override with PERMIT2_ADDRESS if the target Tron network uses a different one. -const CANONICAL_PERMIT2 = '0xBE365314f2E77FD1257d60C346Bb32DbDa369403'; +const CANONICAL_PERMIT2 = '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; From 4743211dc339b59240beea47744f9387b44ee11a Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Wed, 17 Jun 2026 14:42:06 +0800 Subject: [PATCH 05/12] add collector addr --- README.md | 6 ++++ .../05_deploy_ERC3009DepositCollector.ts | 29 +++++++++++++++ .../06_deploy_Permit2DepositCollector.ts | 35 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 deployTron/05_deploy_ERC3009DepositCollector.ts create mode 100644 deployTron/06_deploy_Permit2DepositCollector.ts diff --git a/README.md b/README.md index eee9198..0040bd6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TQGL1oGBPqX7WnYSmjyJQwHHqf5zQikPgR`](https://nile.tronscan.org/#/contract/TQGL1oGBPqX7WnYSmjyJQwHHqf5zQikPgR) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | +### Deposit Collectors + +| Network | Chain / Environment | ERC3009DepositCollector | Permit2DepositCollector | +|-----------|---------------------|--------------------------|--------------------------| +| **TRON Nile** | Testnet | [`TFU7hoDbxG8DE1LnAyY8QunoUpAbGVzP4Z`](https://nile.tronscan.org/#/contract/TFU7hoDbxG8DE1LnAyY8QunoUpAbGVzP4Z) | [`TUdqkmExXXUNjeeXUuQZGwLaEBuCujcvWb`](https://nile.tronscan.org/#/contract/TUdqkmExXXUNjeeXUuQZGwLaEBuCujcvWb) | + | Network | Chain / Environment | PaymentPermit Address | |-----------|---------------------|------------------------| | **BSC Mainnet** | Mainnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | diff --git a/deployTron/05_deploy_ERC3009DepositCollector.ts b/deployTron/05_deploy_ERC3009DepositCollector.ts new file mode 100644 index 0000000..6d94343 --- /dev/null +++ b/deployTron/05_deploy_ERC3009DepositCollector.ts @@ -0,0 +1,29 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const TronWeb = require('tronweb').TronWeb || require('tronweb'); + +// Convert a Tron base58 address to its 20-byte EVM hex form (drops the 0x41 prefix). +const toEvmAddress = (addr: string): string => + addr.startsWith('0x') ? addr : '0x' + TronWeb.address.toHex(addr).slice(2); + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy, get } = deployments; + const { deployer } = await getNamedAccounts(); + + // ERC3009DepositCollector forwards pulled tokens to x402BatchSettlement. + // Override with X402_BATCH_SETTLEMENT to target an existing deployment. + const batchSettlement = toEvmAddress( + process.env.X402_BATCH_SETTLEMENT || (await get('x402BatchSettlement')).address + ); + + await deploy('ERC3009DepositCollector', { + from: deployer, + args: [batchSettlement], + log: true, + }); +}; +export default func; +func.tags = ['ERC3009DepositCollector']; +func.dependencies = ['x402BatchSettlement']; diff --git a/deployTron/06_deploy_Permit2DepositCollector.ts b/deployTron/06_deploy_Permit2DepositCollector.ts new file mode 100644 index 0000000..68931fb --- /dev/null +++ b/deployTron/06_deploy_Permit2DepositCollector.ts @@ -0,0 +1,35 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const TronWeb = require('tronweb').TronWeb || require('tronweb'); + +// Permit2 address on the target Tron network. +// base58: TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h +// Override with PERMIT2_ADDRESS if the target Tron network uses a different one. +const CANONICAL_PERMIT2 = '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b'; + +// Convert a Tron base58 address to its 20-byte EVM hex form (drops the 0x41 prefix). +const toEvmAddress = (addr: string): string => + addr.startsWith('0x') ? addr : '0x' + TronWeb.address.toHex(addr).slice(2); + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy, get } = deployments; + const { deployer } = await getNamedAccounts(); + + // Permit2DepositCollector forwards Permit2 transfers to x402BatchSettlement. + // Override with X402_BATCH_SETTLEMENT to target an existing deployment. + const batchSettlement = toEvmAddress( + process.env.X402_BATCH_SETTLEMENT || (await get('x402BatchSettlement')).address + ); + const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + + await deploy('Permit2DepositCollector', { + from: deployer, + args: [batchSettlement, permit2], + log: true, + }); +}; +export default func; +func.tags = ['Permit2DepositCollector']; +func.dependencies = ['x402BatchSettlement']; From 51a9598f3503fc357a1b340bcaf812d3e490d557 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Wed, 17 Jun 2026 16:39:05 +0800 Subject: [PATCH 06/12] Use SafeTransferLib for token transfers; update Nile addresses Replace OZ SafeERC20.safeTransfer with sun-contract-std SafeTransferLib in x402BatchSettlement and ERC3009DepositCollector for TRON-safe token handling (USDT non-standard return). Wrap calls in a TransferFailed revert since the lib returns a bool instead of reverting. Update README Nile addresses for the redeployed x402BatchSettlement, ERC3009DepositCollector, and Permit2DepositCollector. Co-Authored-By: Claude Opus 4.8 --- README.md | 4 ++-- contracts/periphery/ERC3009DepositCollector.sol | 8 ++++---- contracts/x402BatchSettlement.sol | 11 ++++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0040bd6..41e157a 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,14 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | Network | Chain / Environment | PaymentPermit Address | x402ExactPermit2Proxy | x402BatchSettlement | x402UptoPermit2Proxy | |-----------|---------------------|------------------------|------------------------|------------------------|------------------------| | **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | — | — | — | -| **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TQGL1oGBPqX7WnYSmjyJQwHHqf5zQikPgR`](https://nile.tronscan.org/#/contract/TQGL1oGBPqX7WnYSmjyJQwHHqf5zQikPgR) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | +| **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA`](https://nile.tronscan.org/#/contract/TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | ### Deposit Collectors | Network | Chain / Environment | ERC3009DepositCollector | Permit2DepositCollector | |-----------|---------------------|--------------------------|--------------------------| -| **TRON Nile** | Testnet | [`TFU7hoDbxG8DE1LnAyY8QunoUpAbGVzP4Z`](https://nile.tronscan.org/#/contract/TFU7hoDbxG8DE1LnAyY8QunoUpAbGVzP4Z) | [`TUdqkmExXXUNjeeXUuQZGwLaEBuCujcvWb`](https://nile.tronscan.org/#/contract/TUdqkmExXXUNjeeXUuQZGwLaEBuCujcvWb) | +| **TRON Nile** | Testnet | [`TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW`](https://nile.tronscan.org/#/contract/TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW) | [`TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4`](https://nile.tronscan.org/#/contract/TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4) | | Network | Chain / Environment | PaymentPermit Address | |-----------|---------------------|------------------------| diff --git a/contracts/periphery/ERC3009DepositCollector.sol b/contracts/periphery/ERC3009DepositCollector.sol index 4b55ed5..19f329d 100644 --- a/contracts/periphery/ERC3009DepositCollector.sol +++ b/contracts/periphery/ERC3009DepositCollector.sol @@ -1,8 +1,7 @@ // 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 {SafeTransferLib} from "sun-contract-std/libraries/SafeTransferLib.sol"; import {DepositCollector} from "./DepositCollector.sol"; import {IDepositCollector} from "../interfaces/IDepositCollector.sol"; @@ -17,7 +16,8 @@ import {IERC3009} from "../interfaces/IERC3009.sol"; /// /// @author Coinbase contract ERC3009DepositCollector is DepositCollector { - using SafeERC20 for IERC20; + /// @notice Thrown when the forwarding transfer to `x402BatchSettlement` fails. + error TransferFailed(); /// @param _x402BatchSettlement The `x402BatchSettlement` contract that receives pulled tokens. constructor( @@ -47,6 +47,6 @@ contract ERC3009DepositCollector is DepositCollector { payer, address(this), amount, validAfter, validBefore, expectedNonce, signature ); - IERC20(token).safeTransfer(x402BatchSettlement, amount); + if (!SafeTransferLib.safeTransfer(token, x402BatchSettlement, amount)) revert TransferFailed(); } } diff --git a/contracts/x402BatchSettlement.sol b/contracts/x402BatchSettlement.sol index 2661360..ebff0d8 100644 --- a/contracts/x402BatchSettlement.sol +++ b/contracts/x402BatchSettlement.sol @@ -6,9 +6,10 @@ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol"; import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import {SafeTransferLib} from "sun-contract-std/libraries/SafeTransferLib.sol"; + import {IDepositCollector} from "./interfaces/IDepositCollector.sol"; /// @title x402BatchSettlement @@ -22,7 +23,6 @@ import {IDepositCollector} from "./interfaces/IDepositCollector.sol"; /// /// @author Coinbase contract x402BatchSettlement is EIP712, Multicall, ReentrancyGuardTransient { - using SafeERC20 for IERC20; // ========================================================================= // Structs @@ -176,6 +176,7 @@ contract x402BatchSettlement is EIP712, Multicall, ReentrancyGuardTransient { error InvalidCollector(); error InvalidRefundNonce(); error ZeroRefund(); + error TransferFailed(); // ========================================================================= // Constructor @@ -301,7 +302,7 @@ contract x402BatchSettlement is EIP712, Multicall, ReentrancyGuardTransient { rs.totalSettled = rs.totalClaimed; - IERC20(token).safeTransfer(receiver, amount); + if (!SafeTransferLib.safeTransfer(token, receiver, amount)) revert TransferFailed(); emit Settled(receiver, token, msg.sender, amount); } @@ -381,7 +382,7 @@ contract x402BatchSettlement is EIP712, Multicall, ReentrancyGuardTransient { } if (withdrawAmount > 0) { - IERC20(config.token).safeTransfer(config.payer, withdrawAmount); + if (!SafeTransferLib.safeTransfer(config.token, config.payer, withdrawAmount)) revert TransferFailed(); } } @@ -582,6 +583,6 @@ contract x402BatchSettlement is EIP712, Multicall, ReentrancyGuardTransient { emit ChannelClosed(channelId, config); } - IERC20(config.token).safeTransfer(config.payer, refundAmount); + if (!SafeTransferLib.safeTransfer(config.token, config.payer, refundAmount)) revert TransferFailed(); } } From 1b0052ae889a9360701ae4e1cfc5c1729a96a5ff Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Fri, 19 Jun 2026 17:14:53 +0800 Subject: [PATCH 07/12] update bsc address --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 41e157a..a00a505 100644 --- a/README.md +++ b/README.md @@ -46,18 +46,18 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | — | — | — | | **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA`](https://nile.tronscan.org/#/contract/TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | +| **BSC Mainnet** | Mainnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | [`0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003`](https://bscscan.com/address/0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003) | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) | +| **BSC Testnet** | Testnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://testnet.bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://testnet.bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | — | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://testnet.bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) | + +> BSC proxy/batch contracts use deterministic (CREATE2) addresses shared across EVM chains. Verified on-chain: `x402ExactPermit2Proxy` and `x402UptoPermit2Proxy` are deployed on both BSC Mainnet and Testnet; `x402BatchSettlement` (and its deposit collectors) are deployed on **BSC Mainnet only** — not yet on BSC Testnet. ### Deposit Collectors | Network | Chain / Environment | ERC3009DepositCollector | Permit2DepositCollector | |-----------|---------------------|--------------------------|--------------------------| | **TRON Nile** | Testnet | [`TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW`](https://nile.tronscan.org/#/contract/TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW) | [`TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4`](https://nile.tronscan.org/#/contract/TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4) | - -| Network | Chain / Environment | PaymentPermit Address | -|-----------|---------------------|------------------------| -| **BSC Mainnet** | Mainnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | -| **BSC Testnet** | Testnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://testnet.bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | - +| **BSC Mainnet** | Mainnet | [`0x4020806089470a89826cB9fB1f4059150b550004`](https://bscscan.com/address/0x4020806089470a89826cB9fB1f4059150b550004) | [`0x4020425FAf3B746C082C2f942b4E5159887B0005`](https://bscscan.com/address/0x4020425FAf3B746C082C2f942b4E5159887B0005) | +| **BSC Testnet** | Testnet | — | — | --- From fbc0d0e49deba047a0058d8d0a729ad0cf635c59 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Mon, 22 Jun 2026 14:36:36 +0800 Subject: [PATCH 08/12] Deploy x402 contracts to TRON mainnet; wire TronGrid API key - Add TRON-PRO-API-KEY header (TRONGRID_API_KEY) to tron/nile/shasta networks - Skip legacy PaymentPermit in deploys - Select Permit2 address per network (mainnet/Nile) in Permit2DepositCollector - Record TRON mainnet addresses in README Co-Authored-By: Claude Opus 4.8 --- README.md | 3 ++- deployTron/01_deploy_PaymentPermit.ts | 4 ++++ .../06_deploy_Permit2DepositCollector.ts | 19 ++++++++++++++----- hardhat.config.ts | 8 ++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a00a505..e5c352b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | Network | Chain / Environment | PaymentPermit Address | x402ExactPermit2Proxy | x402BatchSettlement | x402UptoPermit2Proxy | |-----------|---------------------|------------------------|------------------------|------------------------|------------------------| -| **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | — | — | — | +| **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | [`TSaZpDMntELqULvLpyrn61JRATfvSpgNuz`](https://tronscan.org/#/contract/TSaZpDMntELqULvLpyrn61JRATfvSpgNuz) | [`TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm`](https://tronscan.org/#/contract/TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm) | [`TK3imNBs3PL3BGKtee9nEcbnNxBtMtequq`](https://tronscan.org/#/contract/TK3imNBs3PL3BGKtee9nEcbnNxBtMtequq) | | **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA`](https://nile.tronscan.org/#/contract/TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | | **BSC Mainnet** | Mainnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | [`0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003`](https://bscscan.com/address/0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003) | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) | @@ -55,6 +55,7 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | Network | Chain / Environment | ERC3009DepositCollector | Permit2DepositCollector | |-----------|---------------------|--------------------------|--------------------------| +| **TRON Mainnet** | Mainnet | [`TTWA7aWMdx4jfcbp8XRAS2JAd2sUhyF9qj`](https://tronscan.org/#/contract/TTWA7aWMdx4jfcbp8XRAS2JAd2sUhyF9qj) | [`TAg5qqp1K9x5KeSTWnRa8LT79B5HUjzSHY`](https://tronscan.org/#/contract/TAg5qqp1K9x5KeSTWnRa8LT79B5HUjzSHY) | | **TRON Nile** | Testnet | [`TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW`](https://nile.tronscan.org/#/contract/TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW) | [`TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4`](https://nile.tronscan.org/#/contract/TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4) | | **BSC Mainnet** | Mainnet | [`0x4020806089470a89826cB9fB1f4059150b550004`](https://bscscan.com/address/0x4020806089470a89826cB9fB1f4059150b550004) | [`0x4020425FAf3B746C082C2f942b4E5159887B0005`](https://bscscan.com/address/0x4020425FAf3B746C082C2f942b4E5159887B0005) | | **BSC Testnet** | Testnet | — | — | diff --git a/deployTron/01_deploy_PaymentPermit.ts b/deployTron/01_deploy_PaymentPermit.ts index 60dc915..97594a0 100644 --- a/deployTron/01_deploy_PaymentPermit.ts +++ b/deployTron/01_deploy_PaymentPermit.ts @@ -14,3 +14,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }; export default func; func.tags = ['PaymentPermit']; +// PaymentPermit is a legacy contract and is no longer deployed. +// Always skipped (applies to full deploys and `--tags PaymentPermit` alike); +// remove this guard if it ever needs to be deployed again. +func.skip = async () => true; diff --git a/deployTron/06_deploy_Permit2DepositCollector.ts b/deployTron/06_deploy_Permit2DepositCollector.ts index 68931fb..ec8d049 100644 --- a/deployTron/06_deploy_Permit2DepositCollector.ts +++ b/deployTron/06_deploy_Permit2DepositCollector.ts @@ -3,10 +3,14 @@ import { DeployFunction } from 'hardhat-deploy/types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const TronWeb = require('tronweb').TronWeb || require('tronweb'); -// Permit2 address on the target Tron network. -// base58: TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h -// Override with PERMIT2_ADDRESS if the target Tron network uses a different one. -const CANONICAL_PERMIT2 = '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b'; +// Permit2 address per Tron network (Shasta has no Permit2 deployed). +// Override with PERMIT2_ADDRESS to target a different one. +// tron (mainnet): TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9 +// nile (testnet): TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h +const PERMIT2_BY_NETWORK: Record = { + tron: '0xbe365314f2e77fd1257d60c346bb32dbda369403', + nile: '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b', +}; // Convert a Tron base58 address to its 20-byte EVM hex form (drops the 0x41 prefix). const toEvmAddress = (addr: string): string => @@ -22,7 +26,12 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const batchSettlement = toEvmAddress( process.env.X402_BATCH_SETTLEMENT || (await get('x402BatchSettlement')).address ); - const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + const permit2 = process.env.PERMIT2_ADDRESS || PERMIT2_BY_NETWORK[hre.network.name]; + if (!permit2) { + throw new Error( + `No Permit2 address for network "${hre.network.name}". Set PERMIT2_ADDRESS to override.` + ); + } await deploy('Permit2DepositCollector', { from: deployer, diff --git a/hardhat.config.ts b/hardhat.config.ts index 18de982..109159b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -15,6 +15,11 @@ const settings = { }, } +// TronGrid API key (raises rate limits / avoids 429). Sent as TRON-PRO-API-KEY header. +const tronHeaders = process.env.TRONGRID_API_KEY + ? { httpHeaders: { 'TRON-PRO-API-KEY': process.env.TRONGRID_API_KEY } } + : {} + const config: HardhatUserConfig = { solidity: { compilers: [ @@ -33,18 +38,21 @@ const config: HardhatUserConfig = { tron: true, // enable tron network deploy: ['deployTron/'], // folder for tron deploy scripts accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy + ...tronHeaders, // TronGrid API key header (if TRONGRID_API_KEY set) }, nile: { url: process.env.TRON_RPC_URL || 'https://nile.trongrid.io/jsonrpc', // nile rpc url tron: true, // enable nile network deploy: ['deployTron/'], // folder for nile deploy scripts accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy + ...tronHeaders, // TronGrid API key header (if TRONGRID_API_KEY set) }, shasta: { url: process.env.TRON_RPC_URL || 'https://api.shasta.trongrid.io/jsonrpc', // shasta rpc url tron: true, // enable shasta network deploy: ['deployTron/'], // folder for shasta deploy scripts accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy + ...tronHeaders, // TronGrid API key header (if TRONGRID_API_KEY set) }, }, tronSolc: { From 9cc9eb86fff36a608557a6c6d0297e3fe7368d77 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Mon, 22 Jun 2026 15:16:37 +0800 Subject: [PATCH 09/12] Delegate BSC deployment to x402-foundation/x402; record BSC testnet addresses - Remove EVM proxy/batchSettlement deploy scripts (deploy/02-04); keep only PaymentPermit - README: note BSC contracts are deployed from x402-foundation/x402 (CREATE2), addresses for reference - README: record BSC Testnet x402BatchSettlement + deposit collector addresses - Add bscTestnet network to hardhat.config.ts Co-Authored-By: Claude Opus 4.8 --- README.md | 8 +++++--- deploy/02_deploy_x402ExactPermit2Proxy.ts | 21 --------------------- deploy/03_deploy_x402BatchSettlement.ts | 16 ---------------- deploy/04_deploy_x402UptoPermit2Proxy.ts | 21 --------------------- hardhat.config.ts | 6 ++++++ 5 files changed, 11 insertions(+), 61 deletions(-) delete mode 100644 deploy/02_deploy_x402ExactPermit2Proxy.ts delete mode 100644 deploy/03_deploy_x402BatchSettlement.ts delete mode 100644 deploy/04_deploy_x402UptoPermit2Proxy.ts diff --git a/README.md b/README.md index e5c352b..d1efb90 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,11 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA`](https://nile.tronscan.org/#/contract/TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | | **BSC Mainnet** | Mainnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | [`0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003`](https://bscscan.com/address/0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003) | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) | -| **BSC Testnet** | Testnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://testnet.bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://testnet.bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | — | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://testnet.bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) | +| **BSC Testnet** | Testnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://testnet.bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://testnet.bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | [`0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003`](https://testnet.bscscan.com/address/0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003) | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://testnet.bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) | -> BSC proxy/batch contracts use deterministic (CREATE2) addresses shared across EVM chains. Verified on-chain: `x402ExactPermit2Proxy` and `x402UptoPermit2Proxy` are deployed on both BSC Mainnet and Testnet; `x402BatchSettlement` (and its deposit collectors) are deployed on **BSC Mainnet only** — not yet on BSC Testnet. +> BSC proxy/batch contracts use deterministic (CREATE2) addresses shared across EVM chains. Verified on-chain: `x402ExactPermit2Proxy`, `x402UptoPermit2Proxy`, `x402BatchSettlement`, and both deposit collectors are deployed on **both BSC Mainnet and Testnet** at identical CREATE2 addresses. + +> **BSC contracts are deployed from [x402-foundation/x402](https://github.com/x402-foundation/x402/tree/main/contracts/evm)** using its CREATE2 deterministic scripts (mined vanity salts, `cbor_metadata = false`). This repository does not deploy BSC contracts — the addresses above are recorded here for reference only. ### Deposit Collectors @@ -58,7 +60,7 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → | **TRON Mainnet** | Mainnet | [`TTWA7aWMdx4jfcbp8XRAS2JAd2sUhyF9qj`](https://tronscan.org/#/contract/TTWA7aWMdx4jfcbp8XRAS2JAd2sUhyF9qj) | [`TAg5qqp1K9x5KeSTWnRa8LT79B5HUjzSHY`](https://tronscan.org/#/contract/TAg5qqp1K9x5KeSTWnRa8LT79B5HUjzSHY) | | **TRON Nile** | Testnet | [`TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW`](https://nile.tronscan.org/#/contract/TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW) | [`TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4`](https://nile.tronscan.org/#/contract/TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4) | | **BSC Mainnet** | Mainnet | [`0x4020806089470a89826cB9fB1f4059150b550004`](https://bscscan.com/address/0x4020806089470a89826cB9fB1f4059150b550004) | [`0x4020425FAf3B746C082C2f942b4E5159887B0005`](https://bscscan.com/address/0x4020425FAf3B746C082C2f942b4E5159887B0005) | -| **BSC Testnet** | Testnet | — | — | +| **BSC Testnet** | Testnet | [`0x4020806089470a89826cB9fB1f4059150b550004`](https://testnet.bscscan.com/address/0x4020806089470a89826cB9fB1f4059150b550004) | [`0x4020425FAf3B746C082C2f942b4E5159887B0005`](https://testnet.bscscan.com/address/0x4020425FAf3B746C082C2f942b4E5159887B0005) | --- diff --git a/deploy/02_deploy_x402ExactPermit2Proxy.ts b/deploy/02_deploy_x402ExactPermit2Proxy.ts deleted file mode 100644 index 3de3c8c..0000000 --- a/deploy/02_deploy_x402ExactPermit2Proxy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). -const CANONICAL_PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; - -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { deployments, getNamedAccounts } = hre; - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; - - await deploy('x402ExactPermit2Proxy', { - from: deployer, - args: [permit2], - log: true, - }); -}; -export default func; -func.tags = ['x402ExactPermit2Proxy']; diff --git a/deploy/03_deploy_x402BatchSettlement.ts b/deploy/03_deploy_x402BatchSettlement.ts deleted file mode 100644 index 687912a..0000000 --- a/deploy/03_deploy_x402BatchSettlement.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { deployments, getNamedAccounts } = hre; - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - await deploy('x402BatchSettlement', { - from: deployer, - args: [], - log: true, - }); -}; -export default func; -func.tags = ['x402BatchSettlement']; diff --git a/deploy/04_deploy_x402UptoPermit2Proxy.ts b/deploy/04_deploy_x402UptoPermit2Proxy.ts deleted file mode 100644 index af228e2..0000000 --- a/deploy/04_deploy_x402UptoPermit2Proxy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; - -// Canonical Permit2 address, identical on all EVM chains (deployed via CREATE2). -const CANONICAL_PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; - -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { deployments, getNamedAccounts } = hre; - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; - - await deploy('x402UptoPermit2Proxy', { - from: deployer, - args: [permit2], - log: true, - }); -}; -export default func; -func.tags = ['x402UptoPermit2Proxy']; diff --git a/hardhat.config.ts b/hardhat.config.ts index 109159b..e43cede 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -54,6 +54,12 @@ const config: HardhatUserConfig = { accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy ...tronHeaders, // TronGrid API key header (if TRONGRID_API_KEY set) }, + bscTestnet: { + url: process.env.BSC_TESTNET_RPC_URL || 'https://bsc-testnet.publicnode.com', // bsc testnet rpc url + chainId: 97, // bsc testnet chain id + // no `deploy` field => uses default deploy/ (EVM scripts); no `tron` flag => standard EVM flow + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], // account private key for deploy + }, }, tronSolc: { enable: true, // enable tron solc compiler From 29de8416ebc3467c1f7f0e6268b3cc04b9de4eb4 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Tue, 30 Jun 2026 10:06:39 +0800 Subject: [PATCH 10/12] Refresh docs for Permit2 architecture Rewrite README sections (Features, Architecture, Project Layout, Integration, Security) to describe the current x402 Permit2 proxy and batch-settlement contracts instead of the legacy PaymentPermit design. Drop stale AGENTS.md and add CLAUDE.md with repo guardrails. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 81 ------------------------------------------------------- CLAUDE.md | 37 +++++++++++++++++++++++++ README.md | 61 +++++++++++++++++++++-------------------- 3 files changed, 67 insertions(+), 112 deletions(-) delete mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 0d18472..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,81 +0,0 @@ -# AGENTS.md - -> [!NOTE] -> This is a machine-readable open standard for AI coding agents to understand the x402-contracts project. - -## 1. Project Context -x402-contracts is a modular decentralized payment and delivery protocol implementation. It separates the payment authorization (via EIP-712 permits) from the actual fulfillment/delivery (via callback agents). - -**Core Mission**: Enable gasless, atomic "Payment-then-Delivery" cycles. - ---- - -## 2. Environment & Setup - -### Requirements -- **Solidity Version**: `^0.8.20` -- **Dependency Management**: Standard Foundry/Hardhat environment. -- **Key Libraries**: - - `solmate`: Used for standard ERC20 operations. - - `sun-contract-std`: Critical for TRON network compatibility (USDT). - -### Build & Test -- Build: `npx hardhat compile` or `forge build` -- Test: `npx hardhat test` or `forge test` - ---- - -## 3. Core Architecture for AI Agents - -When interacting with this codebase, AI agents must understand the following mapping: - -| Component | Responsibility | Relevant Files | -| :--- | :--- | :--- | -| **Payment Hub** | Entry point for EIP-712 permits and cash flow. | `PaymentPermit.sol` | -| **Interface** | Defines the "Contract" between Hub and Agents. | `IAgentExecInterface.sol` | -| **Logic Libraries** | Hash calculation and safe transfers. | `PermitHash.sol`, `SafeTransferLib.sol` | -| **Agents** | Fulfillment logic (NFT minting, tokens, etc.). | `contracts/merchant_demo/MerchantAgent.sol` | - ---- - -## 4. Coding & Security Guardrails (STRICT) - -### AI Constraint 1: Access Control (MANDATORY) -Every implementation of an Agent's `Execute` function **MUST** include a caller restriction to ensure only the authorized `PaymentPermit` contract can trigger it. - -```solidity -// Required pattern for AI generated agents -modifier onlyPaymentPermit() { - require(msg.sender == paymentPermit, "Unauthorized"); - _; -} -``` - -### AI Constraint 2: Signature Integrity -The `permitTransferFromWithCallback` expects that **all** critical parameters (including delivery details) are covered by the user's signature. -- **Reference**: `PermitHash.sol` for TypeHash definitions. -- **Constraint**: Do not implement callbacks that rely on unsigned `data` for pricing or critical asset distribution. - -### AI Constraint 3: TRON USDT Compatibility -When generating transfer code for USDT on TRON (Nile/Mainnet), agents **MUST** use `SafeTransferLib` to handle non-returning boolean calls. - ---- - -## 5. Typical Workflows for AI - -### Workflow A: Adding a new Fulfillment Agent -1. Create a contract inheriting `IAgentExecInterface`. -2. Implement `Execute(bytes calldata data)`. -3. Apply `onlyPaymentPermit` modifier. -4. Add E2E tests in `test/` ensuring the callback is correctly triggered by `PaymentPermit`. - -### Workflow B: Modifying Permitted Data -1. Update `IPaymentPermit.sol` structs. -2. Update `PermitHash.sol` TypeHash strings and `hash()` functions. -3. Regenerate client-side signing logic. - ---- - -## 6. Resources & Knowledge Base -- **Audit Findings**: Refer to `audit_report.md` for historical vulnerabilities and mitigations. -- **API Spec**: See `IPaymentPermit.sol` for a complete description of available endpoints. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7b04f6d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,37 @@ +# CLAUDE.md + +Guidance for AI agents working in this repo. For protocol/architecture overview, see [README.md](README.md). + +## What this repo deploys + +The current, deployed contracts are the x402 Permit2 proxies and batch-settlement: + +- `contracts/x402ExactPermit2Proxy.sol` — `exact` scheme +- `contracts/x402UptoPermit2Proxy.sol` — `upto` scheme +- `contracts/x402BatchSettlement.sol` — `batch-settlement` payment channels +- `contracts/periphery/` — deposit collectors (ERC-3009 / Permit2) + +## Guardrails (read before changing contracts) + +- **Do not touch `contracts/legacy/`.** It holds the old `PaymentPermit` design; it is retained for reference and is not deployed. Don't wire new code to it. +- **BSC is not deployed from this repo.** BSC contracts come from [`x402-foundation/x402`](https://github.com/x402-foundation/x402/tree/main/contracts/evm) via its CREATE2 scripts; this repo only records the addresses (see README). Do not add or edit BSC deploy logic here. +- **Preserve CREATE2 determinism.** These contracts are expected at identical addresses across chains. Keep constructor args identical on every chain (e.g. the canonical Permit2 address) — any change to initCode changes the deployed address. +- **TRON USDT transfers use `sun-contract-std`'s `SafeTransferLib`**, because TRON USDT does not return a bool. Use it for token transfers rather than a raw `transfer`. +- **Transient storage required.** Contracts use `ReentrancyGuardTransient` (EIP-1153); only deploy on chains that support transient storage. + +## Build & test + +```bash +npm run compile # hardhat compile (or: forge build) +npm test # hardhat test +npm run test-foundry # forge test -vvv +npm run coverage # hardhat coverage +``` + +Forge tests live in `test/` (`*.t.sol`, plus `*.fork.t.sol` fork tests and `*.gas.t.sol`). + +## Deployment + +- TRON deploy scripts: `deployTron/` (run via `npm run deploy` / `deploy-nile` / `deploy-shasta`). +- Networks are configured in `hardhat.config.ts` (`tron`, `nile`, `shasta`, `bscTestnet`). +- Recorded addresses live in `deployments/` and the README address tables — update both when deploying. diff --git a/README.md b/README.md index d1efb90..7cffdbb 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Smart contracts for the **x402** payment protocol on **TRON** and **BSC**. Enabl ## Features -- **EIP-712 typed permits** — Users sign payment details off-chain; a relayer or backend calls `permitTransferFrom` with the signature. -- **Gasless for the signer** — The submitter pays gas; the signer only needs a one-time `approve` of the PaymentPermit contract. -- **Optional fee** — Permit can include `feeTo` and `feeAmount` for protocol or facilitator fees. -- **Replay protection** — Nonce bitmap per owner; time window via `validAfter` / `validBefore`. +- **Permit2-based settlement** — Payers sign a Permit2 signature; a facilitator settles on-chain. No bespoke approval flow. +- **Witness-bound destination** — The payment destination is signed into a Permit2 witness, so a facilitator cannot redirect funds. +- **Gasless for the payer** — The facilitator pays gas; payers can approve Permit2 via an EIP-2612 `permit` in the same transaction. +- **Multiple schemes** — `exact` (fixed amount), `upto` (facilitator settles up to a cap), and `batch-settlement` (escrow-backed payment channels). --- @@ -30,12 +30,15 @@ Smart contracts for the **x402** payment protocol on **TRON** and **BSC**. Enabl | Component | Role | File(s) | |----------------|------------------------------|-------------------------| -| **PaymentPermit** | Entry point for permits and transfers | `contracts/PaymentPermit.sol` | -| **PermitHash** | EIP-712 struct hashes | `contracts/libraries/PermitHash.sol` | -| **EIP712** | Domain separator and typed data hashing | `contracts/EIP712.sol` | -| **IPaymentPermit** | Structs and interface for permits | `contracts/interface/IPaymentPermit.sol` | +| **x402BasePermit2Proxy** | Shared Permit2 witness-transfer logic | `contracts/x402BasePermit2Proxy.sol` | +| **x402ExactPermit2Proxy** | `exact` scheme — settles the full permitted amount | `contracts/x402ExactPermit2Proxy.sol` | +| **x402UptoPermit2Proxy** | `upto` scheme — facilitator settles up to the cap | `contracts/x402UptoPermit2Proxy.sol` | +| **x402BatchSettlement** | `batch-settlement` scheme — escrow-backed payment channels | `contracts/x402BatchSettlement.sol` | +| **DepositCollector** | Pulls funds into batch-settlement escrow (ERC-3009 / Permit2 variants) | `contracts/periphery/` | -Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → **Relayer/backend** calls `permitTransferFrom(permit, transferDetails, owner, signature)` → Contract pulls tokens from `owner` to `payTo` (and optional `feeTo`) in one shot. +> The original `PaymentPermit` contract is retained under `contracts/legacy/` and is not part of the current deployments. + +Flow (exact/upto): **Payer signs** a Permit2 permit + witness (destination, validity) → **Facilitator** calls `settle(...)` (or `settleWithPermit(...)` to bundle an EIP-2612 approval) → the proxy invokes Permit2's `permitWitnessTransferFrom` to pull tokens straight to the destination. --- @@ -68,39 +71,37 @@ Flow: **User signs** `PaymentPermitDetails` (payment, fee, validity, nonce) → ``` ├── contracts/ -│ ├── PaymentPermit.sol # Main permit & transfer logic -│ ├── EIP712.sol # EIP-712 domain and hashing -│ ├── interface/ -│ │ ├── IPaymentPermit.sol # Permit structs and interface -│ │ └── IEIP712.sol -│ └── libraries/ -│ └── PermitHash.sol # TypeHashes and struct hashes -├── deploy/ # Hardhat deploy scripts +│ ├── x402BasePermit2Proxy.sol # Shared Permit2 witness-transfer logic +│ ├── x402ExactPermit2Proxy.sol # exact scheme +│ ├── x402UptoPermit2Proxy.sol # upto scheme +│ ├── x402BatchSettlement.sol # batch-settlement payment channels +│ ├── periphery/ # Deposit collectors (ERC-3009 / Permit2) +│ ├── interfaces/ # ISignatureTransfer, IERC3009, IDepositCollector +│ └── legacy/ # Retained PaymentPermit (not deployed) +├── deploy/ # Hardhat deploy scripts (EVM) +├── deployTron/ # TRON deploy scripts ├── test/ -│ ├── PaymentPermit.t.sol # Forge/Hardhat tests -│ └── MockERC20.sol ├── hardhat.config.ts -├── foundry.toml -└── AGENTS.md # Guidelines for AI/agent use of this repo +└── foundry.toml ``` --- ## Integration -1. **Domain & types** — Use the same EIP-712 domain name `"PaymentPermit"` and the struct definitions from `IPaymentPermit.sol` and `PermitHash.sol` so that hashes match the contract. Domain separator uses `block.chainid` and contract address (see `EIP712.sol`). -2. **ChainId for signing** — When building EIP-712 typed data, use the chainId of the target network so the signature matches the contract. Wallet/TronLink must use the same chainId. -3. **Sign off-chain** — Build `PaymentPermitDetails` (meta, buyer, caller, payment, fee, delivery), hash with `PermitHash` and domain separator, then sign (e.g. 65-byte `r || s || v`). -4. **Submit on-chain** — Call `permitTransferFrom(permit, transferDetails, owner, signature)`. The `owner` must have approved the PaymentPermit contract for the `payToken` (and have sufficient balance for `amount` plus optional `feeAmount`). +1. **Approve Permit2** — The payer approves the canonical Permit2 contract for the token (once), or supplies an EIP-2612 `permit` so the facilitator can do it inline via `settleWithPermit(...)`. +2. **Sign permit + witness** — Build a Permit2 `PermitTransferFrom` with the proxy as the spender, and a witness binding the destination (`to`), validity (`validAfter`), and—for `upto`—the authorized `facilitator`. Use the target chainId. See each proxy's `WITNESS_TYPEHASH` / `WITNESS_TYPE_STRING`. +3. **Settle on-chain** — The facilitator calls `settle(...)` (or `settleWithPermit(...)`) on the relevant proxy. The proxy validates the witness and pulls tokens to the destination via Permit2. -For full struct and field definitions, see `contracts/interface/IPaymentPermit.sol`. +For batch-settlement (payment channels), see `contracts/x402BatchSettlement.sol` and the collectors in `contracts/periphery/`. --- ## Security -- **Access**: Only the signer’s signature authorizes transfers; no single admin can move user funds. -- **Replay**: Nonces and `validAfter`/`validBefore` limit replay across chains and time. +- **Access**: Only the payer’s signature authorizes transfers; the proxies hold no admin keys and cannot move user funds. +- **Destination binding**: The destination is signed into the Permit2 witness, so a facilitator cannot redirect funds. +- **Replay**: Permit2 nonces and the witness `validAfter` window bound replay; batch-settlement channels use cumulative claim amounts and a refund nonce. We welcome responsible disclosure. Please report issues privately before public disclosure when possible. @@ -115,8 +116,6 @@ We welcome responsible disclosure. Please report issues privately before public ## Contributing 1. Fork the repo and open a branch from `main`. -2. Follow existing style (Solidity ^0.8.20, existing patterns in `PaymentPermit.sol` and `PermitHash.sol`). +2. Follow existing style (Solidity ^0.8.20, existing patterns in the `x402*` proxy and settlement contracts). 3. Add or update tests for new behavior. 4. Open a PR with a clear description; maintainers will review. - -For agent/AI usage of this codebase, see [AGENTS.md](AGENTS.md). From 4a249ded60adf93d4e8adf8c5d7ad7861fcd8e8c Mon Sep 17 00:00:00 2001 From: roger-gan Date: Wed, 1 Jul 2026 17:20:07 +0800 Subject: [PATCH 11/12] update deploy scripts --- deployTron/02_deploy_x402ExactPermit2Proxy.ts | 19 ++++++++++++++----- deployTron/04_deploy_x402UptoPermit2Proxy.ts | 19 ++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/deployTron/02_deploy_x402ExactPermit2Proxy.ts b/deployTron/02_deploy_x402ExactPermit2Proxy.ts index 9ee5ef9..53b212d 100644 --- a/deployTron/02_deploy_x402ExactPermit2Proxy.ts +++ b/deployTron/02_deploy_x402ExactPermit2Proxy.ts @@ -1,17 +1,26 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; -// Permit2 address on the target Tron network. -// base58: TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h -// Override with PERMIT2_ADDRESS if the target Tron network uses a different one. -const CANONICAL_PERMIT2 = '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b'; +// Permit2 address per Tron network (Shasta has no Permit2 deployed). +// Override with PERMIT2_ADDRESS to target a different one. +// tron (mainnet): TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9 +// nile (testnet): TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h +const PERMIT2_BY_NETWORK: Record = { + tron: '0xbe365314f2e77fd1257d60c346bb32dbda369403', + nile: '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b', +}; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; const { deploy } = deployments; const { deployer } = await getNamedAccounts(); - const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + const permit2 = process.env.PERMIT2_ADDRESS || PERMIT2_BY_NETWORK[hre.network.name]; + if (!permit2) { + throw new Error( + `No Permit2 address for network "${hre.network.name}". Set PERMIT2_ADDRESS to override.` + ); + } await deploy('x402ExactPermit2Proxy', { from: deployer, diff --git a/deployTron/04_deploy_x402UptoPermit2Proxy.ts b/deployTron/04_deploy_x402UptoPermit2Proxy.ts index 3916279..3393013 100644 --- a/deployTron/04_deploy_x402UptoPermit2Proxy.ts +++ b/deployTron/04_deploy_x402UptoPermit2Proxy.ts @@ -1,17 +1,26 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; -// Permit2 address on the target Tron network. -// base58: TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h -// Override with PERMIT2_ADDRESS if the target Tron network uses a different one. -const CANONICAL_PERMIT2 = '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b'; +// Permit2 address per Tron network (Shasta has no Permit2 deployed). +// Override with PERMIT2_ADDRESS to target a different one. +// tron (mainnet): TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9 +// nile (testnet): TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h +const PERMIT2_BY_NETWORK: Record = { + tron: '0xbe365314f2e77fd1257d60c346bb32dbda369403', + nile: '0xf62f506D1FaA02e2354a9886B4A200496EE96F4b', +}; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; const { deploy } = deployments; const { deployer } = await getNamedAccounts(); - const permit2 = process.env.PERMIT2_ADDRESS || CANONICAL_PERMIT2; + const permit2 = process.env.PERMIT2_ADDRESS || PERMIT2_BY_NETWORK[hre.network.name]; + if (!permit2) { + throw new Error( + `No Permit2 address for network "${hre.network.name}". Set PERMIT2_ADDRESS to override.` + ); + } await deploy('x402UptoPermit2Proxy', { from: deployer, From e9f6d00f65736b6135c92545f560b020e900210e Mon Sep 17 00:00:00 2001 From: roger-gan Date: Wed, 1 Jul 2026 17:39:46 +0800 Subject: [PATCH 12/12] Update TRON mainnet proxy addresses in README. Record the redeployed x402ExactPermit2Proxy and x402UptoPermit2Proxy contracts wired to mainnet Permit2. Co-authored-by: Cursor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cffdbb..0aa9ab6 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Flow (exact/upto): **Payer signs** a Permit2 permit + witness (destination, vali | Network | Chain / Environment | PaymentPermit Address | x402ExactPermit2Proxy | x402BatchSettlement | x402UptoPermit2Proxy | |-----------|---------------------|------------------------|------------------------|------------------------|------------------------| -| **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | [`TSaZpDMntELqULvLpyrn61JRATfvSpgNuz`](https://tronscan.org/#/contract/TSaZpDMntELqULvLpyrn61JRATfvSpgNuz) | [`TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm`](https://tronscan.org/#/contract/TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm) | [`TK3imNBs3PL3BGKtee9nEcbnNxBtMtequq`](https://tronscan.org/#/contract/TK3imNBs3PL3BGKtee9nEcbnNxBtMtequq) | +| **TRON Mainnet** | Mainnet | [`TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`](https://tronscan.org/#/contract/TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn) | [`TN49yaJmZMZoEdDCqjB4uPzQLHvYkGw95m`](https://tronscan.org/#/contract/TN49yaJmZMZoEdDCqjB4uPzQLHvYkGw95m) | [`TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm`](https://tronscan.org/#/contract/TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm) | [`TBLeFPkfDiweBbYmAPqnakaFBPDt9p93sR`](https://tronscan.org/#/contract/TBLeFPkfDiweBbYmAPqnakaFBPDt9p93sR) | | **TRON Nile** | Testnet | [`TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`](https://nile.tronscan.org/#/contract/TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH) | [`TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F`](https://nile.tronscan.org/#/contract/TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F) | [`TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA`](https://nile.tronscan.org/#/contract/TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA) | [`TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K`](https://nile.tronscan.org/#/contract/TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K) | | **TRON Shasta** | Testnet | [`TR2XninQ3jsvRRLGTifFyUHTBysffooUjt`](https://shasta.tronscan.org/#/contract/TR2XninQ3jsvRRLGTifFyUHTBysffooUjt) | — | — | — | | **BSC Mainnet** | Mainnet | [`0x1825bB32db3443dEc2cc7508b2D818fc13EaD878`](https://bscscan.com/address/0x1825bB32db3443dEc2cc7508b2D818fc13EaD878) | [`0x402085c248EeA27D92E8b30b2C58ed07f9E20001`](https://bscscan.com/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | [`0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003`](https://bscscan.com/address/0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003) | [`0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`](https://bscscan.com/address/0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002) |