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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ 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.

---

## Dependencies

| Package | Version | Purpose |
Expand Down
27 changes: 25 additions & 2 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ 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
> (fee-on-transfer, deflationary, rebasing, ERC-777 hooks) will desync this
> wallet's accounting from its real token balance. Use only standard ERC-20s.

### Constructor

```solidity
Expand Down Expand Up @@ -80,6 +85,17 @@ uint256 public walletNum;
mapping(bytes32 => Wallet) private wallets;
```

### Wallet ID derivation

```
walletId = keccak256(chainid, walletAddr, creatorAddr, nonce)
```

`creatorAddr` is `msg.sender` at the time `CelerWallet.create` is called — typically
the `CelerLedger` contract, not the end-user operator. The `chainid` prefix prevents
cross-chain wallet-id collisions when the same creator and nonce are reused across
chains.

---

## CelerLedger
Expand All @@ -91,6 +107,11 @@ 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
> (fee-on-transfer / rebasing / ERC-777 hooks) will desync channel accounting
> from the wallet's real token balance.

### Constructor

```solidity
Expand Down Expand Up @@ -151,7 +172,7 @@ Balance limits are **enabled by default** post-deployment. Configure them via

A wide set of getters: `getChannelStatus`, `getTokenContract`, `getTokenType`,
`getTotalBalance`, `getBalanceMap`, `getStateSeqNumMap`, `getTransferOutMap`,
`getNextPayIdListHashMap`, `getLastPayResolveDeadlineMap`, `getPendingPayOutMap`,
`getNextPayIdListHashMap`, `getPayClearDeadlineMap`, `getPendingPayOutMap`,
`getWithdrawIntent`, `getCooperativeWithdrawSeqNum`, `getSettleFinalizedTime`,
`getDisputeTimeout`, `getMigratedTo`, `getChannelMigrationArgs`,
`getPeersMigrationInfo`, `getChannelStatusNum`, `getEthPool`, `getPayRegistry`,
Expand Down Expand Up @@ -205,6 +226,8 @@ constructor(address _registryAddr, address _virtResolverAddr)

### Resolution rules

- A payment's `chain_id` must equal `block.chainid`.
- A payment's `pay_resolver` must equal the executing resolver's address.
- A payment must be resolved before `pay.resolveDeadline` (Unix timestamp, seconds).
- A result equal to the maximum-transfer amount **finalizes immediately** — no challenge
window.
Expand Down Expand Up @@ -245,7 +268,7 @@ No constructor (no state to initialize).
| [`setPayDeadline`](../src/PayRegistry.sol#L37) | Setter writes the resolve deadline under its own namespace. |
| [`setPayInfo`](../src/PayRegistry.sol#L45) | Combined `setPayAmount` + `setPayDeadline`. |
| [`setPayAmounts`](../src/PayRegistry.sol#L54) / [`setPayDeadlines`](../src/PayRegistry.sol#L68) / [`setPayInfos`](../src/PayRegistry.sol#L82) | Batched variants. |
| [`getPayAmounts`](../src/PayRegistry.sol#L107) | Bulk read for settlement (verifies each pay's deadline ≤ a per-channel `lastPayResolveDeadline`). |
| [`getPayAmounts`](../src/PayRegistry.sol#L107) | Bulk read for settlement; gates each pay by its own `resolveDeadline` if resolved, or the per-channel `pay_clear_deadline` (`max(pay.resolveDeadline) + clearMargin`) if never resolved. |
| [`getPayInfo`](../src/PayRegistry.sol#L126) | Single-pay read. |
| `payInfoMap` (auto-getter) | Public mapping accessor. |

Expand Down
12 changes: 4 additions & 8 deletions src/CelerLedger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -391,18 +391,14 @@ contract CelerLedger is ICelerLedger, Ownable {
}

/**
* @notice Return lastPayResolveDeadline map of a duplex channel
* @notice Return payClearDeadline map of a duplex channel
* @param _channelId ID of the channel to be viewed
* @return peers' addresses
* @return lastPayResolveDeadlines of two simplex channels
* @return payClearDeadlines of two simplex channels
*/
function getLastPayResolveDeadlineMap(bytes32 _channelId)
external
view
returns (address[2] memory, uint256[2] memory)
{
function getPayClearDeadlineMap(bytes32 _channelId) external view returns (address[2] memory, uint256[2] memory) {
LedgerStruct.Channel storage c = ledger.channelMap[_channelId];
return c.getLastPayResolveDeadlineMap();
return c.getPayClearDeadlineMap();
}

/**
Expand Down
16 changes: 6 additions & 10 deletions src/CelerLedgerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ contract CelerLedgerMock {
uint256 _seqNum,
uint256 _transferOut,
bytes32 _nextPayIdListHash,
uint256 _lastPayResolveDeadline,
uint256 _payClearDeadline,
uint256 _pendingPayOut
) external {
LedgerStruct.Channel storage c = ledger.channelMap[_channelId];
Expand All @@ -212,7 +212,7 @@ contract CelerLedgerMock {
state.seqNum = _seqNum;
state.transferOut = _transferOut;
state.nextPayIdListHash = _nextPayIdListHash;
state.lastPayResolveDeadline = _lastPayResolveDeadline;
state.payClearDeadline = _payClearDeadline;
state.pendingPayOut = _pendingPayOut;

_updateOverallStatesByIntendState(_channelId);
Expand Down Expand Up @@ -431,18 +431,14 @@ contract CelerLedgerMock {
}

/**
* @notice Return lastPayResolveDeadline map of a duplex channel
* @notice Return payClearDeadline map of a duplex channel
* @param _channelId ID of the channel to be viewed
* @return peers' addresses
* @return lastPayResolveDeadlines of two simplex channels
* @return payClearDeadlines of two simplex channels
*/
function getLastPayResolveDeadlineMap(bytes32 _channelId)
external
view
returns (address[2] memory, uint256[2] memory)
{
function getPayClearDeadlineMap(bytes32 _channelId) external view returns (address[2] memory, uint256[2] memory) {
LedgerStruct.Channel storage c = ledger.channelMap[_channelId];
return c.getLastPayResolveDeadlineMap();
return c.getPayClearDeadlineMap();
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/CelerWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ contract CelerWallet is ICelerWallet, Pausable, Ownable {

/**
* @notice Create a new wallet
* @dev `walletId = keccak256(chainid, walletAddr, creatorAddr, nonce)` where
* `creatorAddr` is `msg.sender` (the account that calls `create`, e.g. CelerLedger).
* @param _owners owners of the wallet
* @param _operator initial operator of the wallet
* @param _nonce nonce given by caller to generate the wallet id
Expand All @@ -73,7 +75,7 @@ contract CelerWallet is ICelerWallet, Pausable, Ownable {
{
require(_operator != address(0), "New operator is address(0)");

bytes32 walletId = keccak256(abi.encodePacked(address(this), msg.sender, _nonce));
bytes32 walletId = keccak256(abi.encodePacked(block.chainid, address(this), msg.sender, _nonce));
Wallet storage w = wallets[walletId];
// wallet must be uninitialized
require(w.operator == address(0), "Occupied wallet id");
Expand Down
8 changes: 4 additions & 4 deletions src/PayRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,18 @@ contract PayRegistry is IPayRegistry {
}

/// @inheritdoc IPayRegistry
function getPayAmounts(bytes32[] calldata _payIds, uint256 _lastPayResolveDeadline)
function getPayAmounts(bytes32[] calldata _payIds, uint256 _maxResolveDeadline)
external
view
returns (uint256[] memory)
{
uint256[] memory amounts = new uint256[](_payIds.length);
for (uint256 i = 0; i < _payIds.length; i++) {
if (payInfoMap[_payIds[i]].resolveDeadline == 0) {
// should pass last pay resolve deadline if never resolved
require(block.timestamp > _lastPayResolveDeadline, "Payment is not finalized");
// unresolved pays are gated by the caller-supplied upper-bound deadline
require(block.timestamp > _maxResolveDeadline, "Payment is not finalized");
} else {
// should pass resolve deadline if resolved
// resolved pays are gated by their per-pay resolve deadline
require(block.timestamp > payInfoMap[_payIds[i]].resolveDeadline, "Payment is not finalized");
}
amounts[i] = payInfoMap[_payIds[i]].amount;
Expand Down
3 changes: 3 additions & 0 deletions src/PayResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ contract PayResolver is IPayResolver {
* @param _amount payment amount to resolve
*/
function _resolvePayment(PbEntity.ConditionalPay memory _pay, bytes32 _payHash, uint256 _amount) internal {
// bind the signed pay to its intended (chain, resolver) target
require(_pay.chainId == block.chainid, "Wrong chain id for pay");
require(_pay.payResolver == address(this), "Wrong resolver for pay");
uint256 nowTs = block.timestamp;
require(nowTs <= _pay.resolveDeadline, "Passed pay resolve deadline in condPay msg");

Expand Down
16 changes: 14 additions & 2 deletions src/lib/data/PbEntity.sol
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ library PbEntity {
uint256 seqNum; // tag: 3
TokenTransfer transferToPeer; // tag: 4
PayIdList pendingPayIds; // tag: 5
uint256 lastPayResolveDeadline; // tag: 6
uint256 payClearDeadline; // tag: 6
uint256 totalPendingAmount; // tag: 7
} // end struct SimplexPaymentChannel

Expand All @@ -200,7 +200,7 @@ library PbEntity {
m.pendingPayIds = decPayIdList(buf.decBytes());
} else if (key == 48) {
// tag 6
m.lastPayResolveDeadline = buf.decVarint();
m.payClearDeadline = buf.decVarint();
} else if (key == 58) {
// tag 7
m.totalPendingAmount = buf.decUint256();
Expand Down Expand Up @@ -274,6 +274,7 @@ library PbEntity {
uint256 resolveDeadline; // tag: 6
uint256 resolveTimeout; // tag: 7
address payResolver; // tag: 8
uint256 chainId; // tag: 9
} // end struct ConditionalPay

function decConditionalPay(bytes memory raw) internal pure returns (ConditionalPay memory m) {
Expand Down Expand Up @@ -313,6 +314,9 @@ library PbEntity {
} else if (key == 66) {
// tag 8
m.payResolver = buf.decAddress();
} else if (key == 72) {
// tag 9
m.chainId = buf.decVarint();
} else {
buf.skipValue(Pb.WireType(key & 7)); // unknown tag or wrong wire
}
Expand Down Expand Up @@ -455,6 +459,8 @@ library PbEntity {
uint256 openDeadline; // tag: 2
uint256 disputeTimeout; // tag: 3
uint256 msgValueReceiver; // tag: 4
uint256 chainId; // tag: 5
address ledgerAddress; // tag: 6
} // end struct PaymentChannelInitializer

function decPaymentChannelInitializer(bytes memory raw) internal pure returns (PaymentChannelInitializer memory m) {
Expand All @@ -475,6 +481,12 @@ library PbEntity {
} else if (key == 32) {
// tag 4
m.msgValueReceiver = buf.decVarint();
} else if (key == 40) {
// tag 5
m.chainId = buf.decVarint();
} else if (key == 50) {
// tag 6
m.ledgerAddress = buf.decAddress();
} else {
buf.skipValue(Pb.WireType(key & 7)); // unknown tag or wrong wire
}
Expand Down
24 changes: 18 additions & 6 deletions src/lib/data/proto/entity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ message SimplexPaymentChannel {
TokenTransfer transfer_to_peer = 4;
// head of the idlist chain of all pending conditional pays.
PayIdList pending_pay_ids = 5;
// The last resolve deadline of all pending conditional pays.
// confirmSettle must be called after all pending pays have been finalized,
// namely all pending pays have been resolved in the pay registry,
// or after the last_pay_resolve_deadline.
uint64 last_pay_resolve_deadline = 6 [(soltype) = "uint"];
// Unix timestamp (seconds) after which confirmSettle becomes unconditionally
// eligible — i.e. max(pay.resolveDeadline) + clearMargin. The clearMargin
// must be large enough that, after all pays are resolved in PayRegistry,
// recipient peers have time to call clearPays for every pay-list segment
// before confirmSettle can close the channel. Off-chain producers MUST
// include the margin; a literal "last resolve deadline" would race with
// uncleared multi-segment pay lists.
uint64 pay_clear_deadline = 6 [(soltype) = "uint"];
// total amount of all pending pays.
bytes total_pending_amount = 7 [(soltype) = "uint256"];
}
Expand Down Expand Up @@ -121,6 +124,9 @@ message ConditionalPay {
// resolve_timeout is the dispute window after a resolve payment request is submitted
uint64 resolve_timeout = 7 [(soltype) = "uint"];
bytes pay_resolver = 8 [(soltype) = "address"];
// chain id of the intended target chain; binds the signed pay (and any
// vouched result over it) against cross-chain replay
uint64 chain_id = 9 [(soltype) = "uint"];
}

// Next Tag: 3
Expand Down Expand Up @@ -169,7 +175,7 @@ message CooperativeWithdrawInfo {
bytes recipient_channel_id = 5 [(soltype) = "bytes32"];
}

// Next Tag: 5
// Next Tag: 7
message PaymentChannelInitializer {
// require an ascending order based on addresses of init_distribution.distribution[].account
TokenDistribution init_distribution = 1;
Expand All @@ -178,6 +184,12 @@ message PaymentChannelInitializer {
// index of channel peer who receives the blockchain native token
// associated with the transaction (msg.value in case of ETH)
uint64 msg_value_receiver = 4 [(soltype) = "uint"];
// chain id of the intended target chain; binds the signed initializer
// against cross-chain replay
uint64 chain_id = 5 [(soltype) = "uint"];
// address of the intended target CelerLedger; binds the signed initializer
// against same-chain wrong-ledger replay
bytes ledger_address = 6 [(soltype) = "address"];
}

// Next Tag: 5
Expand Down
18 changes: 10 additions & 8 deletions src/lib/interface/ICelerLedger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ interface ICelerLedger {
/**
* @notice Open a fully-funded channel from a co-signed initializer in one tx.
* @dev Atomically creates a wallet in the underlying CelerWallet, derives
* `channelId = keccak256(walletAddr, ledgerAddr, keccak256(initializer))`, and
* pulls the initial deposits. Native value is allowed via `msg.value`.
* `channelId = keccak256(chainid, walletAddr, ledgerAddr, keccak256(initializer))`,
* and pulls the initial deposits. Native value is allowed via `msg.value`.
* The initializer's `chain_id` and `ledger_address` must match this
* contract's execution domain. ERC-20 path assumes plain ERC-20 semantics
* (`balanceDelta == requested`); fee-on-transfer / rebasing / ERC-777
* tokens are unsupported.
* @param _openChannelRequest ABI-encoded `PbChain.OpenChannelRequest` message.
*/
function openChannel(bytes calldata _openChannelRequest) external payable;
Expand All @@ -33,7 +37,8 @@ interface ICelerLedger {
* @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}.
* is pulled from {IEthPool}. 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`).
Expand Down Expand Up @@ -262,11 +267,8 @@ interface ICelerLedger {
/// @notice Per-peer next-list hashes for batched pay clearing during settlement.
function getNextPayIdListHashMap(bytes32 _channelId) external view returns (address[2] memory, bytes32[2] memory);

/// @notice Per-peer latest pay-resolve deadlines used during settlement.
function getLastPayResolveDeadlineMap(bytes32 _channelId)
external
view
returns (address[2] memory, uint256[2] memory);
/// @notice Per-peer pay-clear deadlines (the threshold past which `confirmSettle` is unconditionally eligible).
function getPayClearDeadlineMap(bytes32 _channelId) external view returns (address[2] memory, uint256[2] memory);

/// @notice Per-peer pending pay totals (locked amounts).
function getPendingPayOutMap(bytes32 _channelId) external view returns (address[2] memory, uint256[2] memory);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/interface/ICelerWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pragma solidity ^0.8.20;
interface ICelerWallet {
/**
* @notice Create a new wallet.
* @dev `walletId = keccak256(walletContract, msg.sender, _nonce)`. Reverts if the
* derived id is already in use or if `_operator == address(0)`.
* @dev `walletId = keccak256(chainid, walletContract, msg.sender, _nonce)`.
* Reverts if the derived id is already in use or if `_operator == address(0)`.
* @param _owners Owners of the wallet (typically the two channel peers).
* @param _operator Initial operator authorized to move funds.
* @param _nonce Caller-supplied nonce, used in the wallet-id derivation.
Expand Down
13 changes: 8 additions & 5 deletions src/lib/interface/IPayRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@ interface IPayRegistry {

/**
* @notice Bulk-read amounts for use during channel settlement.
* @dev Each pay's stored deadline must be less than or equal to
* `_lastPayResolveDeadline`; otherwise the call reverts. This guards against
* applying not-yet-finalized results during `intendSettle` / `confirmSettle`.
* @dev Each pay is either resolved (gated by its own `resolveDeadline`) or
* never resolved (gated by `_maxResolveDeadline`). Unresolved pays read
* back as 0 once `block.timestamp > _maxResolveDeadline`.
* @param _payIds List of pay ids.
* @param _lastPayResolveDeadline Per-channel cap on the per-pay resolve deadline.
* @param _maxResolveDeadline Upper bound on per-pay resolve deadlines for
* the batch — callers pass any value ≥ max(pay.resolveDeadline). Channels
* typically pass `pay_clear_deadline` (= max resolve deadline + clear
* margin); the margin is a channel-clearance concern, transparent here.
* @return Amounts indexed identically to `_payIds`.
*/
function getPayAmounts(bytes32[] calldata _payIds, uint256 _lastPayResolveDeadline)
function getPayAmounts(bytes32[] calldata _payIds, uint256 _maxResolveDeadline)
external
view
returns (uint256[] memory);
Expand Down
Loading
Loading