ERC-7824
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
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
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
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
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:
bytes32 channelId = keccak256(
abi.encode(
channel.participants,
channel.adjudicator,
channel.challenge,
channel.nonce
)
);
State Hash
For signature verification, the state hash is computed as:
bytes32 stateHash = keccak256(
abi.encode(
channelId,
state.data,
state.version,
state.allocations
)
);
Note: The stateHash is bare signed without EIP-191 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:
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:
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:
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:
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
- Creation: Creator constructs channel config and signs initial state with
StateIntent.INITIALIZE
- Joining: Participants verify the channel and sign the same funding state
- Active: Once fully funded, the channel transitions to active state for off-chain operation
- Off-chain Updates: Participants exchange and sign state updates according to application logic
- 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
- Cooperative Close: All parties sign a final state with
Example Implementation: Remittance Adjudicator
The Remittance adjudicator validates payment transfers between participants:
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
// 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 and ERC-4337
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.