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 javascript:
// 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
]
]
);
}
const myMessage = generateMessageForMulticallHandler("your-wallet-address", "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", "10", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0")
console.log(myMessage)Example in solidity:
Generating the Deposit
The deposit creation process is nearly identical to the process described in Developer Quickstart. 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. In the above Aave example, the
outputAmountneeds to be set equal to thedepositAmountto 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. However, usually thesuggested-feesendpoint in the API is queried to get the suggestedoutputAmountto set in order to make theinputAmountprofitable for fillers to relay. We recommend doing the following in cases like this Aave example:In the
messageyou use as a parameter when querying thesuggested-feesendpoint, setdepositAmount = outputAmount = inputAmountwhich will return you a suggested output amount.When you call depositV3, set
outputAmountto the newly suggested output amount, and keepinputAmountequal todepositAmount. This will ensure that your deposit is profitable for fillers.
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. Make sure that you re-generate the message settinginputAmount = depositAmountandoutputAmountequal to the suggested output amount.In the Aave example, the
inputAmountshould be set equal to thedepositAmountand theoutputAmountshould be set equal to the suggested-fees' suggested output amount. This suggested output amount will be received on the destination chain by the handler contract and be equal to the amount to be deposited into Aave on behalf of the user.
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 example, 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 a 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:
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.
Charging Protocol Fees (Example #3)
In this example, our message will include the user's address, the fee percentage, and the fee recipient's address as the minimum required information that our handler contract would need to know. Here's an example for generating this in javascript:
Example in solidity:
Using the above contract, generate a message by calling generateMessageForMulticallHandler and then use this message when requesting a quote and submitting a deposit. In the above example, the outputAmount will be set equal to the outputAmount specified in the Across deposit.
We assume feePercentage is set to some fixed value, e.g. 0.01e18to charge 1% on the outputAmount. When querying API Reference, the returned outputAmount is inputAmount less bridge fees only (so that the deposit is profitable to relay). Therefore, the user's expected output amount to receive post-fees will be API Reference outputAmount less feePercentage.
Creating a Transaction for a Uniswap Swap (Example #4)
In this example we'll be walking through how to use this functionality to perform a Uniswap swap on behalf of the user on the destination chain. Our message will include the user's address, the Uniswap pool contract address, and the calldata we want to execute to swap against the pool as the minimum required information that our handler contract would need to know to generate the Uniswap swap instruction.
Example in solidity:
Using the above contract, generate a message by calling generateMessageForMulticallHandler and then use this message when requesting a quote and submitting a deposit.
ExactInputSingleParams parameters:
tokenInwill be the token that we initiate the swap with so it will be theoutputTokenthat we expect to receive on the destination chain following the Across transaction.tokenOutwill be the token that we want to swap into via the Uniswap swap router.feewill be the fee of the pool we expect to swap through. The Uniswap docs will explain this parameter better but we'll know it at deposit time.recipientshould be theuserAddressdeadlineThe Uniswap docs provides details about this parameter, which is the unix time after which the swap fails.amountInwill be the amount to initiate the swap with so it will be theoutputAmountin the Across transaction. When crafting an Across deposit, usually thesuggested-feesendpoint in the API is queried to get the suggestedoutputAmountto set in order to make theinputAmountprofitable for fillers to relay. We recommend doing the following in cases like this Uniswap example:In the
messageyou use as a parameter when querying thesuggested-feesendpoint, setamountIn = outputAmount = inputAmountwhich will return you a suggested output amount.When you call depositV3, set
outputAmountto the newly suggested output amount, and keepinputAmountequal to the originalamountInfrom the prior step. This will ensure that your deposit is profitable for fillers.
amountOutMinimumshould be based on theamountInvalue used above and the minimum price ratio you're willing to acceptsqrtPriceLimitX96similar to above, read the Uniswap docs for better details.
Reverting Transactions
If the
messagespecifies a transaction that could revert when handled on the destination, it is recommended to set afallbackRecipientIf a
fallbackRecipientis set to some non-zero address and any instruction reverts, the tokens that are received in the fill asoutputAmountwill be sent to thefallbackRecipienton thedestinationChainIdIf 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
messageis not empty. Create themessageby encoding instructions correctly that will be executed by theMulticallHandleron the destination chain.Multicall Handler contract deployments can be found here
Alongside the encoded instructions in the
message, set afallbackRecipientaddress to one that should receive any leftover tokens regardless of instruction execution success. In most cases, this should be the end user's addressThe
recipientaddress is the multicall handler contract on thedestinationChainIdUse the Across API to get an estimate of the
outputAmountyou should set for your message and recipient combination.Call
depositV3()passing in your message and the suggestedoutputAmount.Once the relayer calls
fillV3Relay()on the destination, the recipient multicall handler contract'shandleV3AcrossMessagewill be executedThe additional gas cost to execute the above function is compensated for in the difference between the deposit's
inputAmountandoutputAmount.
Security & Safety Considerations
Avoid making unvalidated assumptions about the
messagedata supplied tohandleV3AcrossMessage(). 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
tokenSentandamountvariables supplied with to thehandleV3AcrossMessage()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()relayerparameter. This implies a trust relationship with one or more relayers.
Last updated