Rails enables efficient token transfers by allowing users to settle directly with each other.
Rails (Rolling Asynchronous Interchain Liquidity Settlement) is a novel bridge protocol that decouples cross-chain transfer settlement from underlying message settlement, dramatically improving capital efficiency.
1. Transfer Initiation: A user initiates a cross-chain transfer by calling send() on the source chain. The transfer includes a state attestation that references a previous transfer from the destination chain, creating a cryptographic link between the two directions of flow.
2. Claim Posting: A bonder observes the transfer on the source chain and posts a corresponding claim on the destination chain using postClaim(). The claim includes all transfer details and the state attestation, which is validated to ensure the referenced transfer actually exists.
3. AMM Calculation: When a claim is posted, the system calculates the output amount using a virtual AMM formula that considers the current pool balances and any fraudulent transfers (transfers with invalid attestations). Valid attestations result in favorable rates, while invalid attestations result in zero output and are marked as fraudulent.
4. Settlement Options: Users have two options for settlement:
- Immediate Settlement: A bonder can provide instant liquidity by calling
bond(), receiving the output tokens immediately in exchange for a small fee. - Direct Settlement: Users can wait for sufficient confirmations and call
withdrawClaim()to receive tokens without paying bonder fees.
Rails connects chains through paths - bidirectional bridges where transfers in one direction free up claims from transfers in the other direction. The system supports multi-hop routing, allowing transfers to be routed through multiple paths to reach their destination efficiently.
Each transfer includes a state attestation that references prior transfers from the destination chain. This allows Rails to use liquidity from new transfers to free up prior transfers on a rolling basis, eliminating the need to lock funds for the duration of cross-chain message times.
The core innovation of Rails is its rolling settlement mechanism. Instead of locking liquidity for the duration of cross-chain message times, Rails allows new transfers to immediately free up liquidity from previous transfers:
- When a transfer is initiated from Chain A to Chain B, it includes an attestation to a previous transfer from Chain B to Chain A.
- This attestation allows the new transfer's liquidity to be used to settle the previous transfer.
- Each transfer has the potential to free up an equal amount of locked liquidity, making it's net impact on locked liquidity zero.
Rails uses a virtual automated market maker to dynamically adjust rates between chains and maintain balanced liquidity flows. Unlike traditional AMMs, this requires no external liquidity provisioning since outputs are token claims rather than liquid tokens.
The source pool is calculated by taking the initial reserve amount, adding the total sent on the source chain, subtracting the total claims on the source chain (based on the submitted attestation), and removing the total amount of fraudulent claims (claims with invalid attestedClaimIds).
The destination pool is calculated by taking the initial reserve amount, adding the total sent on the destination chain, subtracting the total claims on the destination chain, and removing the total amount of fraudulent claims on the source chain.
If we did not remove fraudulent claims from the source/destination pools, a fraudulent transfer (a transfer with an invalid attestedClaimId) would increase the source pool size (by increasing totalSent) but would not increase the destination pool since the amount out for the corresponding claim is 0 due to the invalid attestedClaimId. By tracking and removing fraudulent claims, it undoes the increase to the source pool making the net effect of fraudulent transfers on the rate zero.
Bonders are staked actors in the Rails system that provide fast execution by offering immediate liquidity for transfers that require instant settlement, earning fees in exchange. Bonders are also tasked with completing system transactions such as postClaim.
A transfer is represented as a transfer on the source chain and a claim on the destination chain. The transfer's unique identifier is called a transferId on the source chain and a claimId on the destination chain. The transferId and claimId for a single transfer are identical.
Transfers form a hash chain where each transfser's identifier, the transferId, is derived from the transfer data and the previous transferId. This chain is replicated by bonders at the destination by forming a chain of identical claimIds.
Claims form a hash chain where each claim's claimId is derived from the claim data and the previous claimId. This ensures attestations to any claim also attest to all previous claims, maintaining system integrity. The claim chain should mirror the counterpart's transfer chain under normal operation.
Claims are organized into time-based buckets that enable the system to calculate the total confirmed amount without exceeding the block gas limit.
The Rails system uses a two-step confirmation approach:
- Soft Confirmations: Based on attestations at or above the claim
- Hard Confirmations: Cross-chain message confirmations providing final confirmation for claims
This layered approach allows user-contributed attestations to confirm claims before they're hard confirmed by a message.
Users are charged a send fee when sending transfers which covers the cost of initiating a cross-chain message and posting the claim at the destination. The send fee is based on a gas price oracle which is set to the destination gas cost plus a small premium. This allows the fee pool to collect an excess of fees during normal operation. When a claim is posted at the destination, bonders are credited for the ETH they spent plus a small premium as an incentive based on the local gas price. The local gas price may differ from the oracle-based gas price used to calculate the user's send fee but on average should be lower. The credit is paid to the bonder from the pool of fees back on the source chain. If a posted claim is invalid the bonder cannot withdraw the credited fee at the source chain.
A fee is also collected when posting, adding, or removing a claim. This acts as a deterrent from spamming these functions. The post claim fee contributes to the excess fees collected and is covered by the user's send fee. Add and remove fees can be redistributed to the parties creating the correct chain by the DAO.
Rails supports efficient multi-hop routing through its path system. Direct paths can be established between any two chains for any token pair. Transfers can be routed through multiple paths to reach their destination.
RailsGateway is the primary entrypoint for user and bonder operations. RailsGateway is responsible for routing transfers to RailsPath contracts (including multi-hop routing), fee collection, and cross-chain messaging.
RailsPath manages one side of a path. RailsPath is responsible for all path accounting including bonder accounting as well as holding the path's tokens. RailsPaths are deployed as proxies by the RailsGateway. Users/bonders interact with RailsPath through the RailsGateway.
FeeManager manages fee collection, distribution, and pricing across chains for the RailsGateway. It implements a vault system per chain to collect and distribute fees.
StakingRegistry determines which addresses are eligible to act as bonders by tracking staking requirements. Only staked addresses can perform bonder operations like posting claims and bonding transfers.
function send(
address to,
uint256 amount,
Hop[] memory hops
)
Initiates a cross-chain transfer with optional multi-hop routing.
- Value: The value should cover the send fee and message fee. Both fees are returned from
RailsGateway.getSendFee(uint256 chainId). When the path token is ETH, theamountshould also be included. - Parameters:
to: Recipient address on the destination chainamount: Amount of tokens to transferhops:pathId: The unique identifier of the pathmaxBonderFee: The maximum fee that can be charged by the bondermaxTotalSent: The maximumtotalSentvalue for the path. Used as slippage protection.attestedClaimId: The ID of the claim being attested toupdater: The address that can update a claim that has more hops left. This is used to update transfers that have gotten stuck becausemaxTotalSentwas exceeded before the claim was forwarded. When EOAs callsend,updateris expected to be the EOA in most cases.updatershould be address zero for the final (or only)Hop.
- Returns:
transferId- unique identifier for the transfer
function withdrawClaim(
bytes32 pathId,
bytes32 claimId,
Hop[] memory nextHops
)
Withdraws a confirmed claim without requiring bonding. Claims must be confirmed before they can be withdrawn.
- Parameters:
pathId: The unique identifier of the pathclaimId: The unique identifier of the claim to withdrawnextHops: All hops from the original transfer except the first
- Returns:
transferIdfor next hop (if applicable)
function updateClaim(
bytes32 pathId,
bytes32 claimId,
Hop[] memory currentNextHops,
Hop[] memory newNextHops
)
Updates routing information for multi-hop transfers before they're executed. Used to retry a stuck multi-hop transfer when maxTotalSent is exceeded for an intermediary hop.
function postClaim(
bytes32 pathId,
bytes32 claimId,
address to,
uint256 amount,
uint256 maxBonderFee,
bytes32 attestedClaimId,
uint256 sourcePool,
uint256 sourceTotalFraudulent,
bytes32 nextHopsHash
)
Posts a claim for a transfer from the counterpart chain. Claims must be posted in order. If an invalid claim is posted, the bonder will be slashed and the claim will be removed by another bonder. Posting a claim calculates the amountOut and stores any data relevant to withdrawing/bonding the claim.
- Parameters:
pathId: The unique identifier of the pathclaimId: The unique identifier of the claim (derived from claim data and previousclaimId)to: The recipient address for the claimamount: The input amount from the source transfermaxBonderFee: The maximum fee that can be charged by the bonderattestedClaimId: The ID of the claim being attested to (from this chain's transfer chain)sourcePool: The virtual pool size from the source chainsourceTotalFraudulent: The total amount of claims with invalid attestations on the source chainnextHopsHash: Hash of the remaining hops for multi-hop transfers (zero if no additional hops)
function bond(
bytes32 pathId,
bytes32 claimId,
uint256 bonderFee,
Hop[] memory nextHops
)
Provides immediate liquidity for a posted claim. Bonders can only bond transfers in increasing order.
- Parameters:
pathId: The unique identifier of the path containing the claimclaimId: The unique identifier of the claim to bondbonderFee: The fee charged by the bonder (must not exceed maxBonderFee from the claim)nextHops: The remaining hops for multi-hop transfer
- Returns:
transferIdfor next hop (if applicable)
function withdrawBonds(
bytes32 pathId,
bytes32 claimId
)
Withdraws a bonded claim and any unwithdrawn bonded claim before it if the claim is confirmed. Transfers funds from the path to the bonder.
- Parameters:
pathId: The unique identifier of the path containing the bonded claimclaimId: The unique identifier of the claim to withdraw bonds for
- Returns:
amount- The total amount withdrawn and transferred to the bonder
function postClaimAndBond(
bytes32 pathId,
bytes32 claimId,
address to,
uint256 amount,
uint256 maxBonderFee,
bytes32 attestedClaimId,
uint256 sourcePool,
uint256 sourceTotalFraudulent,
uint256 bonderFee,
Hop[] calldata nextHops
)
Atomically posts a claim and bonds it in a single transaction for gas efficiency.
- Parameters:
pathId: The unique identifier of the pathclaimId: The unique identifier of the claimto: The recipient address for the claimamount: The input amount from the source transfermaxBonderFee: The maximum fee that can be charged by the bonderattestedClaimId: The ID of the claim being attested to (from this chain's transfer chain)sourcePool: The virtual pool size from the source chainsourceTotalFraudulent: The total amount of claims with invalid attestations on the source chainbonderFee: The fee charged by the bonder (must not exceed maxBonderFee)nextHops: The remaining hops for multi-hop transfer
- Returns:
transferIdfor next hop (if applicable)
- Install
npmandfoundryif not installed. - Pull the
messengerrepo - In the
messengerrepo, runnpm i - In the
messengerrepo, runforge compile - In the
messengerrepo, runnpm link - In the
railsrepo, runnpm i - In the
railsrepo, runnpm link @hop-protocol/messenger - In the
railsrepo, copyremappings-sample.txttoremappings.txtin therailsrepo - In the
railsrepo, inremappings-sample.txtreplace /YOUR_MESSENGER_PATH with your path to @hop-protocol/messenger. - In the
railsrepo, create an.envfile based on.env-sample.
Optionally, you can spin up local networks using anvil instead of using external RPCs in your .env file.
anvil -p 8545 --chain-id 11155111
anvil -p 8546 --chain-id 11155420
anvil -p 8547 --chain-id 84532
anvil -p 8548 --chain-id 42069
.env
RPC_ENDPOINT_SEPOLIA="http://127.0.0.1:8545"
RPC_ENDPOINT_OPTIMISM_SEPOLIA="http://127.0.0.1:8546"
RPC_ENDPOINT_BASE_SEPOLIA="http://127.0.0.1:8547"
RPC_ENDPOINT_HOP_SEPOLIA="http://127.0.0.1:8548"
npm run test
npm run test-single-path-simulation
npm run test-multi-path-simulation
Transfers can be frontran at any step before reaching it's destination. The user is protected from unbounded slippage by the maxTotalSent parameter which caps the amount transfered ahead of the users transfer at each hop. If maxTotalSent is exceeded for the initial transfer, it will simply revert. If maxTotalSent is exceeded for an intermediary hop, the user will need to use their specified updater address to specify a new maxTotalSent and attestedClaimId or reroute the transfer.
Under normal operation, ETH and tokens should always pass through the RailsGateway and the gateway should never hold an ETH or token balance. All functions that accept ETH will return all remaining ETH in the gateway to the sender. If ETH is sent directly to the gateway, it will be sent to the next address that calls a function that returns ETH.
Fees are collected based on an oracle-supplied gas price that includes a premium above the actual estimated gas price. This allows fees to accumulate in the fee reserve. If the oracle-supplied gas price is lower than the actual gas price, fees from the fee reserve are used to cover the difference. If the fee oracle is not properly managed, fees may run dry, requiring DAO intervention.