Loading
Finalization
Launching
Soul Docs
Contracts
Router

Router

Last updated: 18 / 07 / 2025

The Router.sol smart contract is the cornerstone of Soul Protocol’s cross-chain capabilities. It acts as the protocol’s messaging hub—facilitating secure, modular communication between contracts deployed on different blockchains. Soul integrates with multiple messaging providers such as LayerZero V2, Wormhole, and more.

The Router abstracts cross-chain logic from the core lending features, enabling flexible and efficient message passing through routing orders. Each routing order specifies:

  • dstChainId: the destination chain ID
  • dstAddress: the receiving contract on the destination chain
  • payload: ABI-encoded calldata to be executed remotely

The Router handles the entire message lifecycle: from fee estimation, sending, validation, and delivery—through provider-specific infrastructure. This ensures a consistent developer experience, regardless of the provider used.

Public Functions

1. send

Transmits a cross-chain message to a remote contract on another blockchain.

function send(
  uint96 dstChainId,
  address dstAddress,
  bytes calldata payload,
  AdapterParams memory adapterParams
) external payable onlyWhitelistedAddress;
  • Parameters:

    • uint96 dstChainId: The ID of the destination chain.
    • address dstAddress: The address on the destination chain to receive the payload.
    • bytes calldata payload: The data to be sent.
    • AdapterParams calldata adapterParams: Additional parameters for customizing the message delivery.
  • Payable: Yes (requires a native token for fee costs).

  • onlyWhitelistedAddress: Only whitelisted contracts can send messages through the Router.

Example:
    IRouter(routerAddress).send{value: msg.value}(
        1002, // Destination chain ID (e.g., Polygon)
        0x1234567890abcdef1234567890abcdef12345678, // Destination address
        abi.encode("Hello, Soul!"), // Payload
        adapterParams // Adapter parameters
    );

2. retrySend

Retries sending a previously failed payload.

function retrySend(
  uint64 nonce,
  uint96 dstChainId,
  address dstAddress,
  bytes calldata payload,
  AdapterParams memory adapterParams,
  Provider provider
) external payable;
  • Parameters: are retrieved from the MessageSent event emitted during the original send.

    • uint64 nonce: The nonce of the failed transaction.
    • uint96 dstChainId: The ID of the destination chain.
    • address dstAddress: The address on the destination chain.
    • bytes calldata payload: The original data.
    • AdapterParams memory adapterParams: Retry parameters.
    • Provider provider: Messaging provider to use.
  • Permissionless: Anyone can call it

  • Flexible: Use a different provider than the original attempt

  • Safe: Requires message hash verification to prevent replays or tampering

Example:
    IRouter(routerAddress).retrySend{value: msg.value}(
        failedNonce,
        1002,
        0x1234567890abcdef1234567890abcdef12345678,
        failedPayload,
        retryAdapterParams,
        Provider.LAYER_ZERO // Messaging provider
    );

3. retryReceive

Retries a failed on-chain execution on the destination chain.

function retryReceive(
  uint64 nonce,
  uint96 srcChainId,
  address dstAddress,
  uint nativeTokens,
  bytes calldata payload
) external payable;
  • Parameters:

    • uint64 nonce: The nonce of the failed transaction.
    • uint96 srcChainId: The ID of the source chain.
    • address dstAddress: The destination address (on this chain).
    • uint nativeTokens: Amount of native tokens involved.
    • bytes memory payload: The original data.
    • AdapterParams calldata adapterParams: Retry adapter parameters.
  • Permissionless: Anyone can help recover messages

  • Robust: Verifies against stored message hash

  • Recoverable: Can be retried with updated gas or token values

Example:
    IRouter(routerAddress).retryReceive(
        failedNonce,
        1001, // Source chain ID
        0x1234567890abcdef1234567890abcdef12345678,
        0, // No tokens involved
        failedPayload
    );

AdapterParams

The AdapterParams struct customizes message cost and delivery behavior:

struct AdapterParams {
  address refundAddress;
  uint gasOnDst;
  uint nativeOnDst;
}
  • gasOnDst: Gas forwarded to the destination contract

  • nativeOnDst: Native tokens to airdrop on the destination chain

  • refundAddress: Receives any unused funds from msg.value

These values are critical in configuring execution guarantees and seamless cross-chain UX. Soul’s frontend estimates these values automatically.

Estimating Cross-Chain Fees

To support fee calculation and validation, the Router includes a public view function:

function getFee(
  uint dstChainId,
  uint payloadSize,
  uint gasOnDst,
  uint nativeOnDst,
  Provider provider
) public returns (uint fee);

This returns the exact native token fee required to send a message using the selected parameters and provider.

Soul’s frontend app uses this function to display real-time, provider-based fee estimates for users.

Message Delivery Flow

When a cross-chain message arrives at the destination chain, the Router performs the final call:

(bool success, ) = dstAddress.call{value: nativeTokens, gas: gasleft() - receiveLeftGas}(payload);
  • nativeTokens is the airdrop from nativeOnDst

  • receiveLeftGas ensures the Router can finalize safely even if dstAddress fails

  • Receiving contracts must verify the caller is the Router to protect against malicious calls.

Provider Support

Soul currently supports:

  • LayerZero V2: Default provider for all lanes

  • Wormhole: Available for retrying attempts

Each cross-chain lane can have a distinct default provider. More providers like CCIP and Axelar will be added over time.

Failure Recovery

  • Recovery Scenario 1: Delivery Failure If a message is emitted on the source chain but never reaches the destination, use retrySend. Causes may include:

    • Temporary outage of a cross-chain provider
    • Validator network failure
  • Recovery Scenario 2: Execution Failure If the message arrives but fails on execution (e.g., due to low gas), use retryReceive. Soul does not revert failed deliveries. It tracks and stores the failure state on-chain and allows permissionless retries.