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 0814e6b..0aa9ab6 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,28 +30,40 @@ 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. --- ## 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) | [`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) | +| **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`, `x402UptoPermit2Proxy`, `x402BatchSettlement`, and both deposit collectors are deployed on **both BSC Mainnet and Testnet** at identical CREATE2 addresses. -| 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 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 + +| 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 | [`0x4020806089470a89826cB9fB1f4059150b550004`](https://testnet.bscscan.com/address/0x4020806089470a89826cB9fB1f4059150b550004) | [`0x4020425FAf3B746C082C2f942b4E5159887B0005`](https://testnet.bscscan.com/address/0x4020425FAf3B746C082C2f942b4E5159887B0005) | --- @@ -59,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. @@ -106,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). 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..19f329d --- /dev/null +++ b/contracts/periphery/ERC3009DepositCollector.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {SafeTransferLib} from "sun-contract-std/libraries/SafeTransferLib.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 { + /// @notice Thrown when the forwarding transfer to `x402BatchSettlement` fails. + error TransferFailed(); + + /// @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 + ); + + if (!SafeTransferLib.safeTransfer(token, x402BatchSettlement, amount)) revert TransferFailed(); + } +} 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..ebff0d8 --- /dev/null +++ b/contracts/x402BatchSettlement.sol @@ -0,0 +1,588 @@ +// 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 {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 +/// @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 { + + // ========================================================================= + // 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(); + error TransferFailed(); + + // ========================================================================= + // 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; + + if (!SafeTransferLib.safeTransfer(token, receiver, amount)) revert TransferFailed(); + + 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) { + if (!SafeTransferLib.safeTransfer(config.token, config.payer, withdrawAmount)) revert TransferFailed(); + } + } + + /// @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); + } + + if (!SafeTransferLib.safeTransfer(config.token, config.payer, refundAmount)) revert TransferFailed(); + } +} 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/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']; 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/02_deploy_x402ExactPermit2Proxy.ts b/deployTron/02_deploy_x402ExactPermit2Proxy.ts new file mode 100644 index 0000000..53b212d --- /dev/null +++ b/deployTron/02_deploy_x402ExactPermit2Proxy.ts @@ -0,0 +1,32 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +// 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 || 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, + args: [permit2], + log: true, + }); +}; +export default func; +func.tags = ['x402ExactPermit2Proxy']; diff --git a/deploy/02_deploy_MerchantAgent.ts b/deployTron/03_deploy_x402BatchSettlement.ts similarity index 84% rename from deploy/02_deploy_MerchantAgent.ts rename to deployTron/03_deploy_x402BatchSettlement.ts index ea59a73..687912a 100644 --- a/deploy/02_deploy_MerchantAgent.ts +++ b/deployTron/03_deploy_x402BatchSettlement.ts @@ -6,11 +6,11 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); - await deploy('MerchantAgent', { + await deploy('x402BatchSettlement', { from: deployer, args: [], log: true, }); }; export default func; -func.tags = ['MerchantAgent']; +func.tags = ['x402BatchSettlement']; diff --git a/deployTron/04_deploy_x402UptoPermit2Proxy.ts b/deployTron/04_deploy_x402UptoPermit2Proxy.ts new file mode 100644 index 0000000..3393013 --- /dev/null +++ b/deployTron/04_deploy_x402UptoPermit2Proxy.ts @@ -0,0 +1,32 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +// 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 || 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, + args: [permit2], + log: true, + }); +}; +export default func; +func.tags = ['x402UptoPermit2Proxy']; 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..ec8d049 --- /dev/null +++ b/deployTron/06_deploy_Permit2DepositCollector.ts @@ -0,0 +1,44 @@ +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 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 => + 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 || 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, + args: [batchSettlement, permit2], + log: true, + }); +}; +export default func; +func.tags = ['Permit2DepositCollector']; +func.dependencies = ['x402BatchSettlement']; diff --git a/hardhat.config.ts b/hardhat.config.ts index d49c522..e43cede 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,12 +8,18 @@ import * as dotenv from 'dotenv' dotenv.config() const settings = { + evmVersion: 'cancun', optimizer: { enabled: true, // enabled for optimizer runs: 999999, // runs time for optimizer run }, } +// 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: [ @@ -32,17 +38,26 @@ 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: ['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 + ...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: ['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 + ...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 }, }, 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); + } +}