--- eip: 7824 title: Nitrolite State Channels description: Cross-chain stateful asset transfer author: State Channels (@statechannels), Layer-3 Foundation (@layer-3), Louis Bellet (@mod) discussions-to: https://ethereum-magicians.org/t/erc-7824-state-channels-framework/22566 status: Draft type: Standards Track category: ERC created: 2024-11-22 --- ## Abstract Nitrolite is a lightweight, efficient state channel framework for Ethereum and other EVM-compatible blockchains, enabling off-chain interactions while maintaining on-chain security guarantees. The framework allows participants to perform instant transactions with reduced gas costs while preserving the security of the underlying blockchain. This standard defines a framework for implementing state channel systems through the Nitrolite protocol, providing interfaces for channel creation, state management, dispute resolution, and fund custody. It enables high-throughput applications with minimal on-chain footprint. ## Motivation The Ethereum network faces challenges in scalability and transaction costs, making it less feasible for high-frequency interactions. The Nitrolite framework addresses these challenges by providing a lightweight state channel solution that enables: - **Instant Finality**: Transactions settle immediately between parties - **Reduced Gas Costs**: Most interactions happen off-chain, with minimal on-chain footprint - **High Throughput**: Support for thousands of transactions per second - **Security Guarantees**: Same security as on-chain, with cryptographic proofs - **Chain Agnostic**: Works with any EVM-compatible blockchain This ERC standardizes the Nitrolite protocol interfaces to facilitate widespread adoption of state channels. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. ### Glossary of Terms - **Channel**: A relationship between participants that allows off-chain state updates with on-chain settlement - **Channel ID**: A unique identifier derived from channel configuration (participants, adjudicator, challenge period, nonce) - **Participant**: An entity (EOA) involved in a state channel - **State**: A signed data structure containing version, allocations, and application data - **Allocation**: Specification of token distribution to destinations - **Adjudicator**: Contract that validates state transitions according to application rules - **Challenge Period**: Duration for dispute resolution before finalization - **Status**: Channel lifecycle stage (VOID, INITIAL, ACTIVE, DISPUTE, FINAL) - **State Intent**: Purpose of a state (OPERATE, INITIALIZE, RESIZE, FINALIZE) - **Custody**: On-chain contract holding locked funds for channels - **Checkpoint**: Recording a valid state on-chain without closing the channel ### Data Structures The Nitrolite protocol defines the following core data structures: #### Basic Types ```solidity struct Signature { uint8 v; bytes32 r; bytes32 s; } struct Amount { address token; // ERC-20 token address (address(0) for native tokens) uint256 amount; // Token amount } struct Allocation { address destination; // Where funds are sent on channel closure address token; // ERC-20 token contract address (address(0) for native tokens) uint256 amount; // Token amount allocated } ``` #### Channel Configuration ```solidity struct Channel { address[] participants; // List of participants in the channel address adjudicator; // Address of the contract that validates state transitions uint64 challenge; // Duration in seconds for dispute resolution period uint64 nonce; // Unique per channel with same participants and adjudicator } ``` #### State Structure ```solidity struct State { StateIntent intent; // Intent of the state uint256 version; // State version incremental number to compare most recent bytes data; // Application data encoded, decoded by the adjudicator Allocation[] allocations; // Asset allocation and destination for each participant Signature[] sigs; // stateHash signatures from participants } enum StateIntent { OPERATE, // Normal operation state INITIALIZE, // Initial funding state RESIZE, // Resize allocations state FINALIZE // Final closing state } ``` #### Channel Status ```solidity enum Status { VOID, // Channel was not created, State.version must be 0 INITIAL, // Channel is created and in funding process, State.version must be 0 ACTIVE, // Channel fully funded and operational, State.version > 0 DISPUTE, // Challenge period is active FINAL // Final state, channel can be closed } ``` #### Channel ID The channel ID is computed as: ```solidity bytes32 channelId = keccak256( abi.encode( channel.participants, channel.adjudicator, channel.challenge, channel.nonce ) ); ``` #### State Hash For signature verification, the state hash is computed as: ```solidity bytes32 stateHash = keccak256( abi.encode( channelId, state.data, state.version, state.allocations ) ); ``` Note: The state hash is bare signed without [EIP-191](./eip-191.md) since the protocol is intended to be chain-agnostic. ### Interfaces #### `IAdjudicator` Defines the interface for contracts that validate state transitions according to application-specific rules: ```solidity interface IAdjudicator { /** * @notice Validates a candidate state based on application-specific rules * @dev Used to determine if a state is valid during challenges or checkpoints * @param chan The channel configuration * @param candidate The proposed state to be validated * @param proofs Array of previous states that provide context for validation * @return valid True if the candidate state is valid according to application rules */ function adjudicate( Channel calldata chan, State calldata candidate, State[] calldata proofs ) external view returns (bool valid); } ``` #### `IComparable` Interface for determining the ordering between states: ```solidity interface IComparable { /** * @notice Compares two states to determine their relative ordering * @dev Returns: -1 if candidate < previous, 0 if equal, 1 if candidate > previous * @param candidate The state being evaluated * @param previous The reference state to compare against * @return result The comparison result */ function compare( State calldata candidate, State calldata previous ) external view returns (int8 result); } ``` #### `IChannel` The main state channel interface that manages the channel lifecycle: ```solidity interface IChannel { // Events event Created(bytes32 indexed channelId, Channel channel, State initial); event Joined(bytes32 indexed channelId, uint256 index); event Opened(bytes32 indexed channelId); event Challenged(bytes32 indexed channelId, uint256 expiration); event Checkpointed(bytes32 indexed channelId); event Resized(bytes32 indexed channelId, int256[] deltaAllocations); event Closed(bytes32 indexed channelId); /** * @notice Creates a new channel and initializes funding * @dev The creator must sign the funding state with StateIntent.INITIALIZE * @param ch Channel configuration * @param initial Initial state with StateIntent.INITIALIZE and expected allocations * @return channelId Unique identifier for the created channel */ function create(Channel calldata ch, State calldata initial) external returns (bytes32 channelId); /** * @notice Allows a participant to join a channel by signing the funding state * @dev Participant must provide signature on the same funding state * @param channelId Unique identifier for the channel * @param index Index of the participant in the channel's participants array * @param sig Signature of the participant on the funding state * @return channelId Unique identifier for the joined channel */ function join(bytes32 channelId, uint256 index, Signature calldata sig) external returns (bytes32); /** * @notice Finalizes a channel with a mutually signed closing state * @dev Requires all participants' signatures on a state with StateIntent.FINALIZE * @param channelId Unique identifier for the channel * @param candidate The latest known valid state to be finalized * @param proofs Additional states required by the adjudicator */ function close( bytes32 channelId, State calldata candidate, State[] calldata proofs ) external; /** * @notice Resizes channel allocations with participant agreement * @dev Used for adjusting channel allocations without withdrawing funds * @param channelId Unique identifier for the channel * @param candidate The state with new allocations * @param proofs Supporting states for validation */ function resize( bytes32 channelId, State calldata candidate, State[] calldata proofs ) external; /** * @notice Initiates or updates a challenge with a signed state * @dev Starts a challenge period during which participants can respond * @param channelId Unique identifier for the channel * @param candidate The state being submitted as the latest valid state * @param proofs Additional states required by the adjudicator */ function challenge( bytes32 channelId, State calldata candidate, State[] calldata proofs ) external; /** * @notice Records a valid state on-chain without initiating a challenge * @dev Used to establish on-chain proof of the latest state * @param channelId Unique identifier for the channel * @param candidate The state to checkpoint * @param proofs Additional states required by the adjudicator */ function checkpoint( bytes32 channelId, State calldata candidate, State[] calldata proofs ) external; } ``` #### `IDeposit` Interface for managing token deposits and withdrawals: ```solidity interface IDeposit { /** * @notice Deposits tokens into the contract * @dev For native tokens, the value should be sent with the transaction * @param token Token address (use address(0) for native tokens) * @param amount Amount of tokens to deposit */ function deposit(address token, uint256 amount) external payable; /** * @notice Withdraws tokens from the contract * @dev Can only withdraw available (not locked in channels) funds * @param token Token address (use address(0) for native tokens) * @param amount Amount of tokens to withdraw */ function withdraw(address token, uint256 amount) external; } ``` ### Channel Lifecycle 1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE` 2. **Joining**: Participants verify the channel and sign the same funding state 3. **Active**: Once fully funded, the channel transitions to active state for off-chain operation 4. **Off-chain Updates**: Participants exchange and sign state updates according to application logic 5. **Resolution**: - **Cooperative Close**: All parties sign a final state with `StateIntent.FINALIZE` - **Challenge-Response**: Participant can post a state on-chain and initiate challenge period - **Checkpoint**: Record valid state on-chain without closing for future dispute resolution - **Resize**: Adjust allocations by agreement without closing the channel ### Example Implementation: Remittance Adjudicator The Remittance adjudicator validates payment transfers between participants: ```solidity contract Remittance is IAdjudicator, IComparable { struct Intent { uint8 payer; // Index of the paying participant Amount transfer; // Amount and token being transferred } /** * @notice Validates a payment state transition * @dev Checks that the payer has signed and allocations are correct */ function adjudicate( Channel calldata chan, State calldata candidate, State[] calldata proofs ) external view returns (bool valid) { // Decode the payment intent Intent memory intent = abi.decode(candidate.data, (Intent)); // For first state (version 1), need funding state as proof if (candidate.version == 1) { require(proofs.length >= 1, "Missing funding state"); State memory funding = proofs[0]; require(funding.intent == StateIntent.INITIALIZE, "Invalid funding state"); } // For subsequent states, need previous state if (candidate.version > 1) { require(proofs.length >= 2, "Missing previous state"); State memory previous = proofs[1]; // Verify state transition require(candidate.version == previous.version + 1, "Invalid version"); // Verify allocations match the intent require( candidate.allocations[intent.payer].amount == previous.allocations[intent.payer].amount - intent.transfer.amount, "Invalid payer allocation" ); uint8 payee = intent.payer == 0 ? 1 : 0; require( candidate.allocations[payee].amount == previous.allocations[payee].amount + intent.transfer.amount, "Invalid payee allocation" ); } // Verify payer has signed require(candidate.sigs[intent.payer].v != 0, "Missing payer signature"); return true; } /** * @notice Compares states by version number */ function compare( State calldata candidate, State calldata previous ) external view returns (int8 result) { if (candidate.version < previous.version) return -1; if (candidate.version > previous.version) return 1; return 0; } } ``` ### Example Usage ```solidity // Create a channel between Alice and Bob Channel memory channel = Channel({ participants: [alice, bob], adjudicator: address(remittanceAdjudicator), challenge: 3600, // 1 hour challenge period nonce: 1 }); // Create initial funding state State memory fundingState = State({ intent: StateIntent.INITIALIZE, version: 0, data: "", allocations: [ Allocation(alice, tokenAddress, 100 ether), Allocation(bob, tokenAddress, 50 ether) ], sigs: [aliceSignature, bobSignature] }); // Alice creates and funds the channel bytes32 channelId = custody.create(channel, fundingState); // Bob joins with his signature custody.join(channelId, 1, bobSignature); // Off-chain: Alice pays Bob 10 tokens Intent memory paymentIntent = Intent({ payer: 0, // Alice is payer transfer: Amount(tokenAddress, 10 ether) }); State memory paymentState = State({ intent: StateIntent.OPERATE, version: 1, data: abi.encode(paymentIntent), allocations: [ Allocation(alice, tokenAddress, 90 ether), Allocation(bob, tokenAddress, 60 ether) ], sigs: [aliceSignature, bobSignature] }); // Either party can checkpoint this state on-chain custody.checkpoint(channelId, paymentState, [fundingState]); ``` ## Rationale The Nitrolite framework addresses critical blockchain scalability challenges through a lightweight state channel design. This standard provides clear interfaces to ensure interoperability while maintaining flexibility for diverse applications. **Efficiency**: Nitrolite minimizes on-chain footprint by requiring only essential operations (create, join, close) to occur on-chain, with all application logic executed off-chain. This enables instant finality and dramatically reduces gas costs. **Modularity**: The separation of concerns between the custody contract (IChannel), state validation (IAdjudicator), and fund management (IDeposit) allows developers to implement custom application logic while leveraging battle-tested infrastructure. **Flexibility**: The adjudicator pattern supports any application logic - from simple payments to complex multi-step protocols. The state intent system (INITIALIZE, OPERATE, RESIZE, FINALIZE) provides clear semantics for different operation types. **Security**: The challenge-response mechanism ensures that participants can always recover their funds by posting the latest valid state on-chain. The checkpoint feature allows proactive security by recording states without closing channels. **Chain Agnostic**: The protocol design avoids chain-specific features, enabling deployment across any EVM-compatible blockchain and facilitating cross-chain applications through the clearnode architecture. ## Backwards Compatibility No backward compatibility issues found. This ERC is designed to coexist with existing standards and can integrate with [ERC-1271](./erc-1271.md) and [ERC-4337](./erc-4337.md) ## Security Considerations ### On-Chain Security - **Signature Verification**: All state transitions require valid signatures from participants. The protocol uses bare signatures (without EIP-191) for chain agnosticism. - **Challenge Period**: The configurable challenge duration provides time for honest participants to respond to invalid states. - **Adjudicator Validation**: Custom adjudicators must be carefully audited as they control state transition rules. - **Reentrancy Protection**: Implementation should follow checks-effects-interactions pattern, especially in fund distribution. ### Off-Chain Security - **State Storage**: Participants must securely store all signed states as they may need them for disputes. - **Signature Security**: Private keys used for state signing must be protected as compromise allows unauthorized state transitions. - **Availability**: Participants must monitor the chain for challenges during the challenge period. - **Front-running**: Challenge transactions may be front-run; implementations should consider commit-reveal schemes if needed. ### Implementation Considerations - The custody contract strictly enforces 2-participant channels in the reference implementation. - Adjudicators should validate all state transitions according to application rules. - Proper nonce management prevents replay attacks across different channels. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).