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
26 changes: 26 additions & 0 deletions contracts/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,32 @@ Fee-on-transfer tokens are **not supported**. The amount received by the contrac

---

## Native ETH vs ERC20 Deposit Asymmetry

When pulling funds from a user, ERC20 and native ETH behave differently:

- **ERC20**: Funds are pulled via `transferFrom` using a prior user allowance. Any caller can submit a signed state that triggers a deposit — the funds come from the user's approval.

- **Native ETH**: The caller must attach the exact `msg.value`. Whoever submits the transaction must supply the ETH, regardless of who the logical depositor is.

### Affected operations

This asymmetry applies to every operation that pulls funds from the user:

| Function | Context |
|----------|---------|
| `createChannel` | Initial deposit on channel creation (`DEPOSIT` intent) |
| `depositToChannel` | Channel deposit |
| `initiateEscrowDeposit` | Escrow deposit initiation (non-home chain) |

### Practical consequence

For ERC20 channels, any party holding a valid signed state that requires a user deposit can submit it on-chain, and the user's pre-approved funds are pulled automatically. For native ETH channels, only a caller willing to supply the required `msg.value` can submit such a state. In practice, this means native ETH deposit states must be submitted by the user themselves (or by a party willing to front the ETH on their behalf).

Integrators building relayers or third-party submission flows should account for this difference: ERC20 state submissions are permissionless given prior user approval, while native ETH state submissions that require user funds are not.

---

## ERC20 Transfer Failure Attack Vectors

### Background
Expand Down
9 changes: 9 additions & 0 deletions contracts/src/ChannelHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ import {EcdsaSignatureUtils} from "./sigValidators/EcdsaSignatureUtils.sol";
* There is no hard-coded guardrail preventing deposit of these tokens — the contract will accept
* them, but any discrepancy will produce undefined accounting behavior for all users of that token.
* Enforcement is off-chain: the Node will not sign states that reference unsupported token types.
*
* NATIVE ETH vs ERC20 DEPOSIT ASYMMETRY:
*
* When user funds are pulled, ERC20 uses `transferFrom` (caller-agnostic, requires prior approval),
* while native ETH requires `msg.value == amount` (the transaction submitter must supply the ETH).
* For native ETH channels, deposit states must therefore be submitted by the user or a willing proxy.
*/
contract ChannelHub is ReentrancyGuard {
using EnumerableSet for EnumerableSet.Bytes32Set;
Expand Down Expand Up @@ -500,6 +506,7 @@ contract ChannelHub is ReentrancyGuard {
// Create channel with DEPOSIT, WITHDRAW, or OPERATE intent
// This enables users who already have off-chain virtual states with non-zero version
// to create a channel and perform initial operation simultaneously
// NOTE: For native ETH channels with DEPOSIT intent, msg.sender must supply msg.value == deposit amount.
function createChannel(ChannelDefinition calldata def, State calldata initState) external payable {
require(
initState.intent == StateIntent.DEPOSIT || initState.intent == StateIntent.WITHDRAW
Expand Down Expand Up @@ -537,6 +544,7 @@ contract ChannelHub is ReentrancyGuard {
emit ChannelCreated(channelId, user, def, initState);
}

// NOTE: For native ETH channels, msg.sender must supply msg.value == deposit amount.
function depositToChannel(bytes32 channelId, State calldata candidate) public payable {
require(candidate.intent == StateIntent.DEPOSIT, IncorrectStateIntent());

Expand Down Expand Up @@ -674,6 +682,7 @@ contract ChannelHub is ReentrancyGuard {

// ========= Cross-Chain Functions ==========

// NOTE: On non-home chain, user funds are pulled. For native ETH, msg.sender must supply msg.value == deposit amount.
function initiateEscrowDeposit(ChannelDefinition calldata def, State calldata candidate) external payable {
require(candidate.intent == StateIntent.INITIATE_ESCROW_DEPOSIT, IncorrectStateIntent());
_requireValidDefinition(def);
Expand Down
2 changes: 2 additions & 0 deletions protocol-description.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ Off-chain activity can continue indefinitely between enforcements.
* funds are pulled from User,
* locked into the channel.

**Native ETH vs ERC20 deposit mechanics:** For ERC20 tokens, the contract pulls funds from the user's address via `safeTransferFrom` using a prior allowance — any party can submit the signed state and the user's approved funds are transferred. For native ETH (`token = address(0)`), the **caller** must attach the required amount as `msg.value`. This means native ETH deposit states must be submitted by the user (or by a party willing to supply the ETH on their behalf). This asymmetry also applies to escrow deposit initiation and escrow withdrawal finalization on the non-home chain.

---

### 4. Withdrawal (single-chain)
Expand Down
Loading