0% found this document useful (0 votes)
17 views

Across Protocol - API Docs

API documentation file for using Across Protocol.

Uploaded by

Jordan Durrani
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
17 views

Across Protocol - API Docs

API documentation file for using Across Protocol.

Uploaded by

Jordan Durrani
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 47

Instant Bridging in your Application

Provide a fast, reliable and low cost bridging experience alongside your application, enabling users to onboard without leaving your app. Improve
conversion and reduce churn.

This solution is a great fit for apps that want to encourage users to onboard and bridge funds once (or infrequently) so users can then make recurring,
or subsequent, actions in the application.

The Bridge Integration Guide contains technical instructions for using our API to provide bridge quotes to your users. It is a simple two step integration
process:

​ Request quotes from Across GET /suggested-fees API


​ Initiate a deposit by using data returned from Across API to call functions on Across smart contracts

See Multi Chain Bridge UI Guide or Single Chain Bridge UI Guide for recommended UI and UX patterns in providing a bridge experience to your
users.

If you have further questions or suggestions for this guide, please send a message to the #developer-questions channel in the Across Discord.

Bridge Integration Guide


This guide contains technical instructions for using our API to provide bridge quotes to your users via bridge experience alongside your application. It
is a simple two step integration process:

​ Request quotes from Across API


​ Use data returned to call functions on Across smart contracts (initiate a deposit)

Request a Quote

The process for initiating a deposit begins with determining the fee that needs to be paid to the relayer. To do this, you can use the suggested-fees
endpoint: app.across.to/api/suggested-fees with the following query parameters:

​ originChainId: chainId where the user's deposit is originating

​ destinationChainId: chainId where the user intends to receive their funds

​ token: the address of the token that the user is depositing on the origin chain

​ amount: the raw amount the user is transferring. By raw amount, this means it should be represented exactly as it is in Solidity, meaning 1
USDC would be 1e6 or 1 ETH would be 1e18.

Example for a user transferring 1000 USDC from Ethereum to Optimism:


Copy
curl
"https://app.across.to/api/suggested-fees?token=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&originChainId=1&destinationChainId=10&amou
nt=1000000000"

There are two elements of the response that you'll need to create the deposit:

​ totalRelayFee.total: this is the amount of the deposit that the user will need to pay to have it relayed. It is intended to capture all fees,
including gas fees, relayer fees, and system fees.
​ timestamp: this is the quote timestamp. Specifying this timestamp onchain ensures that the system fees don't shift under the user while the
intent is in-flight.

To determine how long a fill is expected to take and to ensure that Across can process a deposit of a given size, you'll want to refer the following
elements of the response:

​ fillSpeedType: one of instant, shortDelay or slow corresponding to an instant fill (within seconds), short delay (within 5-15 minutes) or slow
(~3 hours)
​ estimateFillTimeSec = the estimated fill time, for the specified route and asset, in seconds

The above values depend on the amount in the request relative to the following limit elements of the response:

​ maxDepositInstant: if the user's amount is less than or equal to this amount, there is known to be enough relayer liquidity on the
destination chain to fill them instantly, within seconds
​ maxDepositShortDelay: if the user's deposit amount is larger than maxDepositInstant, and less than or equal to this amount, there is
known to be enough relayer liquidity that can be moved to the destination chain to fill the user within 30 minutes
​ maxDeposit: if the user's deposit amount is larger than maxDepositShortDelay, and less than or equal to this amount, there is enough
liquidity in Across to fill them via a slow fill, which could take up to 3 hours. In most cases maxDeposit will be equal to
maxDepositShortDelay to reduce the likelihood of slow fills. If the user's deposit amount is larger than this, Across cannot fulfill the user's
intent
​ reccommendedDepositInstant: this is used for certain integrations to limit the input size, and is currently hardcoded to 2 ETH/WETH and
5,000 USDC

You can find details on the Across API here.

Initiating a Deposit (User Intent)

Deposits are initiated by interacting with contracts called SpokePools. There is one SpokePool deployed on each chain supported by Across. Each
SpokePool has minor modifications to work with each chain, but maintains the same core interface and implementation. For example, on Ethereum the
SpokePool contract is named Ethereum_SpokePool.sol, and on Optimism the contract is named Optimism_SpokePool.sol.

Calling depositV3

Before making the call, you'll need the SpokePool address. This can be retrieved from the suggested-fees response for convenience, but we suggest
manually verifying these and hardcoding them per chain in your application for security. You can find the addresses here.

Once you have the SpokePool address, you'll need to approve the SpokePool to spend tokens from the user's EOA or the contract that will be calling
the SpokePool . The approval amount must be >= the amount value. If sending ETH, no approval is necessary.

The deposit call can come from an intermediary contract or directly from the user's EOA. This is the function that needs to be called:
Copy
function depositV3(

address depositor,

address recipient,

address inputToken,

address outputToken,

uint256 inputAmount,

uint256 outputAmount,

uint256 destinationChainId,

address exclusiveRelayer,

uint32 quoteTimestamp,

uint32 fillDeadline,

uint32 exclusivityDeadline,

bytes calldata message

) external;

Here is an example of how the parameters could be populated in Javascript:


Copy
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// Bridge to the same address on the destination chain.

// Note: If the depositor is not an EOA then the depositor's address

// might not be valid on the destination chain.

const depositor = <USER_ADDRESS>;

const recipient = depositor;

// Test WETH deposit.

const inputToken = <ORIGIN_CHAIN_WETH_ADDRESS>;

// The 0 address is resolved automatically to the equivalent supported

// token on the the destination chain. Any other input/output token

// combination should be advertised by the Across API available-routes

// endpoint.
const outputToken = ZERO_ADDRESS;

// The outputAmount is set as the inputAmount - relay fees.

// totalRelayFee.total is returned by the Across API suggested-fees

// endpoint.

const outputAmount = inputAmount.sub(BigNumber.from(response.totalRelayFee.total));

// fillDeadline: A fill deadline of 5 hours. Can be up to

// SpokePool.getCurrentTime() + SpokePool.fillDeadlineBuffer() seconds.

const fillDeadlineBuffer = 18000;

const fillDeadline = Math.round(Date.now() / 1000) + fillDeadlineBuffer;

// timestamp is returned by the Across API suggested-fees endpoint.

// This should be _at least 2_ mainnet blocks behind the current time

// for best service from relayers.

const quoteTimestamp = response.timestamp;

// Exclusive relayer and exclusivity deadline should be taken from the

// Across API suggested-fees response.

const exclusivityDeadline = response.exclusivityDeadline;

const exclusiveRelayer = response.exclusiveRelayer;

// No message will be executed post-fill on the destination chain.

// See `Across+ Integration` for more information.

const message = "0x";

spokePool.depositV3(

depositor,

recipient,

inputToken,

outputToken,

inputAmount,

outputAmount,

destinationChainId,

exclusiveRelayer,

quoteTimestamp,

fillDeadline,

exclusivityDeadline,

message

and in Solidity (see also the Javascript descriptions above):


Copy
spokePool.depositV3(
depositor, // User's address on the origin chain.

recipient, // Receiving address on the destination chain.

weth, // inputToken. This is the WETH address on the origin chain.

address(0), // outputToken: Auto-resolve the destination equivalent token.

inputAmount,

inputAmount - totalRelayFee, // outputAmount

destinationChainId,

exclusiveRelayer, // exclusiveRelayer from suggested-fees, or 0x0 to disable

quoteTimestamp, // suggested-fees timestamp, or block.timestamp - 36

block.timestamp + spokePool.fillDeadlineBuffer(),

exclusivityDeadline, // exclusivityDeadline from suggested-fees, or 0 to disable

"", // message (empty)

);
Append Unique Identifier to CallData

In order to track the origination source for deposits, we request all integrators to append a delimiter of 1dc0de and a unique identifier provided by our
team to the deposit transaction call data.

Here is an example of depositV3 call data with 1dc0de delimiter and 0000 unique identifier appended.
Do NOT use 0000 identifier in your implementation, see below on how to request your unique ID.

Do NOT pass delimiter + identifier to any depositV3 param, including the message param. Only append to call data of the transaction.
Copy
Function: depositV3(address depositor,address recipient,address inputToken,address outputToken,uint256 inputAmount,uint256
outputAmount,uint256 destinationChainId,address exclusiveRelayer,uint32 quoteTimestamp,uint32 fillDeadline,uint32 exclusivityDeadline,bytes
message)

0x7b939232000000000000000000000000c30c7ea910a71ce06ae840868b0c7e47616ba4c9000000000000000000000000c30c7ea910a71ce06ae840
868b0c7e47616ba4c9000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000af88d065e77c8cc2
239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000009502f9000000000000000000000000000000000
000000000000000000000000094fd2f84000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066799bfb00000000000000
0000000000000000000000000000000000000000006679f0f40000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000
0001dc0de0000

Request your unique identifier in our shared communication channel (TG, Slack, etc), if already set up, or reach out to [email protected]. Please do not
use any other identifier other than the one provided to you.

Updating Deposits

Relayer fees are a function of the spread between the outputAmount and inputAmount parameters of a depositV3. If this spread is too low, it may not
be profitable for relayers to fill. While a deposit is not filled, the outputAmount can be increased by calling speedUpDepositV3 , which requires a
signature from the depositor address. If fees are set according to the above instructions, this functionality should not be needed.

Tracking Deposits

To track the lifecycle of a deposit you can use the app.across.to/api/deposit/status endpoint with the following parameters:

​ originChainId: chain Id where the deposit originated from

​ depositId: The deposit id that is emitted from the DepositV3 function call as a V3FundsDeposited event

The recommended solution for tracking all Across deposits originating from your integration, for a single user, is to store the user's depositId and
originChainId from each transaction originating from your app, and then get the status of each via the above endpoint.

We also provide a deeper explanation to implement your service to track events in Tracking Events.

Technical FAQ

Multi Chain Bridge UI Guide


This UI guide is a counterpart to the technical Bridge Integration Guide and is intended to offer UI and UX patterns developers can implement
alongside their core application that has instances on multiple chains, for example on a separate "Bridge" tab.
Best Practices

​ Provide a full bridge (or "onboarding") experience in your application, for example on a "Bridge" tab.
​ Default the source chain selector to the currently connected network.
​ Don't require users to go to wallet, switch network, then return to app to perform an action. For example, if the user selects a source chain
that is not the network they are currently connected to:
​ 1: user clicks the button to perform the action ("confirm")
​ 2: wallet prompts user to switch network to the source chain
​ 3: wallet prompts user to execute the bridge

Example Flow

From chain defaults to Ethereum, the network the user' wallet is currently connected to.

User selects to use their funds on Arbitrum.


User inputs the amount and asset they wish to send.

Since the user is currently connected to Ethereum, they will need to switch to Arbitrum and then confirm the deposit.

After switching the wallet network to Arbitrum the user is immediately prompted to confirm the transaction, without needing to go back to the app.
Transfers in Across are executed extremely fast, typically under 5 seconds and providing the user with a timer to completion provides a magical UX
and reduces the uncertainty they experience when bridging.

Single Chain Bridge UI Guide


This UI guide is a counterpart to the technical Bridge Integration Guide and is intended to offer UI and UX patterns developers can implement
alongside their core application on a single chain, for example on a separate "Bridge" tab.

Best Practices

​ Provide a full bridge or "onboarding" experience in your application, for example on a "Bridge" tab.
​ Default the source chain selector to the balance where the user has the most funds of the input asset. The destination chain is the network
where the user is interacting with your protocol.
​ Don't require users to go to wallet, switch network, then return to app to perform an action. For example, if the user selects a source chain
that is not the network they are currently connected to:
​ 1: user clicks the button to perform the action ("confirm")
​ 2: wallet prompts user to switch network to the source chain
​ 3: wallet prompts user to execute the bridge

Example Flow

For apps that are only deployed on a single chain, the destination chain can be defaulted to your chain, reducing the steps for users.
User selects to use their funds on Arbitrum.

User inputs the amount and asset they wish to send.

Since the user is currently connected to Polygon, they will need to switch to Arbitrum and then confirm the deposit.
After switching the wallet network to Arbitrum the user is immediately prompted to confirm the transaction, without needing to go back to the app.

Transfers in Across are executed extremely fast, typically under 5 seconds and providing the user with a timer to completion provides a magical UX
and reduces the uncertainty they experience when bridging.

Embedded Cross-chain Actions


For developers wanting to enable users to seamlessly interact with your dApp or chain using assets from other chains, this guide provides technical
instructions for bundling a bridge with user actions.

This solution is a great fit for apps on a single-chain or that want to expose core actions that users can execute with funds on other chains. It is a
future-forward solution that aims to abstract bridging from end-users by making cross-chain actions so fast and cheap that it feels like a single-chain
experience.

The Cross-chain Actions Integration Guidecontains technical instructions for bundling a bridge with user actions. The integration process is simple and
consists of three steps:

​ Craft the message that is the set of transactions to execute on the destination
​ Request quotes from Across GET /suggested-fees API, passing in the message parameter defined above

​ Initiate a deposit by using data returned from Across API to call functions on Across smart contracts, specifying a message (set of
transactions to execute on the destination)

See Cross-chain Actions Integration Guide for recommended UI and UX patterns.

Cross-chain Actions Integration Guide


This guide provides technical instructions for bundling a bridge with user actions. The integration process is simple and consists of three steps:

​ Craft the message that is the set of transactions to execute on the destination
​ Request quotes from Across GET /suggested-fees API, passing in the message parameter defined above

​ Initiate a deposit by using data returned from Across API to call functions on Across smart contracts, specifying a message (set of
transactions to execute on the destination)

Executing Destination Actions


At a high level, the system works as follows:

​ End-user (or intermediate contract) includes a message field in the deposit transaction.
​ To be repaid for the fill, the relayer is required to include the same message field in their fill.
​ When this message field is non-empty and the recipient is a contract, the SpokePool calls a special handler function on the recipient handler
contract with the message (and a few other fields).
​ This function can do anything, meaning application-specific actions can be executed atomically

This integration requires a handler contract on the destination to "handle" (execute) the transactions specified in the message. There are two
approaches to implementing this functionality with respect to handler contract.

​ (recommended) Use our already-deployed and audited generic "multicall" handler contract
​ Build and implement your own custom handler contract

Each of the below guides provide instruction and examples for the 3 integration steps above.
Using the Generic Multicaller Handler Contract

The recommended way is to use our already-deployed and audited generic "multicall" handler contract. When this contract is set as the recipient
address, this contract will atomically execute all contract calls specified in the deposit's message.
Using a Custom Handler Contract

If your use case requires additional customization or complexity, you can build your own custom handler contract to execute the contents provided in
the deposit's message.

How to Decide

The tradeoff of using the generic multicall handler contract is that the complexity is encountered when crafting the message, which must be a
correctly-encoded Instructions object. Once the message is formed correctly, you don't need to write any more code.

On the other hand, when using a custom handler contract, you'll need to implement a custom handleV3AcrossMessage function and deploy a new
contract, but creating the deposit message is trivial.

If you have further questions or suggestions for this guide, please send a message to the #developer-questions channel in the Across Discord.

UI Guide

See Cross-chain Actions Integration Guide for recommended UI and UX patterns.

Using the Generic Multicaller Handler Contract


Creating a Transaction for an Aave Deposit (Example #1)

In this example we'll be walking through how to use this functionality to perform an Aave deposit on behalf of the user on the destination chain, using
our audited generic multicall handler. Deployments of the audited multicall handler contract be found here.
Crafting the Message

To execute a destination action, it is required that you send some nonempty message to the handler contract on the other side. This message allows
you to pass arbitrary information to a recipient contract and it ensures that Across understands that you intend to trigger the handler function on the
recipient contract (instead of just transferring tokens). A message is required if you want the handler to be called.

In this example, our message will include the user's address, the Aave contract address, and the calldata we want to execute to submit an Aave
deposit as the minimum required information that our handler contract would need to know to generate an Aave deposit instruction. Here's an
example for generating this in typescript:
Copy
// Ethers V6

import { ethers } from "ethers";

function generateMessageForMulticallHandler(

userAddress,

aaveAddress,

depositAmount,

depositCurrency,

aaveReferralCode

){

const abiCoder = ethers.AbiCoder.defaultAbiCoder();


// Define the ABI of the functions

const approveFunction = "function approve(address spender, uint256 value)";

const depositFunction = "function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)";

// Create Interface instances

const erc20Interface = new ethers.Interface([approveFunction]);

const aaveInterface = new ethers.Interface([depositFunction]);

// Encode the function calls with selectors

const approveCalldata = erc20Interface.encodeFunctionData("approve", [aaveAddress, depositAmount]);

const depositCalldata = aaveInterface.encodeFunctionData("deposit", [depositCurrency, depositAmount, userAddress, aaveReferralCode]);

// Encode the Instructions object

return abiCoder.encode(

"tuple(" +

"tuple(" +

"address target," +

"bytes callData," +

"uint256 value" +

")[]," +

"address fallbackRecipient" +

")"

],

[depositCurrency, approveCalldata, 0],

[aaveAddress, depositCalldata, 0],

],

userAddress

);

Example in solidity:
Copy
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

interface AavePool {
function deposit(

address asset,

uint256 amount,

address onBehalfOf,

uint16 referralCode

) external;

contract GenerateMessageHelper {

using SafeERC20 for IERC20;

struct Call {

address target;

bytes callData;

uint256 value;

struct Instructions {

// Calls that will be attempted.

Call[] calls;

// Where the tokens go if any part of the call fails.

// Leftover tokens are sent here as well if the action succeeds.

address fallbackRecipient;

function generateMessageForMulticallHandler(

userAddress: address,

aavePool: AavePool,

depositAmount: uint256,

depositCurrency: address,

aaveReferralCode: uint16,

) returns (bytes memory) {

bytes memory approveCalldata = abi.encodeWithSelector(

IERC20.safeIncreaseAllowance.selector,

aaveAddress,

depositAmount

);

bytes memory depositCalldata = abi.encodeWithSelector(

AavePool.deposit.selector,

depositCurrency

depositAmount,

userAddress,

aaveReferralCode
);

Call[] calls;

calls.push(Call({ target: depositCurrency, callData: approveCalldata, value: 0 });

calls.push(Call({ target: address(aavePool), callData: depositCalldata, value: 0 });

Instructions memory instructions = Instructions({

calls: calls,

fallbackRecipient: userAddress

});

return abi.encode(instructions);

function generateMessageForCustomhandler(address userAddress) returns (bytes memory) {

return abi.encode(userAddress);

}
Generating the Deposit

The deposit creation process is nearly identical to the process described in Bridge Integration Guide. However, there are a few tweaks to that process
to include a message.

​ When getting a quote, two additional query parameters need to be added.


​ recipient: the recipient for the deposit. In this use case, this is not the end-user. It is the the multicall handler contract.
​ message: the message you crafted above.
​ When calling depositV3, you'll need to make a slight tweak to the parameters.
​ The recipient should be set to the multicall handler contract
​ The message field should be set to the message you generated above instead of 0x.

​ In the Aave example, the outputAmount needs to be set equal to the depositAmount to ensure that the amount expected to be
received on the destination chain by the handler contract is equal to the amount to be deposited into Aave.

What happens after the deposit is received on the destination chain?

This contract, as mentioned above, is audited and works as a generic handler where you can submit a list of transactions to be executed on the
destination chain after receiving the deposit. This contract's deployed addresses can be found here. All you need to do to get started using this
contract to execute a destination action is to set the deployed address on the deposit's destination chain as the recipient address and construct the
message appropriately.

The multicall handler implements a handleV3AcrossMessage that expects the message to be an encoded set of Instructions, which contains a list of
transactions to execute. In the Aave exmaple, the transactions to execute are (1) approve the Aave pool to transfer tokens from the handler contract
and (2) submit a deposit to the Aave pool using the received tokens. In the example above, the function generateMessageForMulticallHandler can
be used to create a message in the correct format.

As you can see above when generating the deposit message and below in building a custom handler contract, the tradeoff of using the multicall
handler contract is that the complexity is encountered when crafting the message, which must be a correctly-encoded Instructions object. Once the
message is formed correctly, you don't need to write any more code.
Message Constraints

Handler contracts only uses the funds that are sent to it. That means that the message is assumed to only have authority over those funds and,
critically, no outside funds. This is important because relayers can send invalid relays. They will not be repaid if they attempt this, but if an invalid
message could unlock other funds, then a relayer could spoof messages maliciously.
Conclusion

Now that you have a process for constructing a message, creating a deposit transaction, all you need to do is send the deposit to have the generic
multicall handler get executed on the destination.

Creating an Transaction for WrapChoice (Example #2)

Here's another contract example using a slightly different message format to allow users to choose whether they want to receive WETH or ETH. In
this example, a bool is passed along with the address of the user to allow the message to define the unwrapping behavior on the destination.

Here is an example of generating a message that can be used with an already-deployed Multicall handler contract:
Copy
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

interface WETHInterface {

function withdraw(uint wad) external;

contract GenerateMessageHelper {

using SafeERC20 for IERC20;

// Optimism WETH contract hardcoded as an example.

address public constant WETH = 0x4200000000000000000000000000000000000006;

struct Call {

address target;

bytes callData;

uint256 value;

struct Instructions {

// Calls that will be attempted.

Call[] calls;

// Where the tokens go if any part of the call fails.

// Leftover tokens are sent here as well if the action succeeds.

address fallbackRecipient;

error NotWETH();

function generateMessageForMulticallHandler(

userAddress: address,

outputToken: address,

outputAmount: uint256,

sendWeth: bool

) returns (bytes memory) {

if (outputToken != weth) revert NotWETH();

Call[] calls;

if (sendWETH) {

// Transfer WETH directly to user address.

bytes memory transferCalldata = abi.encodeWithSelector(

IERC20.safeTransfer.selector,

userAddress,

outputAmount
);

calls.push(Call({ target: outputToken, callData: transferCalldata, value: 0 });

} else {

// Unwrap WETH and send ETH directly to user address

bytes memory withdrawCalldata = abi.encodeWithSelector(

WETHInterface.withdraw.selector,

outputAmount

);

calls.push(Call({ target: outputToken, callData: withdrawCalldata, value: 0 });

calls.push(Call({ target: userAddress, callData: "", value: outputAmount });

Instructions memory instructions = Instructions({

calls: calls,

fallbackRecipient: userAddress

});

return abi.encode(instructions);

Using the above contract, generate a message by calling generateMessageForMulticallHandler and then use this message when submitting a deposit.
Like in the Aave example, the outputAmount and outputToken used to form the message should match the deposit's outputAmount and outputToken.

The deposit recipient should be set to the deployed MulticallHandler address on the destination chain. Generating the Deposit is identical to
process described above.

Reverting Transactions

​ If the message specifies a transaction that could revert when handled on the destination, it is recommended to set a fallbackRecipient

​ If a fallbackRecipient is set to some non-zero address and any instruction reverts, the tokens that are received in the fill as
outputAmount will be sent to the fallbackRecipient on the destinationChainId
​ If no fallback is set, and any instructions fail, the fill on destination will fail and cannot occur. In this case the deposit will expire when
the destination SpokePool timestamp exceeds the deposit fillDeadline timestamp, the depositor will be refunded on the
originChainId. Ensure that the depositor address on the origin SpokePool is capable of receiving refunds.

Summarized Requirements

​ The deposit message is not empty. Create the message by encoding instructions correctly that will be executed by the MulticallHandler on the
destination chain.
​ Multicall Handler contract deployments can be found here
​ Alongside the encoded instructions in the message, set a fallbackRecipient address to one that should receive any leftover tokens
regardless of instruction execution success. In most cases, this should be the end user's address
​ The recipient address is the multicall handler contract on the destinationChainId

​ Use the Across API to get an estimate of the relayerFeePct you should set for your message and recipient combination

​ Call depositV3() passing in your message

​ Once the relayer calls fillV3Relay() on the destination, the recipient multicall handler contract's handleV3AcrossMessage will be executed

​ The additional gas cost to execute the above function is compensated for in the deposit's relayerFeePct

Security & Safety Considerations

​ Avoid making unvalidated assumptions about the message data supplied to handleV3AcrossMessage(). Across does not guarantee message
integrity, only that a relayer who spoofs a message will not be repaid by Across. If integrity is required, integrators should consider including a
depositor signature in the message for additional verification. Message data should otherwise be treated as spoofable and untrusted for use
beyond directing the funds passed along with it.
​ Avoid embedding assumptions about the transfer token or transfer amount into their messages. Instead use the tokenSent and amount
variables supplied with to the handleV3AcrossMessage() function. These fields are enforced by the SpokePool contract and so can be
assumed to be correct by the recipient contract.
​ The relayer(s) able to complete a fill can be restricted by storing an approved set of addresses in a mapping and validating the
handleV3AcrossMessage() relayer parameter. This implies a trust relationship with one or more relayers.

Using a Custom Handler Contract


Creating an Transaction for an Aave Deposit (Example #1)

In this example we'll be walking through how to use this functionality to perform an Aave deposit on behalf of the user on the destination chain, using a
custom handler contract.

It is reccommended for most use cases to use the Using the Generic Multicaller Handler Contract, but if your use case requires more complex logic,
you'll need to implement a custom handleV3AcrossMessage function and deploy a new contract, but creating the deposit message is trivial.

Crafting the Message


Copy
// Ethers V6

import ethers from "ethers";

function generateMessageForCustomHandler(userAddress: string) {

const abiCoder = ethers.AbiCoder.defaultAbiCoder();

return abiCoder.encode(["address"], [userAddress]);

Example in solidity:
Copy
function generateMessageForCustomhandler(address userAddress) returns (bytes memory) {

return abi.encode(userAddress);

}
Implementing the Handler Contract

You will need to implement a function matching the following interface in your handler contract to receive the message:
Copy
function handleV3AcrossMessage(

address tokenSent,

uint256 amount,

address relayer,

bytes memory message

) external;

For this example, we're depositing the funds the user sent into AAVE on the user's behalf. Here's how that full contract implementation might look.
This contract has not been vetted whatsoever, so use this sample code at your own risk.
Copy
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

interface AavePool {

function deposit(

address asset,

uint256 amount,

address onBehalfOf,

uint16 referralCode

) external;
}

contract AaveDepositor {

using SafeERC20 for IERC20;

error Unauthorized();

address public immutable aavePool;

uint16 public immutable referralCode;

address public immutable acrossSpokePool;

constructor(address _aavePool, uint16 _referralCode, address _acrossSpokePool) {

aavePool = _aavePool;

referralCode = _referralCode;

acrossSpokePool = _acrossSpokePool;

function handleV3AcrossMessage(

address tokenSent,

uint256 amount,

address relayer, // relayer is unused

bytes memory message

) external {

// Verify that this call came from the Across SpokePool.

if (msg.sender != acrossSpokePool) revert Unauthorized();

// Decodes the user address from the message.

address user = abi.decode(message, (address));

// Approve and deposit the funds into AAVE on behalf of the user.

IERC20(tokenSent).safeIncreaseAllowance(aavePool, amount);

aavePool.deposit(tokenSent, amount, user, referralCode);

}
Generating the Deposit

The deposit creation process is nearly identical to the process described for initiating a deposit (Bridge Integration Guide). However, there are a few
tweaks to that process to include a message.

​ When getting a quote, two additional query parameters need to be added.


​ recipient: the recipient for the deposit. In this use case this is not the end-user. It is the contract that implements the handler you
would like to call.
​ message: the message you crafted above.
​ When calling depositV3, you'll need to make a slight tweak to the parameters.
​ The recipient should be set to your custom handler contract.
​ The message field should be set to the message you generated above instead of 0x.
​ In the above example, the outputAmount needs to be set equal to the depositAmount to ensure that the amount expected to be
received on the destination chain by the handler contract is equal to the amount to be deposited into Aave.

You can find this interface definition in the codebase here.


What happens after the deposit is received on the destination chain?

When the relay is filled, the destination SpokePool calls handleV3AcrossMessage on the recipient contract (your custom handler contract) with the
message (and a few other fields). You can define any arbitrary logic in handleV3AcrossMessage to fit your use case.

Message Constraints

Handler contracts only uses the funds that are sent to it. That means that the message is assumed to only have authority over those funds and,
critically, no outside funds. This is important because relayers can send invalid relays. They will not be repaid if they attempt this, but if an invalid
message could unlock other funds, then a relayer could spoof messages maliciously.
Conclusion

Now that you have a process for constructing a message, creating a deposit transaction, and you have a handler contract deployed on the destination
chain, all you need to do is send the deposit to have the handler get executed on the destination.

Creating an Transaction for WrapChoice (Example #2)

Here's another contract example using a slightly different message format to allow users to choose whether they want to receive WETH or ETH. This
example uses a custom handler contract. In this example, the message should be contain an encoded address and bool value only:
Copy
function generateMessageForCustomHandler(userAddress: string, sendWeth: boolean) {

const abiCoder = ethers.AbiCoder.defaultAbiCoder();

return abiCoder.encode(["address", "bool"], [userAddress, sendWeth]);

}
Copy
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

interface WETHInterface {

function withdraw(uint wad) external;

contract WrapChoice {

using SafeERC20 for IERC20;

error Unauthorized();

error NotWETH();

error FailedETHTransfer()

address public immutable weth;

address public immutable acrossSpokePool;

constructor(address _weth, address _acrossSpokePool) {

weth = _weth;

acrossSpokePool = _acrossSpokePool;

function handleV3AcrossMessage(

address tokenSent,
uint256 amount,

address relayer, // relayer is unused

bytes memory message

) external {

if (msg.sender != acrossSpokePool) revert Unauthorized();

if (tokenSent != weth) revert NotWETH();

(address payable user, bool sendWETH) = abi.decode(message, (address, bool));

if (sendWETH) {

// Transfer WETH directly to user address.

IERC20(weth).safeTransfer(user, amount);

} else {

weth.withdraw(amount);

// Transfer the funds to the user via low-level call.

(bool success,) = user.call{value: amount}("");

if (!success) revert FailedETHTransfer();

// Required to receive ETH from the WETH contract.

receive() external payable {}

The deposit recipient should be set to your custom handler contract's address on the destination chain. Generating the Deposit is identical to
process described above.

Reverting Transactions

​ If the recipient contract's handleV3AcrossMessage function reverts when message , tokenSent, amount, and relayer are passed to it, then the
fill on destination will fail and cannot occur. In this case the deposit will expire when the destination SpokePool timestamp exceeds the
deposit fillDeadline timestamp, the depositor will be refunded on the originChainId. Ensure that the depositor address on the origin
SpokePool is capable of receiving refunds.
​ It is possible to update the message using speedUpDepositV3but it requires the original depositor address to create a signature, which may
not be possible if the depositor is not an EOA or a smart contract capable of creating ERC1271 signatures.
​ If the recipient address is not a contract on the destination chain, then the fillRelay transaction will not attempt to pass calldata to it.
​ Consider implementing fallback logic in your custom handler contract to transfer funds in the case of a transaction revert based on your use
case.

Summarized Requirements

​ The deposit message is not empty

​ The recipient address is your custom handler contract on the destinationChainId

​ Construct your message

​ Use the Across API to get an estimate of the relayerFeePct you should set for your message and recipient combination

​ Call depositV3() passing in your message

​ Once the relayer calls fillV3Relay() on the destination, the recipient's handler contract's handleV3AcrossMessage will be executed

​ The additional gas cost to execute the above function is compensated for in the deposit's relayerFeePct

Security & Safety Considerations


​ Avoid making unvalidated assumptions about the message data supplied to handleV3AcrossMessage(). Across does not guarantee message
integrity, only that a relayer who spoofs a message will not be repaid by Across. If integrity is required, integrators should consider including a
depositor signature in the message for additional verification. Message data should otherwise be treated as spoofable and untrusted for use
beyond directing the funds passed along with it.
​ Avoid embedding assumptions about the transfer token or transfer amount into their messages. Instead use the tokenSent and amount
variables supplied with to the handleV3AcrossMessage() function. These fields are enforced by the SpokePool contract and so can be
assumed to be correct by the recipient contract.
​ The relayer(s) able to complete a fill can be restricted by storing an approved set of addresses in a mapping and validating the
handleV3AcrossMessage() relayer parameter. This implies a trust relationship with one or more relayers.

Cross-chain Actions UI Guide


This UI guide is a counterpart to the technical Cross-chain Actions Integration Guide and is intended to offer UI and UX patterns developers can
implement directly within their core application CTAs.

Best Practices

​ Embed a source chain selector directly within the UI component where users perform an action.
​ Default the source chain selector to the balance where the user has the most funds of the input asset. The destination chain is the network
where the user is interacting with your protocol.
​ Don't require users to go to wallet, switch network, then return to app to perform an action. For example, if the user selects a source chain
that is not the network they are currently connected to:
​ 1: user clicks the button to perform the action ("confirm")
​ 2: wallet prompts user to switch network to the source chain
​ 3: wallet prompts user to execute the cross-chain action (depositing into Across)

Example Flow

This example app is a borrow/lend protocol where the user selects the vault on Optimism they want to deposit into. The source chain selector is
embedded directly within the deposit flow removing the need for a user to go to a separate page or bridge to use funds from their desired chain.
User selects to use their funds on Arbitrum.

User inputs the amount they wish to send.

Since the user is currently connected to Ethereum, but want to use funds on Arbitrum to deposit into the Optimism vault, they will need to switch to
Arbitrum and then confirm the deposit.
After switching the wallet network to Arbitrum the user is immediately prompted to confirm the transaction, without needing to go back to the app.
Once confirming the user's assets are transferred from Arbitrum to Optimism and deposited into the vault, atomically.

Transfers in Across are executed extremely fast, typically under 5 seconds and providing the user with a timer to completion provides a magical UX
and reduces the uncertainty they experience when bridging.

Settle Cross-chain Intents


The future of interoperability is intents and Across Settlement is the only production-ready, modular settlement layer built to facilitate cross-chain
intents. The Across Bridge and Across+ products build on top of Across Settlement to offer bridging to users and bridge abstraction to dApp
developers.

With Across Settlement, any other application or source of cross-chain intent order flow can leverage Across Settlement to enable the fastest speeds
and lowest fees for their interoperability use-case.

Integrating Across Settlement into Your Application

This guide contains an examples for how to build on top of Across Settlement

If you have further questions or suggestions for this guide, please send a message to the #developer-questions channel in the Across Discord.

Across Settlement Example

Across Settlement is a very flexible system, so this example won't capture all that you can do. However, it should illustrate the types of integrations
that are possible.
Architecture

The example application we're going to build has the following architecture:

​ User asks for a quote on a cross-chain swap from an offchain RFQ system.
​ Market-maker-relayers bid on the opportunity to fill the user.
​ The user signs the winning bid as a Permit2 order.
​ The Permit2 order is passed to the winning market-maker-relayer.
​ The market-maker-relayer sends a transaction where this order is put onchain.
​ In that transaction, the user's funds are pulled via Permit2. A bond is submitted by the market-maker-relayer in the transaction as well in
the same currency as the user's funds.
​ An Across deposit is created. The bond + user funds are set as the input amount. The market-maker-relayer is set as the exclusive
relayer, so they are the only relayer who can submit the relay on the destination.

This tutorial will not implement the offchain components, but it will show a sample contract implementation for the contract that processes the Permit2
order.
Sample Implementation

Note: the implementation below has not been vetted, validated, or optimized in any way. It is purely meant as an illustrative example. Use this sample
code at your own risk. See the comments in the code to understand what each portion of the code is doing.
Copy
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeTransferFrom.sol";

// Across SpokePool Interface

interface AcrossV3SpokePool {

function depositV3(

address depositor,

address recipient,

address inputToken,

address outputToken,

uint256 inputAmount,

uint256 outputAmount,

uint256 destinationChainId,

address exclusiveRelayer,

uint32 quoteTimestamp,

uint32 fillDeadline,

uint32 exclusivityDeadline,

bytes calldata message

) external;

// Sample cross-chain order

struct CrossChainOrder {

address rfqContract;

address swapper;

address relayer;

address recipient;

address inputToken;

address outputToken;

uint256 inputAmount;

uint256 relayerBondAmount;

uint256 outputAmount;

uint256 destinationChainId;
uint256 nonce;

uint256 deadline;

// A typestring is required for Permit2 to be able to process the order as witness data.

string constant ORDER_WITNESS_TYPESTRING = "CrossChainOrder witness)CrossChainOrder(address rfqContract,address swapper,address


relayer,address recipient,address inputToken,address outputToken,uint256 inputAmount,uint256 relayerBondAmount,uint256 outputAmount,uint256
destinationChainId,uint256 nonce,uint256 deadline)";

contract RFQSwapper {

using SafeERC20 for IERC20;

IPermit2 public immutable permit2;

AcrossV3SpokePool public immutable spokePool;

constructor(address _permit2, address _spokePool) {

permit2 = Permit2(_permit2);

spokePool = AcrossV3SpokePool(_spokePool);

// The initiate function:

// 1. Takes in an order and swapper signature.

// 2. Verifies the signature and order validity.

// 3. Passes along the tokens and instructions to the Across SpokePool

// where the order will be filled, that fill verified, and then settled.

function initiate(CrossChainOrder memory order, bytes calldata signature) {

// This code is the basic process for validating and pulling in tokens

// for a Permit2-based order.

// It's somewhat involved, but it does most of the work in validating the

// order and signature.

permit2.permitWitnessTransferFrom(

// PermitTransferFrom struct init

IPermit2.PermitTransferFrom({

permitted: IPermit2.TokenPermissions({

token: order.inputToken,

amount: order.inputAmount

}),

nonce: order.nonce,

deadline: order.deadline

}),

// SignatureTransferDetails struct init

IPermit2.SignatureTransferDetails({

to: address(this),
requestedAmount: order.inputAmount

}),

order.swapper,

keccak256(abi.encode(order));

ORDER_WITNESS_TYPESTRING,

signature

);

// Pull in the bond from the msg.sender.

IERC20(order.inputToken).safeTransferFrom(msg.sender, order.relayerBondAmount);

// Full input amount for Across's purposes both the user amount

// and the bond amount.

// In the case that the relay is filled correctly, the relayer

// gets the full input amount (including the bond).

// In the case that the depositor is refunded, they receive the bond

// as compensation for the relayer's failure to fill.

uint256 amount = order.inputAmount + order.relayerBondAmount;

// Now that all the tokens are in this contract, Across contract needs

// to be approved to pull the tokens from here.

IERC20(order.inputToken).safeIncreaseAllowance(address(spokePool), amount);

// Fill deadline is arbitrarily set to 1 hour after initiation.

uint256 fillDeadline = block.timestamp + 3600;

// Call deposit to pass the order off to Across Settlement.

spokePool.deposit(

order.swapper,

order.recipient,

order.inputToken,

order.outputToken,

order.inputAmount,

order.outputAmount,

order.destinationChainId,

order.relayer,

block.timestamp,

fillDeadline, // 1 hour deadline

fillDeadline, // Exclusivity for the entire fill period

"" // No message

);
}

}
Conclusion

As is shown in the example above, Across Settlement can be used as a tool to verify cross-chain intent fulfillment in many different setups. The
primary advantage is that the integrating protocol can abstract away the complexity of the verification and fulfillment, so they don't have to deal with
the security, timing, and asynchrony challenges of building a cross-chain system.

Across Settlement provides additional advantages over naive cross-chain settlement systems, which ultimately lead to better execution of intent
fulfillment for users and relayers:

​ Aggregated and Optimistic Verification: The Across Settlement system aggregates valid fills events off-chain to create a repayment
bundle, which is then optimistically verified by UMA's Optimistic Oracle. This verification and repayment mechanism scales gas cost of
repayment at O(1) instead of O(N) with the number of fills. This offers an order of magnitude in gas savings vs. other approaches and
ultimately leads to better pricing for users and more profit for relayers.
​ Relayer Cross-chain Management: With Across’ settlement architecture, repayment is made on the relayer’s chain of choice, reducing
overhead and complexity of managing cross-chain positions. This lowers costs for relayers, enabling better pricing and execution for
end-users. It is enabled by Across’ Hub and Spoke model, where passive LPs extend loans to relayers for taking on time-value risk as
funds are rebalanced through canonical bridges by the protocol.

What are Cross-chain Intents?


What are Intents?

An intent is a type of order where a user specifies an outcome instead of an execution path. Intents can be single-chain, or cross-chain where the
user's desired outcomes is on a different chain than input assets. Across is only focused on cross-chain intents.

We see the progression of cross-chain intents in the following way:

Phase 1: Users' intent specifies moving the same asset from Chain A to Chain B

​ This is what Across started with and has been supporting since 2021.

Phase 2: Users' intent specifies moving the same asset from Chain A to Chain B and then executing a transaction on Chain B

​ This is enabled in Across by new functionality to embed instructions on the origin deposit to be executed on the destination.

Phase 3: Users' intent specifies swapping asset X on Chain A for a minimum amount of asset Y on Chain B and then executing a
transaction on Chain B

​ Across Settlement supports these types of orders now. See Intent Structure in Across.

ERC 7683

To grow the adoption of interoperability solutions powered by intents, Across is working on defining a cross-chain intent order standard, based on the
concepts outlined in Phase 3 above. This will enable a unified way to ingest, bid on, escrow and settle user intents. See more at erc7683.org.

Intents Architecture in Across


Across' Intents Architecture

Across' cross-chain intents architecture can be distilled into a 3-layered system: a request for quote mechanism to house users' intents, enabling a
competitive network of relayers to bid, claim and fill those orders, and lastly a settlement layer to verify intent fulfillment and repay relayers.

Below is a diagram of the planned architecture of Across after implementing a RFQ price auction to enable gasless orders and cross-chain swaps:
Users Request Quotes to Fill their Intent

​ User receives a quote from a relayer to fill their order, and signs (no onchain transaction).
Across' current RFQ implementation does not include gasless orders or cross-chain swaps via a RFQ as depicted in steps 1, 2a, 2b, though these are
planned upgrades.

Across' quoting currently has fixed fees and relayer competition is strictly based on a speed.

All other steps are identical to Across' current architecture.

Relayer Network Fills User

​ (a) Relayer claims the order and executes the sign order (b) bringing the transaction on-chain and the users' assets are escrowed via into
the SpokePool. The structure of Across orders can be found in Intent Lifecycle in Across.
​ (a) Relayer calls fillRelayV3 on the destination SpokePool with their own assets which (b) are then transferred to the user. During this
step relayers also specify which chain to take repayment on.

Settlement System Verifies Fills and Repays Relayer

​ Over a 60 minute window, the Dataworker ingests deposit events, matches them to valid fill events (i.e. fills that meet the intent order
requirements). All valid fills are aggregated into a relayer repayment "bundle" and optimistically proposed for verification.
​ If no disputes occur during the challenge period, the Dataworker executes the bundle on the HubPool which then routes repayment
instructions to the various SpokePools to repay relayers.
​ Relayers are repaid after a short delay.

Modular Intents Settlement Layer

RFQ systems can and will be external to Across, and will have different mechanics than the Across RFQ.

​ Across implements a specific type of RFQ for the Across Bridge, but any other auction mechanism that produces a transaction or signed
order recognized by the Across SpokePool is supported in Across' Settlement Layer.

Relayers compete to fill intent order flow and are external to Across.

​ Risk Labs (the team building Across) builds and runs an open source implementation of a relayer to support the Across Bridge and other
intent systems, and to accelerate the expansion of the relayer network.
​ Relayers subscribe to and fill orders from multiple systems, have different service offerings (e.g. same-asset transfers vs. cross-chain swaps)
and different profit motives.

Settlement is the core offering and advantage of Across' architecture.

Across can accept any cross-chain intent based order flow and provide settlement (escrow, verification and repayment). The order only needs to be
able to be translated into a structure SpokePools recognize. Across Settlement provides two core advantages, which ultimately leads to better
execution of intent fulfillment for users and relayers:

​ Aggregated and Optimistic Verification: As described in steps 4-6 in the above diagram, Across Settlement system aggregates valid fills
events off-chain to create a repayment bundle, which is then optimistically verified by UMA's Optimistic Oracle. This verification and
repayment mechanism scales gas cost of repayment at O(1) instead of O(N) with the number of fills. This offers an order of magnitude in gas
savings vs. other approaches and ultimately leads to better pricing for users and more profit for relayers.
​ Relayer Cross-chain Management: With Across' settlement architecture, repayment is made on the relayer’s chain of choice, reducing
overhead and complexity of managing cross-chain positions. This lowers costs for relayers enabling better pricing and execution for
end-users. It is enabled by Across' Hub and Spoke model, where passive LPs extend loans to relayers for taking on time-value risk as funds
are rebalanced through canonical bridges by the protocol.

Intent Lifecycle in Across


Intents as a Building Block

Across is used directly by end-users, but it can also be used by other protocols to ensure their user intents are fulfilled. The Across intent structure
and lifecycle described below is general enough to serve as a settlement and communication layer for many use cases.

Initiation

The initiation process involves 3 basic steps:

​ depositV3 is called on the SpokePool. This call can be made directly by a user, but could also be called on behalf of a user by some other
smart contract system. The chain where this call happens is called the origin chain for this intent. The intent specifies the destination
chain, which is where the user wants to receive the output.
​ The user's are pulled into the SpokePool from the caller. These funds are escrowed until the intent is fulfilled, at which point they can be
released. Because cross-chain intents are not atomic, user funds must be escrowed before the relayer can safely fulfil the intent.
​ The SpokePool emits the V3FundsDeposited event. Relayers can subscribe to this event to identify intents that they can fill.

These steps are intended to be a primitive that can fit into almost any system that requires cross-chain transfers. For instance, this design allows for
gasless order systems, where the user simply signs the order and the filler brings it on chain. In such a system, the relayer may be preselected in an
offchain auction to minimize the user's cost.

Fill

After initiation, a relayer must fulfil the user's intent. This process involves three distinct actions:

​ fillV3Relay is called on the SpokePool contract deployed on the destination chain. In this call, the relayer specifies on which chain they
would like to receive the user's input tokens. The LP Fee that Across charges on input tokens depends on this choice. Generally, if the
relayer takes the input tokens on the chain where the user deposited them, the fee is smallest (if not 0).
​ The intent is marked as filled in the SpokePool. This prevents a second relayer from filling the same intent a second time.

​ The SpokePool emits the FilledV3Relay event. These events can be used to track the status of intents being settled by the system. They
are also used to track

Note: intents can have an exclusivity period whereby a particular relayer address has the sole right to perform the fill during that period.

Slow Fill or Expiration (if no fill)

In the (rare) case where a fill doesn't happen, there are two fallbacks: an expiration or a slow fill.

A slow fill means that the Across system fills the user without requiring a relayer to provide the capital. It's called a slow fill because it requires Across
to optimistically verify this fill before executing it, which means the fill happens a few hours after initiation (much longer than a typical fill). A slow fill
happens when the following conditions are met:

​ The input token and output token are the same asset.
​ requestV3SlowFill is called on the destination chain before the expiration time for the intent.
​ The slow fill is executed before any relayer fills it or the intent expires.

In cases where a slow fill can't or does not happen and a relayer does not fill the intent, the intent expires. Like a slow fill, this expiry must be be
optimistically verified, which takes a few hours. Once this verification is done, the user is then refunded their money on the origin chain.

Settlement

Once an intent has been fulfilled, Across verifies that fulfillment and releases the input tokens to the relayer. Across does this by periodically verifying
a bundle of intents. The general process for producing, verifying, and executing a bundle is:

​ A block range is determined on each chain supported by Across. This block range goes from the end of the previous bundle's range to a
recent block chosen by the proposer.
​ All fill or slow fill request events in the range are validated by ensuring they match some deposit event on the origin chain.
​ All valid fills, slow fill requests, and intent expirations are combined to determine an aggregated list of payments that need to be made on
each chain. Those payments are included in the bundle.
​ If funds need to be moved between chains to make these payments, those transfer instructions are included in the bundle.
​ These payments and transfers are organized into a series of data structures called merkle trees whose roots are then proposed on chain
to the HubPool along with a bond.
​ Once this proposal passes the challenge period without being disputed, the bundle execution can begin: these roots are sent from the
HubPool to each chain's SpokePool via canonical bridges. Funds are also transferred according to the bundle instructions in this step.
​ Once these roots arrive, anyone can execute them to make the payments determined in step 3.

Intent Structure in Across

An intent in Across is essentially a struct (set of values) that specifies what the user expects to happen. The user's funds are only released once
Across verifies that this intent was satisfied as specified.

The basic fields are:


Field Description

recipient the address that should receive the funds

inputToken the token that the user supplies

inputAmount the amount of the inputToken the user supplies

outputToken the token that the user wants to receive

outputAmount the amount of the output token the user wants to receive

destinationChainId where the user wants to receive the output tokens

fillDeadline deadline for the user to receive the tokens

custom data that is sent to the recipient if it's a contract; this allows
message
for custom actions to be executed on the destination chain

Advanced fields:
Field Description

exclusiveRelayer a preselected relayer who is given the exclusive right to fill the user

deadline for the exclusive relayer to perform the fill before it is


exclusivityDeadline
opened to other fillers

Canonical Asset Maximalism


Canonical assets are the only secure way to represent value across chains. Intent bridges like Across Bridge enable interoperability with canonical
assets at faster speeds and lower fees than 3rd party messaging bridges without the security tradeoffs.

Understanding Canonical Assets

Canonical assets refer to the original or "native" form of a token on its home blockchain. In the context of interoperability, tokens can either be
"canonical" or "representative." The distinction generally comes down to the trust assumptions in securing assets on secondary chains.

How Assets Move between Chains

Regardless of chain, interoperability protocol or technology, there is only a singular way to "move" a token from its native chain to a secondary chain
(and it's not really moving at all):

​ locking (or burning) the token on its native chain (the origin)
​ sending a message from the origin chain to the destination chain after the locking (or burning) is complete
​ minting a new token on the destination chain

The critical component in the above flow is step #2, and specifically the verification process ensuring the message is not faulty. The verification
method used is precisely what distinguishes "canonical" vs. "representative" assets. At the highest level there are two verification methods:

​ Via the canonical bridge: Canonical bridge contracts, which already underpin the security of an L2 chain that inherits its security from
mainnet Ethereum in the most trust minimized way, is responsible for verifying messages to mint tokens. These are considered
"canonical" tokens given they do not increase trust assumptions vs. simply using the L2 chain. Similar "trustless" verification mechanisms
may exist from Ethereum to other Alt L1s.
​ Via a 3rd party message bridge: A 3rd party bridge, which can have any number of trust models, is responsible for verifying messages
to mint tokens. These are considered "representative" tokens given they do increase trust assumptions, as users now have to trust a 3rd
party to never allow a faulty message (and if they do, it could result in an infinite mint and unbacked token on the secondary chain).

Canonical Asset Maximalism

In a trust minimized world, only canonical bridges, and only canonical assets would be used. But the current speed of canonical bridges (1 hour to 7
days) makes them an untenable solution for many applications. 3rd party message bridges can be much faster, but they come with security tradeoffs
inherent in minting representative assets.

Across' intent-based bridge introduces a new architecture in interoperability that doesn't suffer from the security hurdles of 3rd party message bridges,
and is empirically faster and cheaper than canonical bridges. Across achieves this by inserting a 3rd party relayer to quickly fulfill users' bridging
requests using their own inventory of canonical assets, and a settlement layer that sits on top of canonical bridges to slowly verify and repay relayers.
In other words, Across' intent-bridge decouples the urgent need of fast-filling users from the eventual need of verification. Users and developers don't
need to make the trust vs. convenience trade-off in canonical vs. representative assets: intent systems like Across offer the best of both worlds.

Diagrams inspired by the insightful work from the Connext Network team.

API Reference
Source Code

The API is designed to be run serverlessly (without storing state) and is a wrapper on top of the SDK. See full implementation here.

Caching & Liveness

Users of the Across API are requested to cache results for no longer than 300 seconds.

The Across API serves data that is derived from the on-chain state of the Across contracts and relayer bots. The on-chain state is subject to change
each block, and cached data can quickly become invalid as a result.

The exception is the /deposit/status endpoint which is implemented in this stateful repository because it relies on an indexing solution.

API Endpoints
The token query parameter in the /limits and /suggested-fees endpoint was scheduled to be deprecated following the migration to Across V3, and it is
no longer officially supported. Legacy users should instead specify inputToken and outputToken.

Retrieve suggested fee quote for a deposit.


Returns suggested fees based inputToken+outputToken, originChainId, destinationChainId, and amount.

GEThttps://app.across.to/api/suggested-fees
Query parameters
Response
200400500
Suggested fees for the transaction
Body
application/json
totalRelayFeeobject
relayerCapitalFeeobject
relayerGasFeeobject
lpFeeobject
timestampstring
The quote timestamp that was used to compute the lpFeePct. To pay the quoted LP fee, the user would need to pass this quote timestamp to the
protocol when sending their bridge transaction.
Example: "1708047000"
isAmountTooLowboolean
Is the input amount below the minimum transfer amount.
Example: false
quoteBlockstring
The block used associated with this quote, used to compute lpFeePct.
Example: "19237525"
spokePoolAddressstring
The contract address of the origin SpokePool.
Example: "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A"
exclusiveRelayerstring
The relayer that is suggested to be set as the exclusive relayer for in the depositV3 call for the fastest fill. Note: when set to
"0x0000000000000000000000000000000000000000", relayer exclusivity will be disabled. This value is returned in cases where using an exclusive
relayer is not recommended.
Example: "0x428AB2BA90Eba0a4Be7aF34C9Ac451ab061AC010"
exclusivityDeadlinestring
The suggested exclusivity period (in seconds) the exclusive relayer should be given to fill before other relayers are allowed to take the fill. Note: when
set to "0", relayer exclusivity will be disabled. This value is returned in cases where using an exclusive relayer is not reccomended.
Example: "10"
Request
Curl
Copy
curl -L \

'https://app.across.to/api/suggested-fees?inputToken=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&outputToken=0x42000000000000000
00000000000000000000006&originChainId=1&destinationChainId=10&amount=1000000000000000000'

Test it
Response
Copy
{

"totalRelayFee": {

"pct": "376607094864283",

"total": "376607094864283"

},

"relayerCapitalFee": {

"pct": "100200000000000",

"total": "100200000000000"

},

"relayerGasFee": {

"pct": "276407094864283",

"total": "276407094864283"

},

"lpFee": {

"pct": "4552495218411721",

"total": "4552495218411721"

},

"timestamp": "1708047000",

"isAmountTooLow": false,

"quoteBlock": "19237525",
"spokePoolAddress": "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A",

"exclusiveRelayer": "0x428AB2BA90Eba0a4Be7aF34C9Ac451ab061AC010",

"exclusivityDeadline": "10"

}
Retrieve current transfer limits of the system
Returns transfer limits for inputToken+outputToken, originChainId, and destinationChainId.

GEThttps://app.across.to/api/limits
Query parameters
Response
200400500
Transfer limits
Body
application/json
minDepositstring
The minimum deposit size in the tokens' units. Note: USDC has 6 decimals, so this value would be the number of USDC multiplied by 1e6. For
WETH, that would be 1e18.
Example: 7799819
maxDepositstring
The maximum deposit size in the tokens' units. Note: The formatting of this number is the same as minDeposit.
Example: 22287428516241
maxDepositInstantstring
The max deposit size that can be relayed "instantly" on the destination chain. Instantly means that there is relayer capital readily available and that a
relayer is expected to relay within seconds to 5 minutes of the deposit.
Example: 201958902363
maxDepositShortDelaystring
The max deposit size that can be relayed with a "short delay" on the destination chain. This means that there is relayer capital available on mainnet
and that a relayer will immediately begin moving that capital over the canonical bridge to relay the deposit. Depending on the chain, the time for this
can vary. Polygon is the worst case where it can take between 20 and 35 minutes for the relayer to receive the funds and relay. Arbitrum is much
faster, with a range between 5 and 15 minutes. Note: if the transfer size is greater than this, the estimate should be between 2-4 hours for a slow relay
to be processed from the mainnet pool.
Example: 2045367713809
recommendedDepositInstantstring
The recommended deposit size that can be relayed "instantly" on the destination chain. Instantly means that there is relayer capital readily available
and that a relayer is expected to relay within seconds to 5 minutes of the deposit. Value is in the smallest unit of the respective token.
Example: 2045367713809
Request
Curl
Copy
curl -L \

'https://app.across.to/api/limits?inputToken=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&outputToken=0x42000000000000000000000000
00000000000006&originChainId=1&destinationChainId=10'

Test it
Response
Copy
{

"minDeposit": 7799819,

"maxDeposit": 22287428516241,

"maxDepositInstant": 201958902363,

"maxDepositShortDelay": 2045367713809,

"recommendedDepositInstant": 2045367713809

}
Retrieve available routes for transfers
Returns available routes based on specified parameters.

GEThttps://app.across.to/api/available-routes
Query parameters
Response
200400500
List of available routes
Body
application/json
originChainIdstring
Chain ID of the originating chain.
Example: 1
destinationChainIdstring
Chain ID of the destination chain.
Example: 10
originTokenstring
Origin chain address of token contract to transfer.
Example: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
destinationTokenstring
Destination chain address of token contract to receive.
Example: "0x4200000000000000000000000000000000000006"
originTokenSymbolstring
The symbol of the origin token to transfer.
Example: "WETH"
destinationTokenSymbolstring
The symbol of the destination token to receive.
Example: "WETH"
Request
JavaScriptCurl
Copy
const response = await fetch('https://app.across.to/api/available-routes', {

method: 'GET',

headers: {},

});

const data = await response.json();


Test it
Response
Copy
[

"originChainId": 1,

"destinationChainId": 10,

"originToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",

"destinationToken": "0x4200000000000000000000000000000000000006",

"originTokenSymbol": "WETH",

"destinationTokenSymbol": "WETH"

]
Track the lifecycle of a deposit
Returns the fill status of a deposit along with a corresponding fill transaction hash if filled.

This endpoint loads data queried by an indexing service that polls relevant events on a 10-second cadence. Users should therefore expect an

average latency of 1 to 15 seconds after submitting a deposit to see the status changed in this endpoint. This delay comes from the time it takes for

the internal indexing to include the deposit transaction.

GEThttps://app.across.to/api/deposit/status
Query parameters
Response
200
Lifecycle of a transaction
Body
application/json
fillStatusenum
The status of the deposit.

● filled: Deposits with this status have been filled on the destination chain and the recipient should have received funds. A FilledV3Relay event
was emitted on the destination chain SpokePool.
● pending: Deposit has not been filled yet.
● expired: Deposit has expired and will not be filled. Expired deposits will be refunded to the depositor on the originChainId in the next batch
of repayments.
filledpendingexpired
fillTxHashstring
The transaction hash of the fill transaction on the destination chain. This field is only present when fillStatus is filled.
destinationChainIdinteger
The chain id where the fill transaction will take place.
Request
JavaScriptCurl
Copy
const response = await fetch('https://app.across.to/api/deposit/status', {

method: 'GET',

headers: {},

});

const data = await response.json();


Test it
Response
Copy
{

"fillStatus": "filled",

"fillTxHash": "text"

SDK Reference
About the SDK

The Across SDK is written and maintained by the engineering team at Risk Labs.

It is written in typescript and available on NPM at @across-protocol/sdk-v2. It's compatible with both Node JS environments and in the browser.

How can I use the SDK?

The SDK can be used currently to query suggested deposit fees and limits, and get liquidity pool statistics. It is imported and used in the API's
implementation.

If I want to integrate Across into my dApp, should I use the SDK or the API?

We recommend using the API, which wraps SDK functions and has an easier interface. However, if speed is a concern then we recommend reviewing
the API implementation of the SDK to understand best how to use the SDK.

Installation

To add the SDK to your project, use npm or yarn to npm install @across-protocol/sdk-v2 or yarn add @across-protocol/sdk-v2.

This can be used either in a frontend application or a node js project.

Basic Usage

You can read about the different SDK modules on the Github README page. For convenience, the available modules are:

​ lpFeeCalculator: Get liquidity provider fee that will be charged on deposit for its quoteTimestamp

​ relayFeeCalculator: Get suggested relayerFeePct for a deposit, which accounts for opportunity cost of capital and gas costs. If the depositor
opts to set this fee lower than the suggested fee, then there is a chance that the deposit goes unfilled for a long time.
​ pool: Get HubPool statistics, such as available liquidReserves that be used to refund relayers and estimatedApy for liquidity providers.

Selected Contract Functions


Detailed contract interfaces for depositors.

SpokePool state-modifying functions

The spoke pool contract is where deposits are originated and fulfilled and is deployed on all chains that Across supports.
deposit
Using deposit will not allow the caller to specify a different outputToken than inputToken. This means that following the CCTP upgrade, deposit
callers will not be able to set inputToken = USDC and outputToken = Bridged USDC.

This function is backwards compatible with the existing function with the same name. The main difference in how to use this function is that the
relayerFeePct should now be set equal to the LP fee + relayer fee.
This is because the concept of the realizedLpFeePct is no longer brought on-chain by the relayer who used to call fillRelay() to fill this deposit.
deposit() now emits a V3FundsDeposited event which can only be filled by fillV3Relay() and these fills get charged the LP fee at refund time.
Therefore, the caller of deposit() is responsible for setting the relayerFeePct high enough that it covers both the relayer fees and the LP fees when
converted into an outputAmount.

See Tracking Events to understand more how the new events are emitted by deposit().
depositV3

This triggers a deposit request of tokens to another chain with the following parameters. The originChainId is automatically set to the chain ID on
which the SpokePool is deployed. For example, sending a deposit from the Optimism_SpokePool will set its originChainId equal to 10.

Note on sending ETH: deposit is a payable function meaning it is possible to send native ETH instead of wrapped ETH (i.e. WETH). If you choose to
send ETH, you must set msg.value equal to amount.

Note on receiving ETH: EOA recipients will always receive ETH while contracts will always receive WETH, regardless of whether ETH or WETH is
deposited.

Note on setting inputToken and outputToken: the list of currently supported tokens is available on our API, which you can test in-browser here. These
values generally must be set equal to ERC20 addresses on the origin and destination chain respectively, so in the case of bridging ETH this should be
set to a WETH address.

Note on approvals: the caller must approve the SpokePool to transfer amount of tokens.

Note on inputAmount limits: If the inputAmount is set too high, it can take a while for the deposit to be filled depending on available relayer liquidity. If
the outputAmount is set too high, it can be unprofitable to relay. Query the suggested max and min limits here. The contracts will not revert if the
outputAmount is set outside of the recommended range, so it is highly recommended to set outputAmount within the suggested limits to avoid locking
up funds for an unexpected length of time. The recommended outputAmount will be equal to inputAmount * ( 1 - relayerFeePct - lpFeePct).
If you are using the API to set the outputAmount then you should set it equal to inputAmount * (1 - fees.totalRelayFee.pct) where
fees.totalRelayFee is returned by the /suggested-fees endpoint.

Note on setting quoteTimestamp:

​ Call the read-only function getCurrentTime() to get the current UNIX timestamp on the origin chain. e.g. this could return: 1665418548.

​ Call the read-only function depositQuoteTimeBuffer() to get the buffer around the current time that the quoteTimestamp must be set to.
e.g. this could return: 600.
​ quoteTimestamp must be <= currentTime + buffer and >= currentTime - buffer.

Type Name Explanation

The account credited with the deposit, but not necessarily the
address depositor one who has to send inputTokens to the contract if the
msg.sender differs from this address.

The account receiving funds on the destination chain. Can be


an EOA or a contract. If the output token is the wrapped native
address recipient
token for the chain, then the recipient will receive native token if
an EOA or wrapped native token if a contract.

The token pulled from the caller's account and locked into this
contract to initiate the deposit. If this is equal to the wrapped
address inputToken
native token then the caller can optionally pass in native token
as * msg.value, as long as msg.value = inputTokenAmount.
The token that the relayer will send to the recipient on the
destination chain. Must be an ERC20. Note, this can be set to
address outputToken
the zero address (0x0) in which case, fillers will replace this with
the destination chain equivalent of the input token.

The amount of input tokens to pull from the caller's account and
lock into this contract. This amount will be sent to the relayer on
uint256 inputAmount
their repayment chain of choice as a refund following an
optimistic challenge window in the HubPool, less a system fee.

The amount of output tokens that the relayer will send to the
uint256 outputAmount
recipient on the destination.

The destination chain identifier. Must be enabled along with the


uint256 destinationChainId input token as a valid deposit route from this spoke pool or this
transaction will revert.

The relayer that will be exclusively allowed to fill this deposit


before the exclusivity deadline timestamp. This must be a valid,
non-zero address if the exclusivity deadline is greater than the
address exclusiveRelayer
current block.timestamp. If the exclusivity deadline is <
currentTime, then this must be address(0), and vice versa if this
is address(0).

Timestamp of deposit. Used by relayers to compute the LP fee


uint32 quoteTimestamp % for the deposit. Must be withindepositQuoteTimeBuffer() of
the current time.

The deadline for the relayer to fill the deposit. After this
destination chain timestamp, the fill will revert on the destination
uint32 fillDeadline chain. Must be set between [currentTime, currentTime +
fillDeadlineBuffer] where currentTime is block.timestamp on this
chain or this transaction will revert.
The deadline for the exclusive relayer to fill the deposit. After
this destination chain timestamp, anyone can fill this deposit on
uint32 exclusivityDeadline the destination chain. If exclusiveRelayer is set to address(0),
then this also must be set to 0, (and vice versa), otherwise this
must be set >= current time.

Data that can be passed to the recipient if it is a contract. If no


message is to be sent, set this field to an empty bytes array:
bytes message ""(i.e. bytes` of length 0, or the "empty string"). See
Composable Bridging for examples on how messaging can be
used.

speedUpDepositV3

Some of a pending deposit's parameters can be modified by calling this function. If a deposit has been completed already, this function will not revert
but it won't be able to be filled anymore with the updated params.

It is the responsibility of the depositor to verify that the deposit has not been fully filled before calling this function.A depositor can request
modifications by signing a hash containing the updated details and information uniquely identifying the deposit to relay. This information ensures that
this signature cannot be re-used for other deposits.We use the EIP-712 standard for hashing and signing typed data. Specifically, we use the version
of the encoding known as "v4", as implemented by the JSON RPC method eth_signedTypedDataV4 in MetaMask.You can see how the message to be
signed is reconstructed in Solidity here.

Successfully calling this function will emit an event RequestedSpeedUpV3Deposit which can be used by relayers to fill the original deposit with the new
parameters.

Depositors should assume that the parameters emitted with the lowest updatedOutputAmount will be used, since they are incentivized to use the
highest fee possible. (Recall that the relayer's fee is derived by the difference between the inputAmount and the outputAmount). Any relayer can use
updated deposit parameters by calling fillV3RelayWithUpdatedDeposit instead of fillV3Relay.

Type Name Description

Sender of deposit to be sped up. Does not need to equal


address depositor
msg.sender

uint32 depositId UUID of deposit to be sped up

New output amount that depositor requests to receive.


uint256 updatedOutputAmount Should be lower than outputAmount to be taken seriously by
fillers

address updatedRecipient New recipient of deposit.

Updated data that is sent to updatedRecipient. As described


bytes updatedMessage in section above, this should be set to 0x for the forseeable
future.
bytes depositorSignature Signed message containing contents here​

Supported Chains
Mainnet Chains
Name Chain ID

Arbitrum 42161

Base 8453

Blast 81457

Ethereum 1

Linea 59144

Lisk 1135

Mode 34443

Optimism 10

Polygon 137

zkSync 324

Testnet Chains
Name Chain ID

Arbitrum 421614

Base 84532

Blast 168587773

Ethereum 11155111

Lisk 4202

Mode 919
Optimism 11155420

Polygon (Amoy) 80002

Fees in the System


Liquidity Provider Fees

We view using Across as being similar to lending protocols such as AAVE or Compound. When a user bridges a particular token from one chain to
another, the fast bridge isn't "moving tokens from one chain to another" but, rather, it is a relayer or the protocol itself providing tokens on the
destination chain in return for tokens on the origin chain. We choose to use a similar pricing model for our liquidity provider fees because of this
parallel.
Utilization based pricing

We base our pricing model on the one described in AAVE's documentation. Let,

​ X
​ Xdenote the size of a particular transaction someone is seeking to bridge
​ 0≤Ut≤1
​ 0≤U
​ t
​ ​

​ ≤1denote the utilization of the liquidity providers' capital prior to the transaction, i.e. the amount of the liquidity providers' capital that is in use
prior to the current transaction
​ 0≤U^t≤1
​ 0≤
​ U
​ ^
​ t
​ ​

​ ≤1denote the utilization of the liquidity providers' capital after to the transaction, i.e. the amount of the liquidity providers' capital that would
be in use if the user chose to execute their transaction
​ Uˉ
​ U
​ ˉ
​ denote the "kink utilization" where the slope on the interest rate changes
​ R0,R1,R2
​ R
​ 0
​ ​

​ ,R
​ 1
​ ​

​ ,R
​ 2
​ ​

​ denote the parameters governing the interest rate model slopes


​ R0
​ R
​ 0
​ ​

​ is the interest rate that would be charged at 0% utilization


​ R0+R1
​ R
​ 0
​ ​

​ +R
​ 1
​ ​

​ is the interest rate that would be charged at


​ Uˉ%
​ U
​ ˉ
​ %utilization
​ R0+R1+R2
​ R
​ 0
​ ​

​ +R
​ 1
​ ​

​ +R
​ 2
​ ​

​ is the interest rate that would be charged at 100% utilization

The (annualized) interest rate model is then defined by

R(Ut)=R0+min⁡(Uˉ,Ut)UˉR1+max⁡(0,Ut−Uˉ)1−UˉR2

R(U

)=R

min(

,U

)

1−

max(0,U

)

R
2

We calculate the (annualized) interest rate for a particular loan by aggregating the marginal increases of utilization by integrating over this function

Rta=1U^t−Ut∫UtU^tR(u)du

−U

1

R(u)du

The actual fee that is charged will be based on making a loan at this rate for a 1 week time-span. The rate that is charged can be computed from:

Rtw=(1+Rta)152−1

=(1+R

)
52

−1

and the fee would be

Fee=RtwX

Fee=R
t

We chose to charge prices this way to ensure that users are paying a "fair" price for the amount of utilization that their bridge transaction incurs.

Relayer Fees

Relayer fees play a similar role in the Across ecosystem as gas fees play in the Ethereum ecosystem. Relayer fees are a fee that the user sets to
incentivize relayers to relay your bridge transaction.

Relaying a transaction has three costs for relayers:

​ Gas fees: The relayer pays gas to perform the relay and to claim their repayment.
​ Capital opportunity costs: The fact that the relayer is using their capital to perform a relay means that they are not using it for other yield
opportunities.
​ Capital at risk: A relayer takes on certain risks by relaying funds. If they make a mistake when relaying that could jeopardize their
repayment of the capital they invested.

Both Liquidity Provider fees and Relayer fees are implied from the spread between inputAmount and outputAmount in a depositV3 transaction.

Actors in the System


User

A user is an actor who submits intent orders, in the form of a deposit order, to bridges assets between L2s and L1 with Across. Users pay relayers and
liquidity providers in order to send tokens instantly between networks.

Relayer

Relayers compete to fulfill users' deposit requests by sending depositors the specified output amount of the specified token on the specified
destination chain with optional data specifying required actions to execute on the destination.

Relayers extend short-term token loans to Users in exchange for fees. Users incentivize relayers to fill their order by the implied fee of the spread
between input amount and output amount specified in the users' order. At the time of fill, relayers request repayment on their desired repayment chain,
which can be any Across supported chain. Relayers are refunded the users' input amount less the realized LP fee at time of deposit, and depends on
the specified repayment chain.

Generally, relayers take on the following risks when fulfilling a deposit and are compensated accordingly by users:

​ Cost of capital: relayers send tokens to a user in order to fulfill a deposit and are reimbursed when their valid fill is included in a bundle
​ Gas costs: relayers pay gas on destination chains to fulfill deposits
​ Software risk: an honest relayer might have a bug in its software that sends an invalid fill
​ Finality risk: a deposit on the origin chain might disappear if the origin chain reorganizes

Running a relayer is permissionless. Risk labs provides open source, opinionated implementations of the Relayer but there are technically many ways
to customize the behavior of the software. Developers can run the open source relayer software or implement their own. You can find more
information about running relayers here.

Dataworker

Dataworkers support stability and healthy functioning of the system by refunding relayers and moving system assets between networks. Whitelisted
dataworkers are in charge of proposing "bundles" to be optimistically validated by the Across system. These proposers must post a bond when
proposing. Proposers can also be removed or changed by an Across DAO Governance vote. Anyone can dispute an invalid bundle and earn a
dispute bond, which includes part of the proposer's bond. Each bundle contains instructions for:

​ Refunding relayers for valid fills


​ Sending tokens to SpokePools that can be used to pay such refunds
​ Sending tokens to SpokePools that can be used to execute slow fills
​ Withdrawing funds originating from deposits on SpokePools back to the HubPool

The dataworker's responsibility is prescribed in UMIP-157 and extended by UMIP-179 and a reference implementation can be found here. An
example proposal transaction can be seen here.

Liquidity Provider

A liquidity provider or LP is an actor who deposits assets into one of the pools on Across.to/pool. All Across LP pools reside on mainnet, and LPs only
interact with these mainnet pools. Liquidity Providers provide the capital that enables the flexibility for relayers choose a repayment chain in exchange
for fees. Moreover, liquidity providers provide capital that can be used to fulfill deposits in the case that no relayers can fast fill the deposit.
Generally, liquidity providers take on the following risks when passively providing liquidity to Across:

​ Cost of capital: LP's must deposit their tokens into an Across contract on mainnet.
​ Liquidity risk: If Across is experiencing high demand then there is a chance that so much of the LP capital is reallocated to repaying
relayers, fulfilling deposits and being rebalanced through canonical bridges that not all LP's who want to withdraw can withdraw at the
current moment. This is a transitory situation as once funds are received from the canonical bridge LP's will be able to withdraw.

Security Model and Verification


The Across smart contracts plus the UMIP provide the fundamental rules of the Across Protocol. The ACROSS-V2 price in UMIP-157 and extended by
UMIP-179 precisely define the constraints and rules of the system. Actors in the system (relayer, dataworker) must behave in accordance to these
rules. The system is secured by optimistic verification of relayer repayments and rebalance instructions by UMA's Optimistic Oracle, requiring only a
single honest actor to dispute invalid proposals to keep the system secure.

Please refer to UMIP-157 and UMIP-179 for detailed information about system architecture, security model and verificaiton.

Disputing Root Bundles


About

Across requires proposals and disputes to be accompanied by a bond. This bond is returned if the proposal or dispute is correct, and is sacrificed if it
is found to be incorrect. This protects against attempts to incorrectly move funds, as well as spam and other denial of service attempts.The Across
Bond Token (ABT) is the bond collateral required by the HubPool contract. This is a WETH-like contract that is minted in return for depositing Ether,
and can be redeemed for the underlying Ether at any time. ABT implements custom ERC20 transferFrom() logic in order to limit the addresses that
are able to make HubPool root bundle proposals.

Manual Dispute Procedure

​ 1.Check the required bond token and amount (nominally 0.45 ABT) by calling bondToken() and bondAmount() on the HubPool.
​ 2.Mint the bond token as necessary by caling deposit() on the BondToken contract.
​ 3.Ensure that the HubPool has permission to pull the bond during the dispute. Increase the allowance as necessary by calling appprove()
on the BondToken contract. The address to approve is 0xc186fa914353c44b2e33ebe05f21846f1048beda.
​ 4.Call disputeRootBundle() on the HubPool.

Automated Dispute Procedure

The Across relayer-v2 repository contains a utility script that automates each of the above steps. Prerequisites are:

​ 1.The relayer-v2 package must be installed.


​ 2.The mnemonic for an EOA must be set in the relayer-v2 .env file.
​ 1.The configured EOA must be funded with at least 0.45 ABT or ETH (1 ABT == 1 ETH), plus additional ETH for gas to handle
the necessary deposit, approval and/or dispute transactions.
​ 2.It is sufficient for the entire amount to be held in ETH, since the dispute script automates the steps of minting ABT and
approving the HubPool to spend it.
​ 3.The actual amounts are subject to change based on the prevailing gas price at the time of the dispute, and the configured bond
amount.

Installation
Copy
$ git clone https://github.com/across-protocol/relayer-v2.git

$ cd relayer-v2

$ yarn install && yarn build

# Copy the predefined sample config and update the MNEMONIC variable in

# .env to match the relevant mnemonic.

$ cp .env.example .env

Execution
Copy
$ yarn dispute

# The dispute script will dump information about the Bond Token and

# latest HubPool proposal. If necessary, it will automatically mint the

# requisite amount of the bond token and will approve the HubPool to use
# it. At the conclusion, the script will provide the transaction hash of

# the most recent proposal and will request to re-run with the flag

# --txnHash <proposal-transaction-hash>

# Re-running the script with this additional argument will automatically

# submit a dispute.

$ yarn dispute --txnHash <proposal-transaction-hash>

Validating Root Bundles


Root bundles instruct the Across system on how to transfer funds between smart contracts on different chains to refund relayers and fulfill user
deposits.

This explainer video explains why root bundles are critical to making the Across system work and how they are validated. Root bundles are
optimistically validated and ultimately secured by the UMA Oracle. It is recommended that UMA voters and other actors in the UMA ecosystem have
an understanding of how Across utilizes the UMA oracle.

Tracking Events
This section contains hints for how you can implement your own managed service to track Across events throughout their lifecycle. As an alternative,
we also host a public managed solution that you can learn more about in the API section.

Using our API

See API reference for GET /deposit/status to query the status of a single deposit.

The recommended solution for tracking all Across deposits originating from your integration, for a single user, is to store the user's depositId and
originChainId from each transaction originating from your app, and then get the status of each via GET /deposit/status.

Using your own managed service


Implementation

Deposit and corresponding fill events are conveniently scraped by a database and available here. The database implementation can be found in this
repository.
How to detect the status of a deposit

When a user deposits capital to a SpokePool, a V3FundsDeposited event is emitted. This event is now emitted for both deposit() (legacy function
interface) and depositV3() calls. FundsDeposited, the V2 event, is no longer possible to emit.
Copy
event V3FundsDeposited(

address inputToken,

// Note: outputToken can be set to 0x0 (zero address) in which case, the filler

// should replace this address with the "equivalent" destination chain token

// as the input token. For example, if input token is USDC on chain A, then the output

// token should be the USDC token supported by Across on the destination chain

address outputToken,

uint256 inputAmount,

uint256 outputAmount,

uint256 indexed destinationChainId,

uint32 indexed depositId,

uint32 quoteTimestamp,

uint32 fillDeadline,

uint32 exclusivityDeadline,

address indexed depositor,


address recipient,

address exclusiveRelayer,

bytes message

This data comprises the new deposit's "RelayData" which is combined with the block.chainId of the SpokePool that emitted the event to form:
Copy
// This struct represents the data to fully specify a **unique** relay submitted on this chain.

// This data is hashed with the chainId() and saved by the SpokePool to prevent collisions and protect against

// replay attacks on other chains. If any portion of this data differs, the relay is considered to be

// completely distinct. See _getV3RelayHash() below for how the relay hash is derived from the relay data.

struct V3RelayData {

// The address that made the deposit on the origin chain.

address depositor;

// The recipient address on the destination chain.

address recipient;

// This is the exclusive relayer who can fill the deposit before the exclusivity deadline.

address exclusiveRelayer;

// Token that is deposited on origin chain by depositor.

address inputToken;

// Token that is received on destination chain by recipient.

address outputToken;

// The amount of input token deposited by depositor.

uint256 inputAmount;

// The amount of output token to be received by recipient.

uint256 outputAmount;

// Origin chain id.

uint256 originChainId;

// The id uniquely identifying this deposit on the origin chain.

uint32 depositId;

// The timestamp on the destination chain after which this deposit can no longer be filled.

uint32 fillDeadline;

// The timestamp on the destination chain after which any relayer can fill the deposit.

uint32 exclusivityDeadline;

// Data that is forwarded to the recipient.

bytes message;

function _getV3RelayHash(V3RelayData memory relayData) private view returns (bytes32) {

return keccak256(abi.encode(relayData, chainId()));

There are two ways to determine whether a deposit has been filled on the destinationChain:
Each fill of a deposit emits a FilledV3Relay which emits all of the data that you'd need to construct another V3RelayData structure (along with the
destination chain's block.chainId). If this relay hash matches the deposit relay hash, then the deposit is considered valid and the filler will receive a
refund in the next bundle.

​ Copy
event FilledV3Relay(


address inputToken,


// Note: outputToken should never be 0x0 in this event, unlike in a V3FundDeposited


// event. If this is 0x0 in the corresponding deposit event, then this should


// be equal to the equivalent destination token supported by this SpokePool.


address outputToken,


uint256 inputAmount,


uint256 outputAmount,


uint256 repaymentChainId,


uint256 indexed originChainId,


uint32 indexed depositId,


uint32 fillDeadline,


uint32 exclusivityDeadline,


address exclusiveRelayer,


address indexed relayer,


address depositor,


address recipient,


bytes message,


V3RelayExecutionEventInfo relayExecutionInfo


​ )
​ Call the SpokePool.fillStatuses(bytes32): uint256 function passing in the deposit relay hash. The possible fill statuses are:
​ Copy
// Fill status tracks on-chain state of deposit, uniquely identified by relayHash.


enum FillStatus {


// Self-explanatory

Unfilled,


// Someone requested a slow fill for this deposit, it will be


// slow filled to the user in the next root bundle unless it gets filled


// by a relayer before that bundle is validated and the slow fill


// is executed.


RequestedSlowFill,


// Filled by a relayer.


Filled


​ }

Expired Deposits

When an unfilled deposit's fillDeadline exceeds the destination chain's block.timestamp, it is no longer fillable--the SpokePool contract on the
destination chain will revert on a fillRelay() call.

Expired deposits are technically refunded by the next root bundle proposed to the HubPool containing an expired deposit refund. Root bundles
contain bridge events for a set of block ranges (start and end blocks) for each chain supported in Across. Root bundles are proposed optimistically as
a batch of Merkle Roots, one of which is a list of refund payments including deposits that expired in the deposit's origin chain block range. Therefore,
to know when precisely an expired deposit was refunded requires reconstructing root bundle data. This is a complex task and we're working on
making this data available in a hosted service with an API interface.

In the meantime, we believe its accurate to state that expired deposits will be refunded approximately 90 minutes after their fillDeadline. Expired
deposits are sent to the depositor address on the originChainId. The 90 minute figure assumes that on average, deposits expire in the middle of a
pending root bundle proposal which implies that the next root bundle will contain the expired deposit refund. Because root bundle proposals are
validated optimistically and the current challenge period is 60 minutes, then on average it will take 30 minutes for the next root bundle to be proposed
containing a refund for the deposit and another 60 minutes for that root bundle to be executed.
Forecasting a pending deposit's estimated time of arrival

Fill ETAs for deposits vary based on route and available filler liquidity. For example, most deposit transactions will not be filled until they
probabilistically finalized on the origin chain, where the deposit occurred.

In general, deposits that exceed current filler liquidity on the destination chain will be delayed, and unprofitable deposits (where the difference in the
output amount and the input amount is insufficient to cover the filler's transaction and opportunity costs) might never be filled until they expire. Expired
deposits will eventually be refunded, as described above.

The Limits endpoint can be called with the deposit information to forecast the fill ETA. If the deposit amount exceeds the maxDepositShortDelay, then
the deposit is likely to have been marked for slow fill. To deterministically check if a deposit has been marked for slow fill, you can call the fillStatuses()
function on the destination SpokePool as described above.

You might also like