Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ chains.

[Source](../src/CelerLedger.sol) · [Interface](../src/interfaces/ICelerLedger.sol) · **Versioned**

The channel state machine and primary user entry point. The contract itself is a thin
wrapper — the actual logic is split across five libraries under
[`src/lib/ledgerlib/`](../src/lib/ledgerlib/) and attached via `using ... for ...`. See
The channel state machine and primary user entry point. The contract is a thin
facade — the bulk of channel logic is split across three libraries under
[`src/lib/ledgerlib/`](../src/lib/ledgerlib/) and attached via `using ... for ...`,
with the type-only `LedgerStruct` namespace alongside them. See
[Ledger libraries](#ledger-libraries) below.

> **Supported tokens:** native (e.g. ETH) and plain ERC-20 only — see the same
Expand Down Expand Up @@ -330,38 +331,43 @@ refresh.

## Ledger libraries

`CelerLedger.sol` is intentionally thin. The actual channel logic lives in five
`CelerLedger.sol` is intentionally thin. The bulk of channel logic lives in three
libraries under [`src/lib/ledgerlib/`](../src/lib/ledgerlib/), attached to `CelerLedger`
via `using ... for ...`:
via `using ... for ...`. A fourth file, `LedgerStruct.sol`, holds shared types but
compiles to no bytecode (no functions).

| Library | Responsibility |
|---|---|
| [`LedgerStruct`](../src/lib/ledgerlib/LedgerStruct.sol) | All shared structs and the `ChannelStatus` enum. No logic. |
| [`LedgerStruct`](../src/lib/ledgerlib/LedgerStruct.sol) | All shared structs and the `ChannelStatus` enum. No logic; type-only namespace. |
| [`LedgerOperation`](../src/lib/ledgerlib/LedgerOperation.sol) | Open / deposit / withdraw / settle / snapshot — the bulk of the user-facing flows. |
| [`LedgerChannel`](../src/lib/ledgerlib/LedgerChannel.sol) | View functions and channel-state derivations (balance maps, peer state, withdraw intent, etc.). |
| [`LedgerChannel`](../src/lib/ledgerlib/LedgerChannel.sol) | Channel-scoped view functions and state derivations (balance maps, peer state, withdraw intent, etc.). Operates on `LedgerStruct.Channel`. |
| [`LedgerMigrate`](../src/lib/ledgerlib/LedgerMigrate.sol) | `migrateChannelFrom` / `migrateChannelTo` — peer-controlled version migration. |
| [`LedgerBalanceLimit`](../src/lib/ledgerlib/LedgerBalanceLimit.sol) | Per-token per-channel deposit caps (gate enabled by default). |

**When debugging or extending channel behavior, the implementation almost always lives
in one of these libraries — not in `CelerLedger.sol`.**

Balance-limit admin (`setBalanceLimits` / `disableBalanceLimits` / `enableBalanceLimits` /
`getBalanceLimit` / `getBalanceLimitsEnabled`) and ledger-wide config getters
(`getNativeWrap` / `getPayRegistry` / `getCelerWallet`) live **directly on
`CelerLedger`** rather than in a library. They're pure storage reads / writes — going
through a library would only add a DELEGATECALL hop with no logic benefit.

### Why split into libraries?

The split is **forced by the EIP-170 deployed-bytecode limit (24,576 bytes)**. The
hot-path library `LedgerOperation` alone is currently ~20.7 KB, leaving only ~3.8 KB
of headroom; merging the rest back into a single `CelerLedger` contract would total
~39.5 KB and fail to deploy.
hot-path library `LedgerOperation` alone is ~20.5 KB, leaving only ~4.0 KB of
headroom; merging the rest back into a single `CelerLedger` contract would total
~37.9 KB and fail to deploy.

| Component | Deployed size | % of 24,576 budget |
|---|---:|---:|
| `LedgerOperation` | ~20.7 KB | 84% |
| `CelerLedger` (facade only) | ~8.9 KB | 36% |
| `LedgerOperation` | ~20.5 KB | 84% |
| `CelerLedger` (facade + balance-limit admin + config getters) | ~8.4 KB | 34% |
| `LedgerMigrate` | ~5.2 KB | 21% |
| `LedgerChannel` | ~3.8 KB | 15% |
| `LedgerBalanceLimit` | ~0.9 KB | 4% |

The split-by-responsibility above also keeps cold paths (migration, balance limits)
out of the hot path's bytecode budget.
The split-by-responsibility above also keeps the cold migration path out of the hot
path's bytecode budget.

### How the libraries work mechanically

Expand Down
15 changes: 6 additions & 9 deletions script/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ existing core without touching the asset-custody contracts.
| Script | Deploys | Lifecycle |
|---|---|---|
| [`DeployCore.s.sol`](DeployCore.s.sol) | `PayRegistry`, `VirtContractResolver`, `CelerWallet` | Once per network. Permanent — never redeployed. |
| [`DeployLedger.s.sol`](DeployLedger.s.sol) | `CelerLedger` (+ 4 ledger libraries; see below) | Versioned. Run again for each new ledger version; peers cooperatively migrate. |
| [`DeployLedger.s.sol`](DeployLedger.s.sol) | `CelerLedger` (+ 3 ledger libraries; see below) | 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. |

## How the libraries are deployed

`CelerLedger` is split across five Solidity `library` contracts under
`CelerLedger` is split across three Solidity `library` contracts under
[`src/lib/ledgerlib/`](../src/lib/ledgerlib/) (forced by EIP-170's 24,576-byte
deployed-bytecode limit — see
[`docs/contracts.md` § Why split into libraries?](../docs/contracts.md#why-split-into-libraries)).
Expand All @@ -39,9 +39,8 @@ handles it implicitly:
3. The now-linked `CelerLedger` is deployed last.

So a single `forge script script/DeployLedger.s.sol --broadcast` call actually emits
**5 deployment transactions** in this order: `LedgerOperation`, `LedgerChannel`,
`LedgerMigrate`, `LedgerBalanceLimit`, then `CelerLedger`. All five appear in the
broadcast log under
**4 deployment transactions** in this order: `LedgerOperation`, `LedgerChannel`,
`LedgerMigrate`, then `CelerLedger`. All four appear in the broadcast log under
`broadcast/DeployLedger.s.sol/<chainId>/run-latest.json` (see
[Broadcast outputs](#broadcast-outputs) below).

Expand All @@ -62,13 +61,12 @@ forge verify-contract <CELER_LEDGER_ADDR> CelerLedger \
--libraries src/lib/ledgerlib/LedgerOperation.sol:LedgerOperation:<ADDR> \
--libraries src/lib/ledgerlib/LedgerChannel.sol:LedgerChannel:<ADDR> \
--libraries src/lib/ledgerlib/LedgerMigrate.sol:LedgerMigrate:<ADDR> \
--libraries src/lib/ledgerlib/LedgerBalanceLimit.sol:LedgerBalanceLimit:<ADDR> \
--watch
```

### Sharing libraries across ledger versions (optional)

By default each `DeployLedger` run **redeploys all four libraries** — fine for a
By default each `DeployLedger` run **redeploys all three libraries** — fine for a
clean version cut, wasteful if you're iterating. To pin already-deployed library
addresses and link `CelerLedger` against them at compile time, add to `foundry.toml`:

Expand All @@ -78,13 +76,12 @@ libraries = [
"src/lib/ledgerlib/LedgerOperation.sol:LedgerOperation:0x...",
"src/lib/ledgerlib/LedgerChannel.sol:LedgerChannel:0x...",
"src/lib/ledgerlib/LedgerMigrate.sol:LedgerMigrate:0x...",
"src/lib/ledgerlib/LedgerBalanceLimit.sol:LedgerBalanceLimit:0x...",
]
```

With those set, `forge build` resolves the link references at compile time and
`forge script` emits exactly **one** transaction (`new CelerLedger(...)`) instead of
five. Most production deploys won't bother — re-deploying ~30 KB of library bytecode
four. Most production deploys won't bother — re-deploying ~29 KB of library bytecode
costs a few hundred thousand gas, which is negligible against the audit / coordination
cost of pinning shared libraries across ledger versions.

Expand Down
29 changes: 16 additions & 13 deletions src/CelerLedger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.20;

import "./lib/ledgerlib/LedgerStruct.sol";
import "./lib/ledgerlib/LedgerOperation.sol";
import "./lib/ledgerlib/LedgerBalanceLimit.sol";
import "./lib/ledgerlib/LedgerMigrate.sol";
import "./lib/ledgerlib/LedgerChannel.sol";
import "./interfaces/ICelerWallet.sol";
Expand All @@ -15,15 +14,16 @@ import "@openzeppelin/contracts/access/Ownable.sol";
* @title CelerLedger
* @notice Channel state machine and primary user entry point for AgentPay. The
* contract itself is a thin wrapper — the bulk of channel logic lives in the
* libraries under `src/lib/ledgerlib/` (LedgerOperation, LedgerChannel, LedgerMigrate,
* LedgerBalanceLimit) attached via `using ... for ...`. CelerLedger acts as the
* libraries under `src/lib/ledgerlib/` (LedgerOperation, LedgerChannel, LedgerMigrate)
* attached via `using ... for ...`. Balance-limit admin and ledger-wide config
* getters live directly on this contract since they are pure storage reads /
* writes that don't justify a separate library hop. CelerLedger acts as the
* operator of a {ICelerWallet}; cooperative migration to a future ledger version
* is supported via {migrateChannelTo} / {migrateChannelFrom}.
* @dev See {ICelerLedger} for canonical NatSpec on each function.
*/
contract CelerLedger is ICelerLedger, Ownable {
using LedgerOperation for LedgerStruct.Ledger;
using LedgerBalanceLimit for LedgerStruct.Ledger;
using LedgerMigrate for LedgerStruct.Ledger;
using LedgerChannel for LedgerStruct.Channel;

Expand Down Expand Up @@ -68,21 +68,24 @@ contract CelerLedger is ICelerLedger, Ownable {
* @param _limits balance limits of the tokens
*/
function setBalanceLimits(address[] calldata _tokenAddrs, uint256[] calldata _limits) external onlyOwner {
ledger.setBalanceLimits(_tokenAddrs, _limits);
require(_tokenAddrs.length == _limits.length, "Lengths do not match");
for (uint256 i = 0; i < _tokenAddrs.length; i++) {
ledger.balanceLimits[_tokenAddrs[i]] = _limits[i];
}
}

/**
* @notice Disable balance limits of all tokens
*/
function disableBalanceLimits() external onlyOwner {
ledger.disableBalanceLimits();
ledger.balanceLimitsEnabled = false;
}

/**
* @notice Enable balance limits of all tokens
*/
function enableBalanceLimits() external onlyOwner {
ledger.enableBalanceLimits();
ledger.balanceLimitsEnabled = true;
}

/**
Expand Down Expand Up @@ -454,27 +457,27 @@ contract CelerLedger is ICelerLedger, Ownable {
}

/**
* @notice Return the wrapped-native (wrapped-native) contract used by this CelerLedger
* @notice Return the wrapped-native contract used by this CelerLedger
* @return wrapped-native contract address
*/
function getNativeWrap() external view returns (address) {
return ledger.getNativeWrap();
return address(ledger.nativeWrap);
}

/**
* @notice Return PayRegistry used by this CelerLedger contract
* @return PayRegistry address
*/
function getPayRegistry() external view returns (address) {
return ledger.getPayRegistry();
return address(ledger.payRegistry);
}

/**
* @notice Return CelerWallet used by this CelerLedger contract
* @return CelerWallet address
*/
function getCelerWallet() external view returns (address) {
return ledger.getCelerWallet();
return address(ledger.celerWallet);
}

/**
Expand All @@ -483,14 +486,14 @@ contract CelerLedger is ICelerLedger, Ownable {
* @return token balance limit
*/
function getBalanceLimit(address _tokenAddr) external view returns (uint256) {
return ledger.getBalanceLimit(_tokenAddr);
return ledger.balanceLimits[_tokenAddr];
}

/**
* @notice Return balanceLimitsEnabled
* @return balanceLimitsEnabled
*/
function getBalanceLimitsEnabled() external view returns (bool) {
return ledger.getBalanceLimitsEnabled();
return ledger.balanceLimitsEnabled;
}
}
1 change: 0 additions & 1 deletion src/CelerLedgerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.20;

import "./lib/ledgerlib/LedgerStruct.sol";
import "./lib/ledgerlib/LedgerOperation.sol";
import "./lib/ledgerlib/LedgerBalanceLimit.sol";
import "./lib/ledgerlib/LedgerMigrate.sol";
import "./lib/ledgerlib/LedgerChannel.sol";
import "./interfaces/ICelerWallet.sol";
Expand Down
10 changes: 6 additions & 4 deletions src/interfaces/ICelerLedger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import "../lib/ledgerlib/LedgerStruct.sol";
* acts as the operator of a {ICelerWallet} and exposes the on-chain APIs for opening,
* funding, withdrawing from, settling, and migrating payment channels.
* @dev The interface is grouped by the library that implements each section in
* `src/lib/ledgerlib/` (LedgerOperation, LedgerChannel, LedgerBalanceLimit,
* LedgerMigrate). Any change here must be mirrored in the corresponding library, and
* events declared here must match the library declarations bit-for-bit.
* `src/lib/ledgerlib/` (LedgerOperation, LedgerChannel, LedgerMigrate).
* Balance-limit admin and ledger-wide config getters are implemented directly on
* CelerLedger (no library hop). Any change here must be mirrored in the
* corresponding implementation, and events declared here must match the library
* declarations bit-for-bit.
*/
interface ICelerLedger {
// =========================================================================
Expand Down Expand Up @@ -291,7 +293,7 @@ interface ICelerLedger {
returns (address receiver, uint256 amount, uint256 requestTime, bytes32 recipientChannelId);

// =========================================================================
// LedgerBalanceLimit — per-channel deposit caps
// Balance limits — per-channel deposit caps (owner-only admin, on CelerLedger)
// =========================================================================

/**
Expand Down
64 changes: 0 additions & 64 deletions src/lib/ledgerlib/LedgerBalanceLimit.sol

This file was deleted.

27 changes: 0 additions & 27 deletions src/lib/ledgerlib/LedgerOperation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -556,33 +556,6 @@ library LedgerOperation {
return _self.channelStatusNums[_channelStatus];
}

/**
* @notice Return wrapped-native (wrapped-native) contract used by this CelerLedger
* @param _self storage data of CelerLedger contract
* @return wrapped-native contract address
*/
function getNativeWrap(LedgerStruct.Ledger storage _self) external view returns (address) {
return address(_self.nativeWrap);
}

/**
* @notice Return PayRegistry used by this CelerLedger contract
* @param _self storage data of CelerLedger contract
* @return PayRegistry address
*/
function getPayRegistry(LedgerStruct.Ledger storage _self) external view returns (address) {
return address(_self.payRegistry);
}

/**
* @notice Return CelerWallet used by this CelerLedger contract
* @param _self storage data of CelerLedger contract
* @return CelerWallet address
*/
function getCelerWallet(LedgerStruct.Ledger storage _self) external view returns (address) {
return address(_self.celerWallet);
}

/**
* @notice create a wallet for a new channel
* @param _self storage data of CelerLedger contract
Expand Down
10 changes: 5 additions & 5 deletions test/gas_logs/CelerLedger-ERC20.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
********** Gas Measurement: CelerLedger ERC20 **********

***** Function Calls Gas Used *****
openChannel() with zero deposit: 305442
openChannel() with non-zero ERC20 deposits: 469472
deposit(): 149957
cooperativeWithdraw(): 89094
cooperativeSettle(): 88937
openChannel() with zero deposit: 305397
openChannel() with non-zero ERC20 deposits: 469427
deposit(): 149979
cooperativeWithdraw(): 89116
cooperativeSettle(): 88959
Loading
Loading