Skip to content

hop-protocol/rails

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hop Rails

Rails enables efficient token transfers by allowing users to settle directly with each other.

How Rails Works

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.

Transfer Lifecycle

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.

Key Components

Paths

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.

State Attestations

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.

Rolling Settlement Mechanism

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.

Virtual AMM

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

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.

Transfer Unique Identifier

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.

Transfer Chain

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.

Claim Chain

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.

Bucket System

Claims are organized into time-based buckets that enable the system to calculate the total confirmed amount without exceeding the block gas limit.

Confirmation System

The Rails system uses a two-step confirmation approach:

  1. Soft Confirmations: Based on attestations at or above the claim
  2. 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.

Fees

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.

Multi-Hop Routing

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.

Architecture

RailsGateway

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

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

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

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.

Core Functions

User Operations

send

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, the amount should also be included.
  • Parameters:
    • to: Recipient address on the destination chain
    • amount: Amount of tokens to transfer
    • hops:
      • pathId: The unique identifier of the path
      • maxBonderFee: The maximum fee that can be charged by the bonder
      • maxTotalSent: The maximum totalSent value for the path. Used as slippage protection.
      • attestedClaimId: The ID of the claim being attested to
      • updater: The address that can update a claim that has more hops left. This is used to update transfers that have gotten stuck because maxTotalSent was exceeded before the claim was forwarded. When EOAs call send, updater is expected to be the EOA in most cases. updater should be address zero for the final (or only) Hop.
  • Returns: transferId - unique identifier for the transfer

withdrawClaim

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 path
    • claimId: The unique identifier of the claim to withdraw
    • nextHops: All hops from the original transfer except the first
  • Returns: transferId for next hop (if applicable)

updateClaim

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.

Bonder Operations

postClaim

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 path
    • claimId: The unique identifier of the claim (derived from claim data and previous claimId)
    • to: The recipient address for the claim
    • amount: The input amount from the source transfer
    • maxBonderFee: The maximum fee that can be charged by the bonder
    • attestedClaimId: The ID of the claim being attested to (from this chain's transfer chain)
    • sourcePool: The virtual pool size from the source chain
    • sourceTotalFraudulent: The total amount of claims with invalid attestations on the source chain
    • nextHopsHash: Hash of the remaining hops for multi-hop transfers (zero if no additional hops)

bond

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 claim
    • claimId: The unique identifier of the claim to bond
    • bonderFee: The fee charged by the bonder (must not exceed maxBonderFee from the claim)
    • nextHops: The remaining hops for multi-hop transfer
  • Returns: transferId for next hop (if applicable)

withdrawBonds

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 claim
    • claimId: The unique identifier of the claim to withdraw bonds for
  • Returns: amount - The total amount withdrawn and transferred to the bonder

Combined Operations

postClaimAndBond

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 path
    • claimId: The unique identifier of the claim
    • to: The recipient address for the claim
    • amount: The input amount from the source transfer
    • maxBonderFee: The maximum fee that can be charged by the bonder
    • attestedClaimId: The ID of the claim being attested to (from this chain's transfer chain)
    • sourcePool: The virtual pool size from the source chain
    • sourceTotalFraudulent: The total amount of claims with invalid attestations on the source chain
    • bonderFee: The fee charged by the bonder (must not exceed maxBonderFee)
    • nextHops: The remaining hops for multi-hop transfer
  • Returns: transferId for next hop (if applicable)

Set up

  1. Install npm and foundry if not installed.
  2. Pull the messenger repo
  3. In the messenger repo, run npm i
  4. In the messenger repo, run forge compile
  5. In the messenger repo, run npm link
  6. In the rails repo, run npm i
  7. In the rails repo, run npm link @hop-protocol/messenger
  8. In the rails repo, copy remappings-sample.txt to remappings.txt in the rails repo
  9. In the rails repo, in remappings-sample.txt replace /YOUR_MESSENGER_PATH with your path to @hop-protocol/messenger.
  10. In the rails repo, create an .env file based on .env-sample.

Testing

Using Anvil

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"

Run Unit Tests

npm run test

Run Single-Path Simulation

npm run test-single-path-simulation

Run Hub Simulation

npm run test-multi-path-simulation

Known Issues

Multi-hop transfers can be frontran

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.

ETH sent to RailsGateway taken by anyone.

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.

Improperly Managed Fee Reserve May Run Dry

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.

About

Hop Rails

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •