diff --git a/README.md b/README.md index d256a5f..c2ecfbc 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ optimizer settings. ## Components -Seven top-level contracts in [`src/`](src/), grouped by role: +Six top-level contracts in [`src/`](src/), grouped by role: ### Asset custody (permanent, audited, not versioned) @@ -50,8 +50,11 @@ Seven top-level contracts in [`src/`](src/), grouped by role: conditional-payment results, indexed by `payId = keccak256(payHash, setterAddress)`. - **[VirtContractResolver](src/VirtContractResolver.sol)** — On-demand deployer for virtual (off-chain) contracts when disputes need them on-chain. -- **[EthPool](src/EthPool.sol)** — ERC-20-shaped wrapper for native ETH; enables - single-tx channel opening via a uniform `transferFrom` flow. + +`CelerLedger` additionally depends on the chain's canonical wrapped-native +(WETH9-style) contract for the multi-party-funding path on native channels — +wired at deploy time via the `_nativeWrap` constructor argument. Users still +deposit and receive native; wrapped-native is internal plumbing only. ### Channel & payment logic (versioned, peer-controlled migration) @@ -78,7 +81,6 @@ src/ ├── CelerLedger.sol # channel state machine; primary entry point (versioned) ├── CelerLedgerMock.sol # test-only ledger variant ├── CelerWallet.sol # multi-owner asset custodian (permanent) -├── EthPool.sol # ERC20-like wrapper for native ETH ├── PayRegistry.sol # global resolved-payment registry (permanent) ├── PayResolver.sol # conditional-pay resolution (versioned) ├── RouterRegistry.sol # optional relay-router registry @@ -110,11 +112,12 @@ gitignored. ## Supported tokens -ETH and **plain ERC-20** only. Tokens whose `transferFrom` delivers anything -other than the requested amount — fee-on-transfer, deflationary, rebasing, -ERC-777 with hooks, etc. — are **not supported**: channel accounting credits -the requested amount, so any actual-vs-requested mismatch will desync the -wallet's internal balance from its real token holdings. +Native (e.g. ETH) and **plain ERC-20** only. Tokens whose `transferFrom` +delivers anything other than the requested amount — fee-on-transfer, +deflationary, rebasing, ERC-777 with hooks, etc. — are **not supported**: +channel accounting credits the requested amount, so any actual-vs-requested +mismatch will desync the wallet's internal balance from its real token +holdings. --- diff --git a/docs/architecture-summary.md b/docs/architecture-summary.md index 6fa781e..5dc1b96 100644 --- a/docs/architecture-summary.md +++ b/docs/architecture-summary.md @@ -20,13 +20,17 @@ activity happens off-chain: peers exchange co-signed simplex states and forward conditional payments through routed paths. The blockchain is only touched for deposits, withdrawals, settlement, dispute resolution, and (rarely) deploying virtual contracts. -The seven contracts split cleanly into two roles. **Asset custody** lives in +The six contracts split cleanly into two roles. **Asset custody** lives in permanent, audited contracts that change rarely or never (`CelerWallet`, `PayRegistry`, -`VirtContractResolver`, `EthPool`). **Channel and payment logic** lives in *versioned* +`VirtContractResolver`). **Channel and payment logic** lives in *versioned* contracts (`CelerLedger`, `PayResolver`) that peers can cooperatively migrate between without disturbing the assets — see [Decentralized Versioning][versioning] in the full docs. `RouterRegistry` is an optional advertisement registry for relay nodes. +`CelerLedger` additionally depends on the chain's canonical wrapped-native +(wrapped-native) contract for the multi-party-funding path on native channels — +wired at deploy time, never user-visible. Users still deposit and receive native. + [versioning]: https://agentpay-docs.celer.network/agentpay-architecture/on-chain-contracts/decentralized-versioning --- @@ -100,7 +104,6 @@ For the full state-transition rules, see | [`PayResolver`](../src/PayResolver.sol) | On-chain conditional-pay resolution; writes results to `PayRegistry`. | **Yes** (chosen per-payment) | | [`PayRegistry`](../src/PayRegistry.sol) | Global `payId → (amount, deadline)` map; immutable, public reference. | No (permanent) | | [`VirtContractResolver`](../src/VirtContractResolver.sol) | On-demand deployment of virtual contracts during disputes. | No (permanent) | -| [`EthPool`](../src/EthPool.sol) | ERC20-like wrapper for native ETH; enables single-tx channel opening. | No | | [`RouterRegistry`](../src/RouterRegistry.sol) | Optional registry for relay-router self-advertisement. | No | For per-contract APIs (constructor args, external functions, events, storage), see diff --git a/docs/contracts.md b/docs/contracts.md index 67de775..7d72654 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -12,7 +12,6 @@ contract file is authoritative. - [PayResolver](#payresolver) - [PayRegistry](#payregistry) - [VirtContractResolver](#virtcontractresolver) -- [EthPool](#ethpool) - [RouterRegistry](#routerregistry) - [Ledger libraries](#ledger-libraries) (where the channel logic actually lives) - [Helpers and mocks](#helpers-and-mocks) @@ -29,8 +28,8 @@ future versions) is an *operator* over individual wallets within it. The contrac deliberately minimal logic — it only knows how to deposit, withdraw, and transfer operatorship — to keep the audit surface small. -> **Supported tokens:** ETH and plain ERC-20 only. `depositERC20` credits the -> requested `_amount`, so tokens that deliver less than requested +> **Supported tokens:** native (e.g. ETH) and plain ERC-20 only. `depositERC20` +> credits the requested `_amount`, so tokens that deliver less than requested > (fee-on-transfer, deflationary, rebasing, ERC-777 hooks) will desync this > wallet's accounting from its real token balance. Use only standard ERC-20s. @@ -55,7 +54,7 @@ can `pause` / `unpause` and (when paused) `drainToken` to recover stuck funds. | Function | Caller | Purpose | |---|---|---| | [`create`](../src/CelerWallet.sol#L66) | anyone (typically a `CelerLedger`) | Create a new wallet for a peer-pair, returning its `walletId`. | -| [`depositETH`](../src/CelerWallet.sol#L89) | anyone (payable) | Deposit native ETH into a wallet. | +| [`depositNative`](../src/CelerWallet.sol#L89) | anyone (payable) | Deposit native (e.g., ETH) into a wallet. | | [`depositERC20`](../src/CelerWallet.sol#L101) | anyone | Deposit ERC-20 tokens (requires prior `approve`). | | [`withdraw`](../src/CelerWallet.sol#L119) | operator only | Withdraw funds to a receiver. | | [`transferToWallet`](../src/CelerWallet.sol#L141) | operator only | Move funds between two wallets sharing the same operator (channel rebalancing). | @@ -107,20 +106,20 @@ wrapper — the actual logic is split across five libraries under [`src/lib/ledgerlib/`](../src/lib/ledgerlib/) and attached via `using ... for ...`. See [Ledger libraries](#ledger-libraries) below. -> **Supported tokens:** ETH and plain ERC-20 only — see the same note under -> [CelerWallet](#celerwallet). Non-standard ERC-20s +> **Supported tokens:** native (e.g. ETH) and plain ERC-20 only — see the same +> note under [CelerWallet](#celerwallet). Non-standard ERC-20s > (fee-on-transfer / rebasing / ERC-777 hooks) will desync channel accounting > from the wallet's real token balance. ### Constructor ```solidity -constructor(address _ethPool, address _payRegistry, address _celerWallet) Ownable(msg.sender) +constructor(address _nativeWrap, address _payRegistry, address _celerWallet) Ownable(msg.sender) ``` | Param | Purpose | |---|---| -| `_ethPool` | Address of the deployed [`EthPool`](#ethpool). | +| `_nativeWrap` | Chain's canonical wrapped-native (wrapped-native) address. Used internally as a funding-flow primitive for native channels; never user-visible. Constructor-set; no setter. | | `_payRegistry` | Address of the deployed [`PayRegistry`](#payregistry). | | `_celerWallet` | Address of the deployed [`CelerWallet`](#celerwallet). | @@ -132,8 +131,8 @@ Balance limits are **enabled by default** post-deployment. Configure them via | Function | Purpose | |---|---| | [`openChannel`](../src/CelerLedger.sol#L66) | Open a fully-funded channel from a co-signed `PaymentChannelInitializer` (single tx). | -| [`deposit`](../src/CelerLedger.sol#L78) | Deposit ETH (msg.value) and/or pull from `EthPool`/ERC20 into a channel. | -| [`depositInBatch`](../src/CelerLedger.sol#L91) | Batch deposit across multiple channels in one tx. | +| [`deposit`](../src/CelerLedger.sol#L78) | Deposit native (msg.value) and/or pull from pre-approved wrapped-native or ERC-20 into a channel. | +| [`depositInBatch`](../src/CelerLedger.sol#L91) | Batch deposit across multiple channels in one tx. Not payable — native entries fund only via pre-approved wrapped-native (no `msg.value` path). | | [`snapshotStates`](../src/CelerLedger.sol#L114) | Persist a co-signed simplex state on-chain (lightweight checkpoint). | ### External functions — withdrawals @@ -175,7 +174,7 @@ A wide set of getters: `getChannelStatus`, `getTokenContract`, `getTokenType`, `getNextPayIdListHashMap`, `getPayClearDeadlineMap`, `getPendingPayOutMap`, `getWithdrawIntent`, `getCooperativeWithdrawSeqNum`, `getSettleFinalizedTime`, `getDisputeTimeout`, `getMigratedTo`, `getChannelMigrationArgs`, -`getPeersMigrationInfo`, `getChannelStatusNum`, `getEthPool`, `getPayRegistry`, +`getPeersMigrationInfo`, `getChannelStatusNum`, `getNativeWrap`, `getPayRegistry`, `getCelerWallet`, `getBalanceLimit`, `getBalanceLimitsEnabled`. See [`ICelerLedger.sol`](../src/lib/interface/ICelerLedger.sol). @@ -190,7 +189,7 @@ Settle: `IntendSettle`, `ClearOnePay`, `ConfirmSettle`, `ConfirmSettleFail`, ```solidity LedgerStruct.Ledger private ledger; -// → channelStatusNums, ethPool, payRegistry, celerWallet, +// → channelStatusNums, nativeWrap, payRegistry, celerWallet, // balanceLimits, balanceLimitsEnabled, channelMap (bytes32 → Channel) ``` @@ -306,31 +305,6 @@ virtual address (used in `Condition.virtual_contract_address`) is --- -## EthPool - -[Source](../src/EthPool.sol) · [Interface](../src/lib/interface/IEthPool.sol) · **Permanent** - -ERC-20-shaped wrapper for native ETH. Used so that `CelerLedger.openChannel` and -`deposit` can pull funds via a uniform `transferFrom` flow regardless of token type. -Etherscan-friendly metadata: `name = "EthInPool"`, `symbol = "EthIP"`, `decimals = 18`. - -### External / public functions - -| Function | Purpose | -|---|---| -| [`deposit`](../src/EthPool.sol#L24) (payable) | Deposit `msg.value` ETH for a receiver. | -| [`withdraw`](../src/EthPool.sol#L35) | Withdraw ETH back to `msg.sender`. | -| [`approve`](../src/EthPool.sol#L44) / [`increaseAllowance`](../src/EthPool.sol#L93) / [`decreaseAllowance`](../src/EthPool.sol#L106) | ERC-20-style allowance management. | -| [`transferFrom`](../src/EthPool.sol#L59) | Pull-based ETH transfer to a payable address. | -| [`transferToCelerWallet`](../src/EthPool.sol#L73) | Specialized transfer that funds a `CelerWallet` wallet directly. | -| [`balanceOf`](../src/EthPool.sol#L119) / [`allowance`](../src/EthPool.sol#L129) | Standard ERC-20 views. | - -### Events - -`Deposit`, `Transfer`, `Approval` (ERC-20 shape). - ---- - ## RouterRegistry [Source](../src/RouterRegistry.sol) · [Interface](../src/lib/interface/IRouterRegistry.sol) · **Permanent** diff --git a/script/DeployCore.s.sol b/script/DeployCore.s.sol index b159b2d..571966a 100644 --- a/script/DeployCore.s.sol +++ b/script/DeployCore.s.sol @@ -2,24 +2,24 @@ pragma solidity ^0.8.20; import {Script, console} from "forge-std/Script.sol"; -import {EthPool} from "../src/EthPool.sol"; import {PayRegistry} from "../src/PayRegistry.sol"; import {VirtContractResolver} from "../src/VirtContractResolver.sol"; import {CelerWallet} from "../src/CelerWallet.sol"; /** * @title DeployCore - * @notice Deploys the four permanent (non-versioned) AgentPay contracts on a fresh - * network: `EthPool`, `PayRegistry`, `VirtContractResolver`, `CelerWallet`. These - * are deployed once per network and shared by every `CelerLedger` / `PayResolver` - * version that follows. + * @notice Deploys the three permanent (non-versioned) AgentPay contracts on a + * fresh network: `PayRegistry`, `VirtContractResolver`, `CelerWallet`. These + * are deployed once per network and shared by every `CelerLedger` / + * `PayResolver` version that follows. * * @dev Usage: * forge script script/DeployCore.s.sol --rpc-url $RPC_URL --broadcast --verify -vv * - * After deploy, paste the four addresses into `config.json`'s `core` block (see - * [`example_config.json`](example_config.json)) before running `DeployLedger` or - * `DeployPayResolver`. + * After deploy, paste the three addresses into `config.json`'s `core` block + * (see [`example_config.json`](example_config.json)) along with the chain's + * canonical `nativeWrap` (wrapped-native) address, before running + * `DeployLedger` or `DeployPayResolver`. * * Environment variables: * PRIVATE_KEY — Deployer private key (required). Deployer becomes the @@ -28,18 +28,16 @@ import {CelerWallet} from "../src/CelerWallet.sol"; contract DeployCore is Script { function run() external - returns (EthPool ethPool, PayRegistry payRegistry, VirtContractResolver virtResolver, CelerWallet celerWallet) + returns (PayRegistry payRegistry, VirtContractResolver virtResolver, CelerWallet celerWallet) { uint256 deployerKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerKey); - ethPool = new EthPool(); payRegistry = new PayRegistry(); virtResolver = new VirtContractResolver(); celerWallet = new CelerWallet(); vm.stopBroadcast(); - console.log("EthPool: ", address(ethPool)); console.log("PayRegistry: ", address(payRegistry)); console.log("VirtContractResolver:", address(virtResolver)); console.log("CelerWallet: ", address(celerWallet)); diff --git a/script/DeployLedger.s.sol b/script/DeployLedger.s.sol index 603292e..40ac801 100644 --- a/script/DeployLedger.s.sol +++ b/script/DeployLedger.s.sol @@ -7,14 +7,16 @@ import {CelerLedger} from "../src/CelerLedger.sol"; /** * @title DeployLedger * @notice Deploys a `CelerLedger` instance wired against the existing core - * contracts. Run once per ledger version — peers cooperatively migrate channels - * between versions; the wallet / registry / pool stay shared. + * contracts and the chain's canonical wrapped-native (wrapped-native) + * contract. Run once per ledger version — peers cooperatively migrate + * channels between versions; the wallet / registry / nativeWrap stay shared. * * @dev Usage: * forge script script/DeployLedger.s.sol --rpc-url $RPC_URL --broadcast --verify -vv * - * Reads the core addresses from `config.json` (or the path in `DEPLOY_CONFIG`). - * See [`example_config.json`](example_config.json) for the schema. + * Reads the core addresses (including `nativeWrap`) from `config.json` (or + * the path in `DEPLOY_CONFIG`). See [`example_config.json`](example_config.json) + * for the schema. * * Environment variables: * PRIVATE_KEY — Deployer private key (required). Deployer becomes the @@ -28,16 +30,16 @@ contract DeployLedger is Script { string memory configPath = vm.envOr("DEPLOY_CONFIG", string("config.json")); string memory config = vm.readFile(configPath); - address ethPool = abi.decode(vm.parseJson(config, ".core.ethPool"), (address)); + address nativeWrap = abi.decode(vm.parseJson(config, ".core.nativeWrap"), (address)); address payRegistry = abi.decode(vm.parseJson(config, ".core.payRegistry"), (address)); address celerWallet = abi.decode(vm.parseJson(config, ".core.celerWallet"), (address)); - require(ethPool != address(0), "ethPool address required"); + require(nativeWrap != address(0), "nativeWrap address required"); require(payRegistry != address(0), "payRegistry address required"); require(celerWallet != address(0), "celerWallet address required"); vm.startBroadcast(deployerKey); - ledger = new CelerLedger(ethPool, payRegistry, celerWallet); + ledger = new CelerLedger(nativeWrap, payRegistry, celerWallet); vm.stopBroadcast(); console.log("CelerLedger: ", address(ledger)); diff --git a/script/README.md b/script/README.md index a85d233..596bb58 100644 --- a/script/README.md +++ b/script/README.md @@ -15,7 +15,7 @@ existing core without touching the asset-custody contracts. | Script | Deploys | Lifecycle | |---|---|---| -| [`DeployCore.s.sol`](DeployCore.s.sol) | `EthPool`, `PayRegistry`, `VirtContractResolver`, `CelerWallet` | Once per network. Permanent — never redeployed. | +| [`DeployCore.s.sol`](DeployCore.s.sol) | `PayRegistry`, `VirtContractResolver`, `CelerWallet` | Once per network. Permanent — never redeployed. | | [`DeployLedger.s.sol`](DeployLedger.s.sol) | `CelerLedger` | Versioned. Run again for each new ledger version; peers cooperatively migrate. | | [`DeployPayResolver.s.sol`](DeployPayResolver.s.sol) | `PayResolver` | Versioned per-payment. Run when adding a new resolver. | | [`DeployRouterRegistry.s.sol`](DeployRouterRegistry.s.sol) | `RouterRegistry` | Optional. Independent of the channel graph. | @@ -33,7 +33,9 @@ set -a; source script/.env; set +a # 3. Deploy the permanent core contracts forge script script/DeployCore.s.sol --rpc-url $RPC_URL --broadcast --verify -vv -# 4. Paste the four addresses from step 3 into config.json's `core` block +# 4. Paste the three addresses from step 3 into config.json's `core` block, +# plus the chain's canonical `nativeWrap` (wrapped-native) address — see +# `example_config.json` for canonical values per chain. # 5. Deploy the first ledger and resolver forge script script/DeployLedger.s.sol --rpc-url $RPC_URL --broadcast --verify -vv @@ -79,7 +81,7 @@ After **`DeployLedger`**: balance limits **enabled by default** but no limits set — every deposit will revert until you do one of: ```bash - # Option A: set caps for the tokens you'll use (address(0) = ETH) + # Option A: set caps for the tokens you'll use (address(0) = native) cast send "setBalanceLimits(address[],uint256[])" "[0x0000000000000000000000000000000000000000]" "[1000000000000000000000]" --rpc-url $RPC_URL --private-key $PRIVATE_KEY # Option B: disable limits entirely diff --git a/script/example_config.json b/script/example_config.json index 2298410..21a9f84 100644 --- a/script/example_config.json +++ b/script/example_config.json @@ -1,10 +1,10 @@ { "_comment": "Copy this file to config.json and fill in the values for your target network.", "core": { - "_comment": "Addresses of the four permanent core contracts. Set after running DeployCore.s.sol; reused by every CelerLedger / PayResolver version.", - "ethPool": "0x0000000000000000000000000000000000000000", + "_comment": "Addresses of the three permanent core contracts (set after running DeployCore.s.sol) plus the chain's canonical wrapped-native (wrapped-native) contract. Reused by every CelerLedger / PayResolver version.", + "nativeWrap": "0x0000000000000000000000000000000000000000", "payRegistry": "0x0000000000000000000000000000000000000000", "virtResolver": "0x0000000000000000000000000000000000000000", "celerWallet": "0x0000000000000000000000000000000000000000" } -} +} \ No newline at end of file diff --git a/src/CelerLedger.sol b/src/CelerLedger.sol index 46de19c..03e3fc7 100644 --- a/src/CelerLedger.sol +++ b/src/CelerLedger.sol @@ -7,7 +7,7 @@ import "./lib/ledgerlib/LedgerBalanceLimit.sol"; import "./lib/ledgerlib/LedgerMigrate.sol"; import "./lib/ledgerlib/LedgerChannel.sol"; import "./lib/interface/ICelerWallet.sol"; -import "./lib/interface/IEthPool.sol"; +import "./lib/interface/INativeWrap.sol"; import "./lib/interface/IPayRegistry.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; @@ -32,23 +32,39 @@ contract CelerLedger is ICelerLedger, Ownable { /** * @notice Construct the ledger and wire it to its dependencies. * @dev Balance limits are enabled by default; configure or disable via the - * owner-only admin functions. - * @param _ethPool Address of the deployed {IEthPool}. + * owner-only admin functions. `_nativeWrap` is constructor-set with no + * setter — effectively immutable for the lifetime of this ledger + * instance. + * @param _nativeWrap Address of the chain's canonical wrapped-native + * (wrapped-native) contract. Used internally as a funding-flow + * primitive for native-token channels; never user-visible. * @param _payRegistry Address of the deployed {IPayRegistry}. * @param _celerWallet Address of the deployed {ICelerWallet} — this ledger must * later become its operator (during channel opening or via wallet creation). */ - constructor(address _ethPool, address _payRegistry, address _celerWallet) Ownable(msg.sender) { - ledger.ethPool = IEthPool(_ethPool); + constructor(address _nativeWrap, address _payRegistry, address _celerWallet) Ownable(msg.sender) { + require(_nativeWrap != address(0), "nativeWrap address required"); + require(_nativeWrap.code.length > 0, "nativeWrap code required"); + ledger.nativeWrap = INativeWrap(_nativeWrap); ledger.payRegistry = IPayRegistry(_payRegistry); ledger.celerWallet = ICelerWallet(_celerWallet); // enable balance limits in default ledger.balanceLimitsEnabled = true; } + /** + * @notice Restricted `receive()` — accepts native only from `nativeWrap`'s + * `withdraw(...)` callback. Reverts on direct sends from any other address + * to keep accidental dust from getting stranded (the ledger has no + * native-drain path). + */ + receive() external payable { + require(msg.sender == address(ledger.nativeWrap), "Only nativeWrap"); + } + /** * @notice Set the per-channel balance limits of given tokens - * @param _tokenAddrs addresses of the tokens (address(0) is for ETH) + * @param _tokenAddrs addresses of the tokens (address(0) is for native) * @param _limits balance limits of the tokens */ function setBalanceLimits(address[] calldata _tokenAddrs, uint256[] calldata _limits) external onlyOwner { @@ -78,11 +94,11 @@ contract CelerLedger is ICelerLedger, Ownable { } /** - * @notice Deposit ETH or ERC20 tokens into the channel + * @notice Deposit native or ERC20 tokens into the channel * @dev total deposit amount = msg.value(must be 0 for ERC20) + _transferFromAmount * @param _channelId ID of the channel * @param _receiver address of the receiver - * @param _transferFromAmount amount of funds to be transfered from EthPool for ETH + * @param _transferFromAmount amount of funds to be transferred from `nativeWrap` (wrapped-native) for native channels * or ERC20 contract for ERC20 tokens */ function deposit(bytes32 _channelId, address _receiver, uint256 _transferFromAmount) external payable { @@ -90,12 +106,15 @@ contract CelerLedger is ICelerLedger, Ownable { } /** - * @notice Deposit ETH via EthPool or ERC20 tokens into the channel - * @dev do not support sending ETH in msg.value for function simplicity. - * Index in three arrays should match. + * @notice Batched variant of {deposit} across multiple channels in one tx. + * @dev Not payable: native-channel entries are funded only via pre-approved + * wrapped-native (pulled from `nativeWrap` and unwrapped per channel); + * `msg.value` funding is unsupported in the batch path. ERC-20 entries + * pull from the corresponding token contract. Index in the three arrays + * must match. * @param _channelIds IDs of the channels * @param _receivers addresses of the receivers - * @param _transferFromAmounts amounts of funds to be transfered from EthPool for ETH + * @param _transferFromAmounts amounts of funds to be transferred from `nativeWrap` (wrapped-native) for native channels * or ERC20 contract for ERC20 tokens */ function depositInBatch( @@ -435,11 +454,11 @@ contract CelerLedger is ICelerLedger, Ownable { } /** - * @notice Return EthPool used by this CelerLedger contract - * @return EthPool address + * @notice Return the wrapped-native (wrapped-native) contract used by this CelerLedger + * @return wrapped-native contract address */ - function getEthPool() external view returns (address) { - return ledger.getEthPool(); + function getNativeWrap() external view returns (address) { + return ledger.getNativeWrap(); } /** diff --git a/src/CelerLedgerMock.sol b/src/CelerLedgerMock.sol index fcad53c..02734fc 100644 --- a/src/CelerLedgerMock.sol +++ b/src/CelerLedgerMock.sol @@ -7,7 +7,7 @@ import "./lib/ledgerlib/LedgerBalanceLimit.sol"; import "./lib/ledgerlib/LedgerMigrate.sol"; import "./lib/ledgerlib/LedgerChannel.sol"; import "./lib/interface/ICelerWallet.sol"; -import "./lib/interface/IEthPool.sol"; +import "./lib/interface/INativeWrap.sol"; import "./lib/interface/IPayRegistry.sol"; /** @@ -27,11 +27,11 @@ contract CelerLedgerMock { /** * @notice CelerLedger constructor - * @param _ethPool address of ETH pool + * @param _nativeWrap address of wrapped-native (wrapped-native) contract * @param _payRegistry address of PayRegistry */ - constructor(address _ethPool, address _payRegistry, address _celerWallet) { - ledger.ethPool = IEthPool(_ethPool); + constructor(address _nativeWrap, address _payRegistry, address _celerWallet) { + ledger.nativeWrap = INativeWrap(_nativeWrap); ledger.payRegistry = IPayRegistry(_payRegistry); ledger.celerWallet = ICelerWallet(_celerWallet); // enable balance limits in default @@ -74,11 +74,11 @@ contract CelerLedgerMock { } /** - * @notice Deposit ETH or ERC20 tokens into the channel + * @notice Deposit native (e.g., ETH) or ERC20 tokens into the channel * @dev total deposit amount = msg.value(must be 0 for ERC20) + _transferFromAmount * @param _channelId ID of the channel * @param _receiver address of the receiver - * @param _transferFromAmount amount of funds to be transfered from EthPool for ETH + * @param _transferFromAmount amount of funds to be transferred from `nativeWrap` (wrapped-native) for native channels * or ERC20 contract for ERC20 tokens */ function deposit(bytes32 _channelId, address _receiver, uint256 _transferFromAmount) external payable { @@ -475,11 +475,11 @@ contract CelerLedgerMock { } /** - * @notice Return EthPool used by this CelerLedger contract - * @return EthPool address + * @notice Return wrapped-native (wrapped-native) contract used by this CelerLedger + * @return wrapped-native contract address */ - function getEthPool() external view returns (address) { - return address(ledger.ethPool); + function getNativeWrap() external view returns (address) { + return address(ledger.nativeWrap); } /** diff --git a/src/CelerWallet.sol b/src/CelerWallet.sol index e07396a..397fad3 100644 --- a/src/CelerWallet.sol +++ b/src/CelerWallet.sol @@ -29,7 +29,7 @@ contract CelerWallet is ICelerWallet, Pausable, Ownable { address[] owners; // corresponding to CelerLedger address operator; - // adderss(0) for ETH + // address(0) for native mapping(address => uint256) balances; address proposedNewOperator; mapping(address => bool) proposalVotes; @@ -88,10 +88,10 @@ contract CelerWallet is ICelerWallet, Pausable, Ownable { } /** - * @notice Deposit ETH to a wallet + * @notice Deposit native (e.g. ETH) to a wallet * @param _walletId id of the wallet to deposit into */ - function depositETH(bytes32 _walletId) public payable whenNotPaused { + function depositNative(bytes32 _walletId) public payable whenNotPaused { uint256 amount = msg.value; _updateBalance(_walletId, address(0), amount, MathOperation.Add); emit DepositToWallet(_walletId, address(0), amount); @@ -112,9 +112,9 @@ contract CelerWallet is ICelerWallet, Pausable, Ownable { /** * @notice Withdraw funds to an address - * @dev Since this withdraw() function uses direct transfer to send ETH, if CelerLedger + * @dev Since this withdraw() function uses direct transfer to send native, if CelerLedger * allows non externally-owned account (EOA) to be a peer of the channel namely an owner - * of the wallet, CelerLedger should implement a withdraw pattern for ETH to avoid + * of the wallet, CelerLedger should implement a withdraw pattern for native to avoid * maliciously fund locking. Withdraw pattern reference: * @param _walletId id of the wallet to withdraw from * @param _tokenAddress address of token to withdraw @@ -273,7 +273,7 @@ contract CelerWallet is ICelerWallet, Pausable, Ownable { function _withdrawToken(address _tokenAddress, address _receiver, uint256 _amount) internal { if (_tokenAddress == address(0)) { (bool success,) = payable(_receiver).call{value: _amount}(""); - require(success, "ETH transfer failed"); + require(success, "Native transfer failed"); } else { IERC20(_tokenAddress).safeTransfer(_receiver, _amount); } diff --git a/src/EthPool.sol b/src/EthPool.sol deleted file mode 100644 index 9f7d7cb..0000000 --- a/src/EthPool.sol +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "./lib/interface/IEthPool.sol"; -import "./lib/interface/ICelerWallet.sol"; - -/** - * @title EthPool - * @notice ERC20-shaped wrapper for native ETH. Lets `ICelerLedger.openChannel` and - * `ICelerLedger.deposit` pull ETH via a uniform `transferFrom` flow regardless of - * the underlying token type. ERC20-like metadata is exposed so that block-explorer - * tooling can monitor the pool as if it were a token contract. - * @dev See {IEthPool} for canonical NatSpec on each function. - */ -contract EthPool is IEthPool { - mapping(address => uint256) private balances; - mapping(address => mapping(address => uint256)) private allowed; - - /// @dev ERC20-shaped metadata; lets etherscan-like tools track EthPool correctly. - string public constant name = "EthInPool"; - string public constant symbol = "EthIP"; - uint8 public constant decimals = 18; - - /** - * @notice Deposit ETH to ETH Pool - * @param _receiver the address ETH is deposited to - */ - function deposit(address _receiver) public payable { - require(_receiver != address(0), "Receiver address is 0"); - - balances[_receiver] = balances[_receiver] + msg.value; - emit Deposit(_receiver, msg.value); - } - - /** - * @notice Withdraw ETH from ETH Pool - * @param _value the amount of ETH to withdraw - */ - function withdraw(uint256 _value) public { - _transfer(msg.sender, payable(msg.sender), _value); - } - - /** - * @notice Approve the passed address to spend the specified amount of ETH on behalf of msg.sender. - * @param _spender The address which will spend the funds. - * @param _value The amount of ETH to be spent. - */ - function approve(address _spender, uint256 _value) public returns (bool) { - require(_spender != address(0), "Spender address is 0"); - - allowed[msg.sender][_spender] = _value; - emit Approval(msg.sender, _spender, _value); - return true; - } - - /** - * @notice Transfer ETH from one address to another. - * @dev Note that while this function emits an Approval event, this is not required as per the specification. - * @param _from The address which you want to transfer ETH from - * @param _to The address which you want to transfer to - * @param _value the amount of ETH to be transferred - */ - function transferFrom(address _from, address payable _to, uint256 _value) public returns (bool) { - allowed[_from][msg.sender] = allowed[_from][msg.sender] - _value; - emit Approval(_from, msg.sender, allowed[_from][msg.sender]); - _transfer(_from, _to, _value); - return true; - } - - /** - * @notice Transfer ETH from one address to a wallet in CelerWallet contract. - * @param _from The address which you want to transfer ETH from - * @param _walletAddr CelerWallet address which should have a depositETH(bytes32) payable API - * @param _walletId id of the wallet you want to deposit ETH into - * @param _value the amount of ETH to be transferred - */ - function transferToCelerWallet(address _from, address _walletAddr, bytes32 _walletId, uint256 _value) - external - returns (bool) - { - allowed[_from][msg.sender] = allowed[_from][msg.sender] - _value; - emit Approval(_from, msg.sender, allowed[_from][msg.sender]); - balances[_from] = balances[_from] - _value; - emit Transfer(_from, _walletAddr, _value); - - ICelerWallet wallet = ICelerWallet(_walletAddr); - wallet.depositETH{value: _value}(_walletId); - - return true; - } - - /** - * @notice Increase the amount of ETH that an owner allowed to a spender. - * @param _spender The address which will spend the funds. - * @param _addedValue The amount of ETH to increase the allowance by. - */ - function increaseAllowance(address _spender, uint256 _addedValue) public returns (bool) { - require(_spender != address(0), "Spender address is 0"); - - allowed[msg.sender][_spender] = allowed[msg.sender][_spender] + _addedValue; - emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); - return true; - } - - /** - * @notice Decrease the amount of ETH that an owner allowed to a spender. - * @param _spender The address which will spend the funds. - * @param _subtractedValue The amount of ETH to decrease the allowance by. - */ - function decreaseAllowance(address _spender, uint256 _subtractedValue) public returns (bool) { - require(_spender != address(0), "Spender address is 0"); - - allowed[msg.sender][_spender] = allowed[msg.sender][_spender] - _subtractedValue; - emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); - return true; - } - - /** - * @notice Gets the balance of the specified address. - * @param _owner The address to query the balance of. - * @return An uint representing the amount owned by the passed address. - */ - function balanceOf(address _owner) public view returns (uint256) { - return balances[_owner]; - } - - /** - * @notice Function to check the amount of ETH that an owner allowed to a spender. - * @param _owner address The address which owns the funds. - * @param _spender address The address which will spend the funds. - * @return A uint specifying the amount of ETH still available for the spender. - */ - function allowance(address _owner, address _spender) public view returns (uint256) { - return allowed[_owner][_spender]; - } - - /** - * @notice Transfer ETH for a specified addresses - * @param _from The address to transfer from. - * @param _to The address to transfer to. - * @param _value The amount to be transferred. - */ - function _transfer(address _from, address payable _to, uint256 _value) internal { - require(_to != address(0), "To address is 0"); - - balances[_from] = balances[_from] - _value; - emit Transfer(_from, _to, _value); - (bool success,) = _to.call{value: _value}(""); - require(success, "ETH transfer failed"); - } -} diff --git a/src/helper/NativeWrapMock.sol b/src/helper/NativeWrapMock.sol new file mode 100644 index 0000000..8346c0c --- /dev/null +++ b/src/helper/NativeWrapMock.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "../lib/interface/INativeWrap.sol"; + +/** + * @title NativeWrapMock + * @notice **Test-only.** Minimal wrapped-native (WETH9-style ABI) + * reimplementation for Foundry tests. Covers `deposit` / `withdraw` / + * standard ERC-20. Production deploys reference each chain's canonical + * wrapped-native address (e.g., WETH) via `_nativeWrap` in `CelerLedger`'s + * constructor. **Do not deploy to a production network.** + */ +contract NativeWrapMock is INativeWrap { + string public constant name = "Wrapped Native (test mock)"; + string public constant symbol = "WMOCK"; + uint8 public constant decimals = 18; + + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + function deposit() external payable { + _balances[msg.sender] += msg.value; + emit Transfer(address(0), msg.sender, msg.value); + } + + function withdraw(uint256 _value) external { + _balances[msg.sender] -= _value; + emit Transfer(msg.sender, address(0), _value); + (bool ok,) = payable(msg.sender).call{value: _value}(""); + require(ok, "NativeWrapMock: withdraw send failed"); + } + + function totalSupply() external view returns (uint256) { + return address(this).balance; + } + + function balanceOf(address _owner) external view returns (uint256) { + return _balances[_owner]; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return _allowances[_owner][_spender]; + } + + function approve(address _spender, uint256 _value) external returns (bool) { + _allowances[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + function transfer(address _to, uint256 _value) external returns (bool) { + _balances[msg.sender] -= _value; + _balances[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; + } + + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + if (msg.sender != _from) { + _allowances[_from][msg.sender] -= _value; + } + _balances[_from] -= _value; + _balances[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } + + receive() external payable { + _balances[msg.sender] += msg.value; + emit Transfer(address(0), msg.sender, msg.value); + } +} diff --git a/src/lib/data/PbEntity.sol b/src/lib/data/PbEntity.sol index 160c6c9..bc43036 100644 --- a/src/lib/data/PbEntity.sol +++ b/src/lib/data/PbEntity.sol @@ -18,7 +18,7 @@ library PbEntity { enum TokenType { INVALID, - ETH, + NATIVE, ERC20 } diff --git a/src/lib/data/proto/entity.proto b/src/lib/data/proto/entity.proto index a6b88db..53a0da8 100644 --- a/src/lib/data/proto/entity.proto +++ b/src/lib/data/proto/entity.proto @@ -20,7 +20,7 @@ message AccountAmtPair { enum TokenType { INVALID = 0; - ETH = 1; + NATIVE = 1; ERC20 = 2; } diff --git a/src/lib/interface/ICelerLedger.sol b/src/lib/interface/ICelerLedger.sol index 6fea15a..e64562e 100644 --- a/src/lib/interface/ICelerLedger.sol +++ b/src/lib/interface/ICelerLedger.sol @@ -33,12 +33,13 @@ interface ICelerLedger { function openChannel(bytes calldata _openChannelRequest) external payable; /** - * @notice Deposit ETH or ERC-20 tokens into an existing channel. + * @notice Deposit native or ERC-20 tokens into an existing channel. * @dev Anyone can deposit; total credited is `msg.value + _transferFromAmount`. * For ERC-20 channels, `msg.value` must be 0 and the depositor must have approved - * this contract for `_transferFromAmount`. For ETH channels, `_transferFromAmount` - * is pulled from {IEthPool}. ERC-20 path assumes plain ERC-20 semantics — - * fee-on-transfer / rebasing / ERC-777 tokens are unsupported. + * this contract for `_transferFromAmount`. For native channels, `_transferFromAmount` + * is pulled from {INativeWrap} and unwrapped before crediting the wallet. + * ERC-20 path assumes plain ERC-20 semantics — fee-on-transfer / rebasing / + * ERC-777 tokens are unsupported. * @param _channelId Channel to credit. * @param _receiver Peer credited with the deposit. * @param _transferFromAmount Amount to pull via `transferFrom` (in addition to `msg.value`). @@ -47,6 +48,10 @@ interface ICelerLedger { /** * @notice Batched variant of {deposit} across multiple channels in one tx. + * @dev Not payable: native-channel entries are funded only via pre-approved + * wrapped-native (pulled from `nativeWrap` and unwrapped per channel); + * `msg.value` funding is unsupported in the batch path. ERC-20 entries + * pull from the corresponding token contract. * @param _channelIds Channels to credit. * @param _receivers Peer per channel credited with the deposit. * @param _transferFromAmounts Amount per channel pulled via `transferFrom`. @@ -130,8 +135,8 @@ interface ICelerLedger { /// @notice Number of channels currently in the given {LedgerStruct.ChannelStatus}. function getChannelStatusNum(uint256 _channelStatus) external view returns (uint256); - /// @notice Address of the configured {IEthPool}. - function getEthPool() external view returns (address); + /// @notice Address of the configured {INativeWrap}. + function getNativeWrap() external view returns (address); /// @notice Address of the configured {IPayRegistry}. function getPayRegistry() external view returns (address); @@ -203,10 +208,10 @@ interface ICelerLedger { /// @notice Unix timestamp (seconds) after which a settling channel can be confirmed. function getSettleFinalizedTime(bytes32 _channelId) external view returns (uint256); - /// @notice ERC-20 token contract address for this channel (`address(0)` for ETH). + /// @notice ERC-20 token contract address for this channel (`address(0)` for native). function getTokenContract(bytes32 _channelId) external view returns (address); - /// @notice Token type (ETH / ERC20) for this channel. + /// @notice Token type (NATIVE / ERC20) for this channel. function getTokenType(bytes32 _channelId) external view returns (PbEntity.TokenType); /// @notice Current channel status. @@ -292,7 +297,7 @@ interface ICelerLedger { /** * @notice Set the per-channel maximum deposit for one or more tokens. * @dev Owner-only. Limits are enforced by {deposit} / {openChannel} when enabled. - * @param _tokenAddrs Token addresses (`address(0)` for ETH). + * @param _tokenAddrs Token addresses (`address(0)` for native). * @param _limits New limits, indexed identically to `_tokenAddrs`. */ function setBalanceLimits(address[] calldata _tokenAddrs, uint256[] calldata _limits) external; @@ -303,7 +308,7 @@ interface ICelerLedger { /// @notice Re-enable balance-limit enforcement for all tokens (owner only). function enableBalanceLimits() external; - /// @notice Configured per-channel limit for a specific token (`address(0)` for ETH). + /// @notice Configured per-channel limit for a specific token (`address(0)` for native). function getBalanceLimit(address _tokenAddr) external view returns (uint256); /// @notice Whether balance-limit enforcement is currently enabled globally. diff --git a/src/lib/interface/ICelerWallet.sol b/src/lib/interface/ICelerWallet.sol index 354a317..d49dc57 100644 --- a/src/lib/interface/ICelerWallet.sol +++ b/src/lib/interface/ICelerWallet.sol @@ -23,10 +23,13 @@ interface ICelerWallet { function create(address[] calldata _owners, address _operator, bytes32 _nonce) external returns (bytes32); /** - * @notice Deposit `msg.value` ETH into a wallet's ETH balance. + * @notice Deposit `msg.value` native (e.g. ETH) into a wallet's native balance. + * @dev Public payable; called directly by users or by `CelerLedger` after + * unwrapping wrapped-native internally for the multi-party-funding path. + * Credits `balances[walletId][address(0)]`. * @param _walletId Wallet to deposit into. */ - function depositETH(bytes32 _walletId) external payable; + function depositNative(bytes32 _walletId) external payable; /** * @notice Deposit ERC-20 tokens into a wallet (caller must have approved this contract). @@ -38,11 +41,11 @@ interface ICelerWallet { /** * @notice Withdraw funds from a wallet to a receiver who is also an owner. - * @dev Caller must be the wallet's operator. ETH is sent via raw `call`; if the + * @dev Caller must be the wallet's operator. Native is sent via raw `call`; if the * ledger ever permits non-EOA peers, the ledger should layer a withdraw-pattern * on top to avoid griefing. * @param _walletId Wallet to debit. - * @param _tokenAddress Token to withdraw (`address(0)` for ETH). + * @param _tokenAddress Token to withdraw (`address(0)` for native). * @param _receiver Beneficiary; must be an owner of the wallet. * @param _amount Amount to withdraw. */ @@ -54,7 +57,7 @@ interface ICelerWallet { * list `_receiver` among their owners. * @param _fromWalletId Source wallet id. * @param _toWalletId Destination wallet id. - * @param _tokenAddress Token to move (`address(0)` for ETH). + * @param _tokenAddress Token to move (`address(0)` for native). * @param _receiver Beneficiary owner present in both wallets. * @param _amount Amount to transfer. */ @@ -88,7 +91,7 @@ interface ICelerWallet { /** * @notice Emergency token recovery (callable only when the contract is paused). - * @param _tokenAddress Token to drain (`address(0)` for ETH). + * @param _tokenAddress Token to drain (`address(0)` for native). * @param _receiver Recipient of drained funds. * @param _amount Amount to drain. */ @@ -100,7 +103,7 @@ interface ICelerWallet { /// @notice Operator of `_walletId`. function getOperator(bytes32 _walletId) external view returns (address); - /// @notice Token balance of `_walletId` for `_tokenAddress` (`address(0)` for ETH). + /// @notice Token balance of `_walletId` for `_tokenAddress` (`address(0)` for native). function getBalance(bytes32 _walletId, address _tokenAddress) external view returns (uint256); /// @notice Currently proposed new operator for `_walletId`, if any. diff --git a/src/lib/interface/IEthPool.sol b/src/lib/interface/IEthPool.sol deleted file mode 100644 index db2a890..0000000 --- a/src/lib/interface/IEthPool.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/** - * @title EthPool interface - * @notice ERC20-shaped wrapper for native ETH. Lets the rest of the AgentPay system — - * in particular {ICelerLedger.openChannel} and {ICelerLedger.deposit} — pull ETH via a - * uniform `transferFrom` flow regardless of the underlying token type. The pool - * exposes ERC20-style allowance semantics so a depositor can pre-approve the ledger - * to draw funds. - */ -interface IEthPool { - /** - * @notice Deposit `msg.value` ETH for `_receiver`. - * @param _receiver Account credited with the deposited ETH. - */ - function deposit(address _receiver) external payable; - - /** - * @notice Withdraw `_value` ETH from `msg.sender`'s pool balance back to its address. - * @param _value Amount of ETH to withdraw. - */ - function withdraw(uint256 _value) external; - - /** - * @notice Approve `_spender` to draw up to `_value` of `msg.sender`'s ETH balance. - * @param _spender Address authorized to spend. - * @param _value Maximum spendable amount. - * @return Always true (ERC20-style); reverts on invalid input. - */ - function approve(address _spender, uint256 _value) external returns (bool); - - /** - * @notice Transfer `_value` ETH from `_from`'s pool balance to `_to` (off the pool). - * @dev Decrements `allowance(_from, msg.sender)`; emits both `Approval` and - * `Transfer`. - * @param _from Source account inside the pool. - * @param _to Destination address (receives raw ETH). - * @param _value Amount of ETH to transfer. - * @return Always true on success. - */ - function transferFrom(address _from, address payable _to, uint256 _value) external returns (bool); - - /** - * @notice Transfer ETH from a pool account directly into a CelerWallet, in one call. - * @dev Decrements `allowance(_from, msg.sender)`. Used by {ICelerLedger.openChannel} - * and friends to fund a channel without the user having to first withdraw from - * the pool. - * @param _from Source account inside the pool. - * @param _walletAddr Target {ICelerWallet} address (must accept `depositETH`). - * @param _walletId Target wallet id within `_walletAddr`. - * @param _value Amount of ETH to forward. - * @return Always true on success. - */ - function transferToCelerWallet(address _from, address _walletAddr, bytes32 _walletId, uint256 _value) - external - returns (bool); - - /** - * @notice Increase `_spender`'s allowance from `msg.sender` by `_addedValue`. - * @param _spender Authorized spender. - * @param _addedValue Increment. - * @return Always true on success. - */ - function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool); - - /** - * @notice Decrease `_spender`'s allowance from `msg.sender` by `_subtractedValue`. - * @param _spender Authorized spender. - * @param _subtractedValue Decrement. - * @return Always true on success. - */ - function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool); - - /// @notice Pool balance of `_owner`. - function balanceOf(address _owner) external view returns (uint256); - - /// @notice Remaining allowance `_owner` has granted `_spender`. - function allowance(address _owner, address _spender) external view returns (uint256); - - /// @notice Emitted when ETH is deposited into the pool. - event Deposit(address indexed receiver, uint256 value); - - /// @notice Emitted when ETH leaves the pool (`from` is the pool balance debited; `to` receives raw ETH). - event Transfer(address indexed from, address indexed to, uint256 value); - - /// @notice Emitted on allowance changes (matches ERC20 semantics). - event Approval(address indexed owner, address indexed spender, uint256 value); -} diff --git a/src/lib/interface/INativeWrap.sol b/src/lib/interface/INativeWrap.sol new file mode 100644 index 0000000..1010c8b --- /dev/null +++ b/src/lib/interface/INativeWrap.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title INativeWrap + * @notice Minimal interface for the canonical wrapped-native token on the + * target chain (e.g., WETH on Ethereum). AgentPay uses this only internally + * as a funding-flow primitive — `LedgerOperation` pulls a peer's pre-approved + * wrapped-native via `transferFrom`, then unwraps via `withdraw` to forward + * the resulting native (e.g., ETH) to `CelerWallet`. Users never see + * wrapped-native through AgentPay's native-channel API. + */ +interface INativeWrap is IERC20 { + /// @notice Wrap `msg.value` of native into the same amount of wrapped-native credited to the caller. + function deposit() external payable; + + /// @notice Burn `_value` wrapped-native from the caller's balance and send native back via `.call{value:}`. + function withdraw(uint256 _value) external; +} diff --git a/src/lib/ledgerlib/LedgerBalanceLimit.sol b/src/lib/ledgerlib/LedgerBalanceLimit.sol index 6a72798..22bbf9a 100644 --- a/src/lib/ledgerlib/LedgerBalanceLimit.sol +++ b/src/lib/ledgerlib/LedgerBalanceLimit.sol @@ -13,7 +13,7 @@ library LedgerBalanceLimit { /** * @notice Set the per-channel balance limits of given tokens * @param _self storage data of CelerLedger contract - * @param _tokenAddrs addresses of the tokens (address(0) is for ETH) + * @param _tokenAddrs addresses of the tokens (address(0) is for native) * @param _limits balance limits of the tokens */ function setBalanceLimits( diff --git a/src/lib/ledgerlib/LedgerOperation.sol b/src/lib/ledgerlib/LedgerOperation.sol index 74d21cd..536583a 100644 --- a/src/lib/ledgerlib/LedgerOperation.sol +++ b/src/lib/ledgerlib/LedgerOperation.sol @@ -49,9 +49,8 @@ library LedgerOperation { // enforce ascending order of peers' addresses to simplify contract code require(peerAddrs[0] < peerAddrs[1], "Peer addrs are not ascending"); - ICelerWallet celerWallet = _self.celerWallet; bytes32 h = keccak256(openRequest.channelInitializer); - (bytes32 channelId, LedgerStruct.Channel storage c) = _createWallet(_self, celerWallet, peerAddrs, h); + (bytes32 channelId, LedgerStruct.Channel storage c) = _createWallet(_self, _self.celerWallet, peerAddrs, h); c.disputeTimeout = channelInitializer.disputeTimeout; _updateChannelStatus(_self, c, LedgerStruct.ChannelStatus.Operable); @@ -77,42 +76,63 @@ library LedgerOperation { require(amtSum <= _self.balanceLimits[token.tokenAddress], "Balance exceeds limit"); } - if (token.tokenType == PbEntity.TokenType.ETH) { - uint256 msgValueReceiver = channelInitializer.msgValueReceiver; - require(msg.value == amounts[msgValueReceiver], "msg.value mismatch"); - if (amounts[msgValueReceiver] > 0) { - celerWallet.depositETH{value: amounts[msgValueReceiver]}(channelId); - } + _fundChannelOpen(_self, channelId, peerAddrs, amounts, amtSum, token, channelInitializer.msgValueReceiver); + } - // peer ID of non-msgValueReceiver - uint256 pid = 1 - msgValueReceiver; - if (amounts[pid] > 0) { - _self.ethPool.transferToCelerWallet(peerAddrs[pid], address(celerWallet), channelId, amounts[pid]); + /** + * @notice Pull peer deposits into the channel's wallet at `openChannel` time. + * @dev Purpose: address the EVM 16-slot stack-depth limit. + * @param _self Storage data of CelerLedger contract. + * @param _channelId Id of the channel being opened. + * @param _peerAddrs Sorted peer addresses from the initializer. + * @param _amounts Per-peer initial deposits, indexed identically to `_peerAddrs`. + * @param _amtSum `_amounts[0] + _amounts[1]`, precomputed by the caller. + * @param _token Token type and address from the initializer. + * @param _msgValueReceiver Index (0 or 1) of the peer whose contribution is + * paid via `msg.value` on the native path; ignored on the ERC-20 path. + */ + function _fundChannelOpen( + LedgerStruct.Ledger storage _self, + bytes32 _channelId, + address[2] memory _peerAddrs, + uint256[2] memory _amounts, + uint256 _amtSum, + PbEntity.TokenInfo memory _token, + uint256 _msgValueReceiver + ) internal { + if (_token.tokenType == PbEntity.TokenType.NATIVE) { + require(msg.value == _amounts[_msgValueReceiver], "msg.value mismatch"); + uint256 pid = 1 - _msgValueReceiver; + if (_amounts[pid] > 0) { + IERC20(address(_self.nativeWrap)).safeTransferFrom(_peerAddrs[pid], address(this), _amounts[pid]); + _self.nativeWrap.withdraw(_amounts[pid]); } - } else if (token.tokenType == PbEntity.TokenType.ERC20) { + // `_amtSum > 0` is guaranteed by `openChannel`'s early return; + // a single combined depositNative covers both peers' contributions. + _self.celerWallet.depositNative{value: _amtSum}(_channelId); + } else if (_token.tokenType == PbEntity.TokenType.ERC20) { require(msg.value == 0, "msg.value is not 0"); - IERC20 erc20Token = IERC20(token.tokenAddress); + IERC20 erc20Token = IERC20(_token.tokenAddress); for (uint256 i = 0; i < 2; i++) { - if (amounts[i] == 0) continue; - - erc20Token.safeTransferFrom(peerAddrs[i], address(this), amounts[i]); + if (_amounts[i] == 0) continue; + erc20Token.safeTransferFrom(_peerAddrs[i], address(this), _amounts[i]); } - erc20Token.forceApprove(address(celerWallet), amtSum); - celerWallet.depositERC20(channelId, address(erc20Token), amtSum); + erc20Token.forceApprove(address(_self.celerWallet), _amtSum); + _self.celerWallet.depositERC20(_channelId, address(erc20Token), _amtSum); } else { assert(false); } } /** - * @notice Deposit ETH or ERC20 tokens into the channel + * @notice Deposit native or ERC20 tokens into the channel * @dev total deposit amount = msg.value(must be 0 for ERC20) + _transferFromAmount. * library function can't be payable but can read msg.value in caller's context. * @param _self storage data of CelerLedger contract * @param _channelId ID of the channel * @param _receiver address of the receiver - * @param _transferFromAmount amount of funds to be transfered from EthPool for ETH + * @param _transferFromAmount amount of funds to be transferred from `nativeWrap` (wrapped-native) for native channels * or ERC20 contract for ERC20 tokens */ function deposit( @@ -126,13 +146,14 @@ library LedgerOperation { _addDeposit(_self, _channelId, _receiver, _transferFromAmount + msgValue); LedgerStruct.Channel storage c = _self.channelMap[_channelId]; - if (c.token.tokenType == PbEntity.TokenType.ETH) { + if (c.token.tokenType == PbEntity.TokenType.NATIVE) { if (msgValue > 0) { - _self.celerWallet.depositETH{value: msgValue}(_channelId); + _self.celerWallet.depositNative{value: msgValue}(_channelId); } if (_transferFromAmount > 0) { - _self.ethPool - .transferToCelerWallet(msg.sender, address(_self.celerWallet), _channelId, _transferFromAmount); + IERC20(address(_self.nativeWrap)).safeTransferFrom(msg.sender, address(this), _transferFromAmount); + _self.nativeWrap.withdraw(_transferFromAmount); + _self.celerWallet.depositNative{value: _transferFromAmount}(_channelId); } } else if (c.token.tokenType == PbEntity.TokenType.ERC20) { require(msgValue == 0, "msg.value is not 0"); @@ -536,12 +557,12 @@ library LedgerOperation { } /** - * @notice Return EthPool used by this CelerLedger contract + * @notice Return wrapped-native (wrapped-native) contract used by this CelerLedger * @param _self storage data of CelerLedger contract - * @return EthPool address + * @return wrapped-native contract address */ - function getEthPool(LedgerStruct.Ledger storage _self) external view returns (address) { - return address(_self.ethPool); + function getNativeWrap(LedgerStruct.Ledger storage _self) external view returns (address) { + return address(_self.nativeWrap); } /** @@ -770,7 +791,7 @@ library LedgerOperation { * @return validated token info */ function _validateTokenInfo(PbEntity.TokenInfo memory _token) internal view returns (PbEntity.TokenInfo memory) { - if (_token.tokenType == PbEntity.TokenType.ETH) { + if (_token.tokenType == PbEntity.TokenType.NATIVE) { require(_token.tokenAddress == address(0)); } else if (_token.tokenType == PbEntity.TokenType.ERC20) { require(_token.tokenAddress != address(0)); diff --git a/src/lib/ledgerlib/LedgerStruct.sol b/src/lib/ledgerlib/LedgerStruct.sol index cb04d04..87d828c 100644 --- a/src/lib/ledgerlib/LedgerStruct.sol +++ b/src/lib/ledgerlib/LedgerStruct.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import "../interface/ICelerWallet.sol"; -import "../interface/IEthPool.sol"; +import "../interface/INativeWrap.sol"; import "../interface/IPayRegistry.sol"; import "../data/PbEntity.sol"; @@ -87,12 +87,12 @@ library LedgerStruct { * @notice Top-level ledger storage: many channels under one operation logic. * @dev Held in CelerLedger as a single private state variable. Each Ledger * binds to one CelerWallet (asset custody), one PayRegistry (resolved-pay - * results), and one EthPool (ETH wrapper). + * results), and one wrapped-native contract (used as funding-flow primitive only). */ struct Ledger { // ChannelStatus value => number of channels currently in that status. mapping(uint256 => uint256) channelStatusNums; - IEthPool ethPool; + INativeWrap nativeWrap; IPayRegistry payRegistry; ICelerWallet celerWallet; // Per-token per-channel deposit caps. diff --git a/test/CelerLedger.ETH.t.sol b/test/CelerLedger.ETH.t.sol index 5d95203..047507e 100644 --- a/test/CelerLedger.ETH.t.sol +++ b/test/CelerLedger.ETH.t.sol @@ -119,7 +119,7 @@ contract CelerLedgerEthTest is LedgerTestBase { // Initializer is bound to a sibling ledger (a fresh CelerLedger sharing // the same wallet+pool+registry). Replaying the same co-signed request // against the original `celerLedger` must revert. - address otherLedger = address(new CelerLedger(address(ethPool), address(payRegistry), address(celerWallet))); + address otherLedger = address(new CelerLedger(address(nativeWrap), address(payRegistry), address(celerWallet))); Fixtures.PaymentChannelInitializer memory init = Fixtures.PaymentChannelInitializer({ tokenType: 1, @@ -170,7 +170,7 @@ contract CelerLedgerEthTest is LedgerTestBase { // Direct cross-ledger replay: peers co-sign a *valid* initializer for // `celerLedger`; the same signed bytes are then submitted to a sibling // ledger sharing the same wallet+pool+registry. - CelerLedger siblingLedger = new CelerLedger(address(ethPool), address(payRegistry), address(celerWallet)); + CelerLedger siblingLedger = new CelerLedger(address(nativeWrap), address(payRegistry), address(celerWallet)); Fixtures.PaymentChannelInitializer memory init = Fixtures.PaymentChannelInitializer({ tokenType: 1, @@ -250,7 +250,7 @@ contract CelerLedgerEthTest is LedgerTestBase { assertEq(celerLedger.getTotalBalance(channelId), 50); } - function test_deposit_viaEthPool_succeeds() public { + function test_deposit_viaWrappedNative_succeeds() public { _setEthBalanceLimit(1_000_000); bytes32 channelId = _openZeroEthChannel(); @@ -260,6 +260,28 @@ contract CelerLedgerEthTest is LedgerTestBase { assertEq(celerLedger.getTotalBalance(channelId), 100); } + function test_deposit_viaWrappedNative_insufficientApproval_reverts() public { + _setEthBalanceLimit(1_000_000); + bytes32 channelId = _openZeroEthChannel(); + + // peer0 lowers their nativeWrap allowance below the requested deposit. + vm.prank(peer0); + nativeWrap.approve(address(celerLedger), 50); + + vm.expectRevert(); + vm.prank(peer0); + celerLedger.deposit(channelId, peer0, 100); + } + + function test_celerLedger_receive_revertsForNonNativeWrap() public { + // Restricted receive(): only `nativeWrap` can deliver native to the + // ledger (during its `withdraw(...)` callback). + vm.deal(stranger, 1 ether); + vm.prank(stranger); + (bool ok,) = address(celerLedger).call{value: 1}(""); + assertFalse(ok, "direct native send to ledger should revert"); + } + function test_deposit_byStranger_succeeds() public { _setEthBalanceLimit(1_000_000); bytes32 channelId = _openZeroEthChannel(); @@ -1105,7 +1127,7 @@ contract CelerLedgerEthTest is LedgerTestBase { function test_getTokenContract_andTokenType_forEthChannel() public { bytes32 channelId = _openZeroEthChannel(); assertEq(celerLedger.getTokenContract(channelId), address(0)); - // PbEntity.TokenType.ETH = 1 + // PbEntity.TokenType.NATIVE = 1 assertEq(uint256(celerLedger.getTokenType(channelId)), 1); } @@ -1171,8 +1193,8 @@ contract CelerLedgerEthTest is LedgerTestBase { // 11. Connection getters // ========================================================================= - function test_getEthPool_returnsConfiguredPool() public view { - assertEq(celerLedger.getEthPool(), address(ethPool)); + function test_getNativeWrap_returnsConfiguredPool() public view { + assertEq(celerLedger.getNativeWrap(), address(nativeWrap)); } function test_getPayRegistry_returnsConfiguredRegistry() public view { diff --git a/test/CelerLedger.Migrate.t.sol b/test/CelerLedger.Migrate.t.sol index 1908372..d839b48 100644 --- a/test/CelerLedger.Migrate.t.sol +++ b/test/CelerLedger.Migrate.t.sol @@ -21,18 +21,18 @@ contract CelerLedgerMigrateTest is LedgerTestBase { function setUp() public override { super.setUp(); - celerLedgerNew = new CelerLedger(address(ethPool), address(payRegistry), address(celerWallet)); + celerLedgerNew = new CelerLedger(address(nativeWrap), address(payRegistry), address(celerWallet)); vm.label(address(celerLedgerNew), "CelerLedgerNew"); // Disable balance limits on both for simplicity. celerLedger.disableBalanceLimits(); celerLedgerNew.disableBalanceLimits(); - // Approve the new ledger from the EthPool too. + // Approve the new ledger from the wrapped-native too. vm.prank(peer0); - ethPool.approve(address(celerLedgerNew), type(uint256).max); + nativeWrap.approve(address(celerLedgerNew), type(uint256).max); vm.prank(peer1); - ethPool.approve(address(celerLedgerNew), type(uint256).max); + nativeWrap.approve(address(celerLedgerNew), type(uint256).max); // Fund both peers with ERC20 + approve both ledgers. erc20.transfer(peer0, 1_000_000); diff --git a/test/CelerWallet.t.sol b/test/CelerWallet.t.sol index 5c925b0..7ce1226 100644 --- a/test/CelerWallet.t.sol +++ b/test/CelerWallet.t.sol @@ -69,7 +69,7 @@ contract CelerWalletTest is Test { vm.deal(operator, 10 ether); vm.prank(operator); - wallet.depositETH{value: 100}(walletId); + wallet.depositNative{value: 100}(walletId); token.transfer(owner0, 100_000); vm.prank(owner0); @@ -130,12 +130,12 @@ contract CelerWalletTest is Test { // Deposits // ========================================================================= - function test_depositETH_emitsEvent_creditsBalance() public { + function test_depositNative_emitsEvent_creditsBalance() public { vm.deal(stranger, 1 ether); vm.expectEmit(true, true, false, true, address(wallet)); emit DepositToWallet(walletId, address(0), 25); vm.prank(stranger); - wallet.depositETH{value: 25}(walletId); + wallet.depositNative{value: 25}(walletId); assertEq(wallet.getBalance(walletId, address(0)), 100 + 25); } @@ -306,7 +306,7 @@ contract CelerWalletTest is Test { // Deposits work again after unpause. vm.deal(stranger, 1 ether); vm.prank(stranger); - wallet.depositETH{value: 5}(walletId); + wallet.depositNative{value: 5}(walletId); assertEq(wallet.getBalance(walletId, address(0)), 100 + 5); } @@ -329,7 +329,7 @@ contract CelerWalletTest is Test { vm.deal(operator, 1 ether); vm.expectRevert(); vm.prank(operator); - wallet.depositETH{value: 100}(walletId); + wallet.depositNative{value: 100}(walletId); vm.expectRevert(); vm.prank(owner0); diff --git a/test/EthPool.t.sol b/test/EthPool.t.sol deleted file mode 100644 index 7c5ada0..0000000 --- a/test/EthPool.t.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {EthPool} from "../src/EthPool.sol"; - -/** - * @title EthPool tests - * @notice Unit tests for {EthPool}. Verifies the ERC20-shaped wrapper for - * native ETH: deposit, withdraw, allowance management, and `transferFrom`. - */ -contract EthPoolTest is Test { - EthPool internal pool; - - address payable internal alice; - address payable internal bob; - address payable internal recipient = payable(address(0x123456789)); - - event Deposit(address indexed receiver, uint256 value); - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - function setUp() public { - pool = new EthPool(); - alice = payable(makeAddr("alice")); - bob = payable(makeAddr("bob")); - - // Fund alice and bob with ETH so they can interact with the pool. - vm.deal(alice, 10 ether); - vm.deal(bob, 10 ether); - } - - // ------------------------------------------------------------------------- - // deposit - // ------------------------------------------------------------------------- - - function test_deposit_creditsReceiver_emitsEvent() public { - vm.expectEmit(true, false, false, true, address(pool)); - emit Deposit(bob, 100); - - vm.prank(alice); - pool.deposit{value: 100}(bob); - - assertEq(pool.balanceOf(bob), 100); - } - - function test_deposit_revertsForZeroReceiver() public { - vm.expectRevert(bytes("Receiver address is 0")); - pool.deposit{value: 1}(address(0)); - } - - // ------------------------------------------------------------------------- - // withdraw - // ------------------------------------------------------------------------- - - function test_withdraw_failsWithoutDeposit() public { - // Solidity 0.8 will revert with arithmetic underflow when subtracting from 0. - vm.expectRevert(); - vm.prank(alice); - pool.withdraw(100); - } - - function test_withdraw_succeedsAfterDeposit_emitsTransfer() public { - // Deposit 100 to alice's balance. - vm.prank(alice); - pool.deposit{value: 100}(alice); - - uint256 aliceBalanceBefore = alice.balance; - - vm.expectEmit(true, true, false, true, address(pool)); - emit Transfer(alice, alice, 100); - - vm.prank(alice); - pool.withdraw(100); - - assertEq(pool.balanceOf(alice), 0); - assertEq(alice.balance, aliceBalanceBefore + 100); - } - - // ------------------------------------------------------------------------- - // approve / allowance - // ------------------------------------------------------------------------- - - function test_approve_setsAllowance_emitsEvent() public { - vm.expectEmit(true, true, false, true, address(pool)); - emit Approval(alice, bob, 200); - - vm.prank(alice); - bool ok = pool.approve(bob, 200); - - assertTrue(ok); - assertEq(pool.allowance(alice, bob), 200); - } - - function test_approve_revertsForZeroSpender() public { - vm.expectRevert(bytes("Spender address is 0")); - vm.prank(alice); - pool.approve(address(0), 100); - } - - // ------------------------------------------------------------------------- - // transferFrom - // ------------------------------------------------------------------------- - - function test_transferFrom_movesEthAndDecrementsAllowance() public { - // alice deposits 200 and approves bob for 200. - vm.prank(alice); - pool.deposit{value: 200}(alice); - vm.prank(alice); - pool.approve(bob, 200); - - uint256 recipientBefore = recipient.balance; - - // bob transfers 150 from alice's pool balance to recipient. - vm.expectEmit(true, true, false, true, address(pool)); - emit Approval(alice, bob, 50); // remaining allowance after decrement - vm.expectEmit(true, true, false, true, address(pool)); - emit Transfer(alice, recipient, 150); - - vm.prank(bob); - bool ok = pool.transferFrom(alice, recipient, 150); - assertTrue(ok); - - assertEq(pool.balanceOf(alice), 50); - assertEq(pool.allowance(alice, bob), 50); - assertEq(recipient.balance, recipientBefore + 150); - } - - function test_transferFrom_revertsWhenAllowanceTooSmall() public { - // alice deposits 200, approves bob for only 50. - vm.prank(alice); - pool.deposit{value: 200}(alice); - vm.prank(alice); - pool.approve(bob, 50); - - vm.expectRevert(); - vm.prank(bob); - pool.transferFrom(alice, recipient, 100); - } - - // ------------------------------------------------------------------------- - // increaseAllowance / decreaseAllowance - // ------------------------------------------------------------------------- - - function test_increaseAllowance_addsToCurrent() public { - vm.prank(alice); - pool.approve(bob, 50); - - vm.expectEmit(true, true, false, true, address(pool)); - emit Approval(alice, bob, 100); - - vm.prank(alice); - bool ok = pool.increaseAllowance(bob, 50); - - assertTrue(ok); - assertEq(pool.allowance(alice, bob), 100); - } - - function test_decreaseAllowance_subtractsFromCurrent() public { - vm.prank(alice); - pool.approve(bob, 100); - - vm.expectEmit(true, true, false, true, address(pool)); - emit Approval(alice, bob, 20); - - vm.prank(alice); - bool ok = pool.decreaseAllowance(bob, 80); - - assertTrue(ok); - assertEq(pool.allowance(alice, bob), 20); - } - - // ------------------------------------------------------------------------- - // Round-trip fuzz - // ------------------------------------------------------------------------- - - function testFuzz_depositWithdraw_roundTripPreservesBalance(uint96 _amount) public { - vm.assume(_amount > 0); - vm.deal(alice, uint256(_amount)); - - uint256 aliceEthBefore = alice.balance; - - vm.prank(alice); - pool.deposit{value: _amount}(alice); - assertEq(pool.balanceOf(alice), _amount); - - vm.prank(alice); - pool.withdraw(_amount); - assertEq(pool.balanceOf(alice), 0); - assertEq(alice.balance, aliceEthBefore); - } -} diff --git a/test/GasReport.t.sol b/test/GasReport.t.sol index c824fde..8f5a458 100644 --- a/test/GasReport.t.sol +++ b/test/GasReport.t.sol @@ -7,7 +7,7 @@ import {Fixtures} from "./utils/Fixtures.sol"; import {SignUtil} from "./utils/SignUtil.sol"; import {CelerLedger} from "../src/CelerLedger.sol"; import {CelerWallet} from "../src/CelerWallet.sol"; -import {EthPool} from "../src/EthPool.sol"; +import {NativeWrapMock} from "../src/helper/NativeWrapMock.sol"; import {PayRegistry} from "../src/PayRegistry.sol"; import {PayResolver} from "../src/PayResolver.sol"; import {VirtContractResolver} from "../src/VirtContractResolver.sol"; @@ -25,7 +25,6 @@ import {BooleanCondMock} from "../src/helper/BooleanCondMock.sol"; * - `test/gas_logs/CelerLedger-ETH.txt` * - `test/gas_logs/CelerLedger-ERC20.txt` * - `test/gas_logs/CelerLedger-Migrate.txt` - * - `test/gas_logs/EthPool.txt` * - `test/gas_logs/PayResolver.txt` * - `test/gas_logs/VirtContractResolver.txt` * @@ -62,7 +61,7 @@ contract GasReport is LedgerTestBase { string memory s = "********** Gas Measurement: CelerLedger ETH **********\n\n"; s = string.concat(s, "***** Deploy Gas Used *****\n"); s = string.concat(s, _deployRow("VirtContractResolver", _measureDeploy_virt())); - s = string.concat(s, _deployRow("EthPool", _measureDeploy_ethPool())); + s = string.concat(s, _deployRow("NativeWrapMock", _measureDeploy_nativeWrap())); s = string.concat(s, _deployRow("PayRegistry", _measureDeploy_payRegistry())); s = string.concat(s, _deployRow("CelerWallet", _measureDeploy_celerWallet())); s = string.concat(s, _deployRow("PayResolver", _measureDeploy_payResolver())); @@ -70,12 +69,12 @@ contract GasReport is LedgerTestBase { s = string.concat(s, "\n***** Function Calls Gas Used *****\n"); s = string.concat(s, _row("openChannel() with zero deposit", _measure_openChannel_zeroDeposit())); - s = string.concat(s, _row("openChannel() using EthPool and msg.value", _measure_openChannel_funded())); + s = string.concat(s, _row("openChannel() using nativeWrap and msg.value", _measure_openChannel_funded())); s = string.concat(s, _row("setBalanceLimits()", _measure_setBalanceLimits())); s = string.concat(s, _row("disableBalanceLimits()", _measure_disableBalanceLimits())); s = string.concat(s, _row("enableBalanceLimits()", _measure_enableBalanceLimits())); s = string.concat(s, _row("deposit() via msg.value", _measure_deposit_msgValue())); - s = string.concat(s, _row("deposit() via EthPool", _measure_deposit_ethPool())); + s = string.concat(s, _row("deposit() via nativeWrap", _measure_deposit_nativeWrap())); s = string.concat(s, _row("depositInBatch() with 5 deposits", _measure_depositInBatch_5())); s = string.concat(s, _row("intendWithdraw()", _measure_intendWithdraw())); s = string.concat(s, _row("vetoWithdraw()", _measure_vetoWithdraw())); @@ -122,7 +121,7 @@ contract GasReport is LedgerTestBase { } // ========================================================================= - // Top-level report — PayResolver / VirtContractResolver / EthPool + // Top-level report — PayResolver / VirtContractResolver // ========================================================================= function test_writeReport_PayResolver() public { @@ -140,18 +139,6 @@ contract GasReport is LedgerTestBase { _writeReport("test/gas_logs/VirtContractResolver.txt", s); } - function test_writeReport_EthPool() public { - string memory s = "********** Gas Measurement: EthPool **********\n\n"; - s = string.concat(s, "***** Function Calls Gas Used *****\n"); - s = string.concat(s, _row("deposit()", _measure_ethPool_deposit())); - s = string.concat(s, _row("withdraw()", _measure_ethPool_withdraw())); - s = string.concat(s, _row("approve()", _measure_ethPool_approve())); - s = string.concat(s, _row("transferFrom()", _measure_ethPool_transferFrom())); - s = string.concat(s, _row("increaseAllowance()", _measure_ethPool_increaseAllowance())); - s = string.concat(s, _row("decreaseAllowance()", _measure_ethPool_decreaseAllowance())); - _writeReport("test/gas_logs/EthPool.txt", s); - } - // ========================================================================= // Fine-granularity report — intendSettle one state with N pays // ========================================================================= @@ -265,10 +252,10 @@ contract GasReport is LedgerTestBase { vm.revertToState(snap); } - function _measureDeploy_ethPool() internal returns (uint256 g) { + function _measureDeploy_nativeWrap() internal returns (uint256 g) { uint256 snap = vm.snapshotState(); uint256 g0 = gasleft(); - new EthPool(); + new NativeWrapMock(); g = g0 - gasleft(); vm.revertToState(snap); } @@ -300,7 +287,7 @@ contract GasReport is LedgerTestBase { function _measureDeploy_celerLedger() internal returns (uint256 g) { uint256 snap = vm.snapshotState(); uint256 g0 = gasleft(); - new CelerLedger(address(ethPool), address(payRegistry), address(celerWallet)); + new CelerLedger(address(nativeWrap), address(payRegistry), address(celerWallet)); g = g0 - gasleft(); vm.revertToState(snap); } @@ -383,7 +370,7 @@ contract GasReport is LedgerTestBase { vm.revertToState(snap); } - function _measure_deposit_ethPool() internal returns (uint256 g) { + function _measure_deposit_nativeWrap() internal returns (uint256 g) { uint256 snap = vm.snapshotState(); bytes32 ch = _openZeroEthChannel(); vm.prank(peer0); @@ -747,73 +734,6 @@ contract GasReport is LedgerTestBase { vm.revertToState(snap); } - function _measure_ethPool_deposit() internal returns (uint256 g) { - uint256 snap = vm.snapshotState(); - vm.deal(stranger, 1 ether); - vm.prank(stranger); - uint256 g0 = gasleft(); - ethPool.deposit{value: 100}(peer0); - g = g0 - gasleft(); - vm.revertToState(snap); - } - - function _measure_ethPool_withdraw() internal returns (uint256 g) { - uint256 snap = vm.snapshotState(); - vm.deal(peer0, 1 ether); - vm.prank(peer0); - ethPool.deposit{value: 100}(peer0); - vm.prank(peer0); - uint256 g0 = gasleft(); - ethPool.withdraw(100); - g = g0 - gasleft(); - vm.revertToState(snap); - } - - function _measure_ethPool_approve() internal returns (uint256 g) { - uint256 snap = vm.snapshotState(); - vm.prank(peer0); - uint256 g0 = gasleft(); - ethPool.approve(stranger, 200); - g = g0 - gasleft(); - vm.revertToState(snap); - } - - function _measure_ethPool_transferFrom() internal returns (uint256 g) { - uint256 snap = vm.snapshotState(); - vm.deal(peer0, 1 ether); - vm.prank(peer0); - ethPool.deposit{value: 200}(peer0); - vm.prank(peer0); - ethPool.approve(stranger, 200); - vm.prank(stranger); - uint256 g0 = gasleft(); - ethPool.transferFrom(peer0, payable(makeAddr("recipient")), 150); - g = g0 - gasleft(); - vm.revertToState(snap); - } - - function _measure_ethPool_increaseAllowance() internal returns (uint256 g) { - uint256 snap = vm.snapshotState(); - vm.prank(peer0); - ethPool.approve(stranger, 50); - vm.prank(peer0); - uint256 g0 = gasleft(); - ethPool.increaseAllowance(stranger, 50); - g = g0 - gasleft(); - vm.revertToState(snap); - } - - function _measure_ethPool_decreaseAllowance() internal returns (uint256 g) { - uint256 snap = vm.snapshotState(); - vm.prank(peer0); - ethPool.approve(stranger, 100); - vm.prank(peer0); - uint256 g0 = gasleft(); - ethPool.decreaseAllowance(stranger, 80); - g = g0 - gasleft(); - vm.revertToState(snap); - } - // ========================================================================= // Common reusable helpers (PayIdList / migration / hash-lock pay). // ========================================================================= @@ -865,12 +785,12 @@ contract GasReport is LedgerTestBase { } function _deployNewLedgerSiblingAndApprove() internal returns (CelerLedger newLedger) { - newLedger = new CelerLedger(address(ethPool), address(payRegistry), address(celerWallet)); + newLedger = new CelerLedger(address(nativeWrap), address(payRegistry), address(celerWallet)); newLedger.disableBalanceLimits(); vm.prank(peer0); - ethPool.approve(address(newLedger), type(uint256).max); + nativeWrap.approve(address(newLedger), type(uint256).max); vm.prank(peer1); - ethPool.approve(address(newLedger), type(uint256).max); + nativeWrap.approve(address(newLedger), type(uint256).max); } function _buildMigrationRequest(bytes32 _channelId, address _fromLedger, address _toLedger, uint256 _deadline) diff --git a/test/gas_logs/CelerLedger-ERC20.txt b/test/gas_logs/CelerLedger-ERC20.txt index 4894aaa..d11a489 100644 --- a/test/gas_logs/CelerLedger-ERC20.txt +++ b/test/gas_logs/CelerLedger-ERC20.txt @@ -1,8 +1,8 @@ ********** Gas Measurement: CelerLedger ERC20 ********** ***** Function Calls Gas Used ***** -openChannel() with zero deposit: 305422 -openChannel() with non-zero ERC20 deposits: 469132 -deposit(): 150001 -cooperativeWithdraw(): 89182 -cooperativeSettle(): 88893 +openChannel() with zero deposit: 305442 +openChannel() with non-zero ERC20 deposits: 469472 +deposit(): 149957 +cooperativeWithdraw(): 89094 +cooperativeSettle(): 88937 diff --git a/test/gas_logs/CelerLedger-ETH.txt b/test/gas_logs/CelerLedger-ETH.txt index 4723512..20d90e5 100644 --- a/test/gas_logs/CelerLedger-ETH.txt +++ b/test/gas_logs/CelerLedger-ETH.txt @@ -2,28 +2,28 @@ ***** Deploy Gas Used ***** VirtContractResolver Deploy Gas: 206156 -EthPool Deploy Gas: 558320 -PayRegistry Deploy Gas: 565565 -CelerWallet Deploy Gas: 1148513 +NativeWrapMock Deploy Gas: 416592 +PayRegistry Deploy Gas: 565639 +CelerWallet Deploy Gas: 1149117 PayResolver Deploy Gas: 1910790 -CelerLedger Deploy Gas: 1920270 +CelerLedger Deploy Gas: 1939849 ***** Function Calls Gas Used ***** -openChannel() with zero deposit: 301454 -openChannel() using EthPool and msg.value: 412371 -setBalanceLimits(): 26101 +openChannel() with zero deposit: 301469 +openChannel() using nativeWrap and msg.value: 433020 +setBalanceLimits(): 26123 disableBalanceLimits(): 1688 -enableBalanceLimits(): 32589 -deposit() via msg.value: 78052 -deposit() via EthPool: 91281 -depositInBatch() with 5 deposits: 382082 -intendWithdraw(): 72739 +enableBalanceLimits(): 32611 +deposit() via msg.value: 77942 +deposit() via nativeWrap: 121744 +depositInBatch() with 5 deposits: 526397 +intendWithdraw(): 72761 vetoWithdraw(): 3995 -confirmWithdraw(): 57548 -cooperativeWithdraw(): 92387 +confirmWithdraw(): 57592 +cooperativeWithdraw(): 92298 snapshotStates() with one non-null simplex state: 77674 intendSettle() with a null state: 87600 -intendSettle() with two 2-payment-hashList states: 279887 +intendSettle() with two 2-payment-hashList states: 279888 clearPays() with 2 payments: 16990 -confirmSettle(): 48721 -cooperativeSettle(): 93083 +confirmSettle(): 48787 +cooperativeSettle(): 93127 diff --git a/test/gas_logs/CelerLedger-Migrate.txt b/test/gas_logs/CelerLedger-Migrate.txt index 22d3f95..0fb23a4 100644 --- a/test/gas_logs/CelerLedger-Migrate.txt +++ b/test/gas_logs/CelerLedger-Migrate.txt @@ -1,6 +1,6 @@ ********** Gas Measurement: CelerLedger Migration ********** ***** Function Calls Gas Used ***** -migrateChannelFrom() an Operable ETH channel: 302074 -migrateChannelFrom() a Settling ETH channel: 322023 -migrateChannelFrom() an Operable ERC20 channel: 302074 +migrateChannelFrom() an Operable ETH channel: 302029 +migrateChannelFrom() a Settling ETH channel: 321978 +migrateChannelFrom() an Operable ERC20 channel: 302029 diff --git a/test/gas_logs/EthPool.txt b/test/gas_logs/EthPool.txt deleted file mode 100644 index d00bced..0000000 --- a/test/gas_logs/EthPool.txt +++ /dev/null @@ -1,9 +0,0 @@ -********** Gas Measurement: EthPool ********** - -***** Function Calls Gas Used ***** -deposit(): 20910 -withdraw(): 10060 -approve(): 31759 -transferFrom(): 41948 -increaseAllowance(): 3692 -decreaseAllowance(): 3647 diff --git a/test/gas_logs/fine_granularity/DepositEthInBatch.txt b/test/gas_logs/fine_granularity/DepositEthInBatch.txt index 5f1cb0f..341d987 100644 --- a/test/gas_logs/fine_granularity/DepositEthInBatch.txt +++ b/test/gas_logs/fine_granularity/DepositEthInBatch.txt @@ -1,9 +1,9 @@ ********** Gas Measurement of depositInBatch() - ETH ********** batch size used gas -1 92848 -5 382035 -10 743813 -25 1830668 -50 3648846 -75 5481260 +1 123311 +5 526350 +10 1030423 +25 2544290 +50 5074098 +75 7617986 diff --git a/test/utils/Base.t.sol b/test/utils/Base.t.sol index 2722a91..e83035e 100644 --- a/test/utils/Base.t.sol +++ b/test/utils/Base.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {CelerWallet} from "../../src/CelerWallet.sol"; import {CelerLedger} from "../../src/CelerLedger.sol"; -import {EthPool} from "../../src/EthPool.sol"; +import {NativeWrapMock} from "../../src/helper/NativeWrapMock.sol"; import {PayRegistry} from "../../src/PayRegistry.sol"; import {PayResolver} from "../../src/PayResolver.sol"; import {VirtContractResolver} from "../../src/VirtContractResolver.sol"; @@ -14,7 +14,7 @@ import {ERC20ExampleToken} from "../../src/helper/ERC20ExampleToken.sol"; * @title BaseTest * @notice Common deployment + helper utilities for AgentPay Foundry tests. Inherit * this from individual test contracts to get a fully wired contract graph - * (`celerWallet`, `celerLedger`, `ethPool`, `payRegistry`, `payResolver`, + * (`celerWallet`, `celerLedger`, `nativeWrap`, `payRegistry`, `payResolver`, * `virtResolver`, `erc20`) and a few addressing helpers. * @dev `setUp()` here can be called from a child's own `setUp()` via `super.setUp()`. * Tests that don't need every dependency may reach into the contracts directly @@ -26,7 +26,7 @@ contract BaseTest is Test { // ------------------------------------------------------------------------- CelerWallet internal celerWallet; CelerLedger internal celerLedger; - EthPool internal ethPool; + NativeWrapMock internal nativeWrap; PayRegistry internal payRegistry; PayResolver internal payResolver; VirtContractResolver internal virtResolver; @@ -60,17 +60,17 @@ contract BaseTest is Test { vm.warp(1_000_000); // Deploy the full contract graph in dependency order. - ethPool = new EthPool(); + nativeWrap = new NativeWrapMock(); payRegistry = new PayRegistry(); virtResolver = new VirtContractResolver(); payResolver = new PayResolver(address(payRegistry), address(virtResolver)); celerWallet = new CelerWallet(); - celerLedger = new CelerLedger(address(ethPool), address(payRegistry), address(celerWallet)); + celerLedger = new CelerLedger(address(nativeWrap), address(payRegistry), address(celerWallet)); erc20 = new ERC20ExampleToken(); vm.label(address(celerWallet), "CelerWallet"); vm.label(address(celerLedger), "CelerLedger"); - vm.label(address(ethPool), "EthPool"); + vm.label(address(nativeWrap), "NativeWrap"); vm.label(address(payRegistry), "PayRegistry"); vm.label(address(payResolver), "PayResolver"); vm.label(address(virtResolver), "VirtContractResolver"); diff --git a/test/utils/LedgerTestBase.t.sol b/test/utils/LedgerTestBase.t.sol index 0a2f8fd..78a3c71 100644 --- a/test/utils/LedgerTestBase.t.sol +++ b/test/utils/LedgerTestBase.t.sol @@ -22,19 +22,21 @@ contract LedgerTestBase is BaseTest { function setUp() public virtual override { super.setUp(); - // Pre-fund the peers in EthPool so deposits via the pool path work. + // Wrap native to the canonical wrapped-native (test mock) for each peer + // so they can fund their channel-open deposit via the pre-approved-WETH + // path (the non-msgValueReceiver side of multi-party funding). vm.deal(peer0, 100 ether); vm.deal(peer1, 100 ether); vm.prank(peer0); - ethPool.deposit{value: POOL_DEPOSIT}(peer0); + nativeWrap.deposit{value: POOL_DEPOSIT}(); vm.prank(peer1); - ethPool.deposit{value: POOL_DEPOSIT}(peer1); + nativeWrap.deposit{value: POOL_DEPOSIT}(); - // Approve the ledger to draw from the pool. + // Approve the ledger to draw from each peer's wrapped-native balance. vm.prank(peer0); - ethPool.approve(address(celerLedger), type(uint256).max); + nativeWrap.approve(address(celerLedger), type(uint256).max); vm.prank(peer1); - ethPool.approve(address(celerLedger), type(uint256).max); + nativeWrap.approve(address(celerLedger), type(uint256).max); } // ------------------------------------------------------------------------- @@ -61,7 +63,7 @@ contract LedgerTestBase is BaseTest { returns (bytes memory request, bytes memory initializer, bytes32 channelId) { Fixtures.PaymentChannelInitializer memory init = Fixtures.PaymentChannelInitializer({ - tokenType: 1, // ETH + tokenType: 1, // NATIVE tokenAddress: address(0), peers: [peer0, peer1], amounts: _amounts, @@ -114,7 +116,7 @@ contract LedgerTestBase is BaseTest { function _openFundedEthChannel(uint256[2] memory _amounts) internal returns (bytes32 channelId) { uint256 deadline = openDeadlineCursor++; (bytes memory request,, bytes32 derivedId) = _buildOpenEth(_amounts, 0, deadline); - // peer0 sends msg.value = amounts[0]; remainder pulled from peer1's EthPool balance. + // peer0 sends msg.value = amounts[0]; remainder pulled from peer1's wrapped-native balance. vm.prank(peer0); celerLedger.openChannel{value: _amounts[0]}(request); return derivedId;