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:
Example in solidity:
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. In the above Aave example, the
outputAmount
needs to be set equal to thedepositAmount
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. However, usually thesuggested-fees
endpoint in the API is queried to get the suggestedoutputAmount
to set in order to make theinputAmount
profitable for fillers to relay. We recommend doing the following in cases like this Aave example:In the
message
you use as a parameter when querying thesuggested-fees
endpoint, setdepositAmount = outputAmount = inputAmount
which will return you a suggested output amount.When you call depositV3, set
outputAmount
to the newly suggested output amount, and keepinputAmount
equal 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 = depositAmount
andoutputAmount
equal to the suggested output amount.In the Aave example, the
inputAmount
should be set equal to thedepositAmount
and theoutputAmount
should 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 typescript:
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.01e18
to charge 1% on the outputAmount
. When querying GET /suggested-fees, 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 GET /suggested-fees 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:
tokenIn
will be the token that we initiate the swap with so it will be theoutputToken
that we expect to receive on the destination chain following the Across transaction.tokenOut
will be the token that we want to swap into via the Uniswap swap router.fee
will 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.recipient
should be theuserAddress
deadline
The Uniswap docs provides details about this parameter, which is the unix time after which the swap fails.amountIn
will be the amount to initiate the swap with so it will be theoutputAmount
in the Across transaction. When crafting an Across deposit, usually thesuggested-fees
endpoint in the API is queried to get the suggestedoutputAmount
to set in order to make theinputAmount
profitable for fillers to relay. We recommend doing the following in cases like this Uniswap example:In the
message
you use as a parameter when querying thesuggested-fees
endpoint, setamountIn = outputAmount = inputAmount
which will return you a suggested output amount.When you call depositV3, set
outputAmount
to the newly suggested output amount, and keepinputAmount
equal to the originalamountIn
from the prior step. This will ensure that your deposit is profitable for fillers.
amountOutMinimum
should be based on theamountIn
value used above and the minimum price ratio you're willing to acceptsqrtPriceLimitX96
similar to above, read the Uniswap docs for better details.
Reverting Transactions
If the
message
specifies a transaction that could revert when handled on the destination, it is recommended to set afallbackRecipient
If a
fallbackRecipient
is set to some non-zero address and any instruction reverts, the tokens that are received in the fill asoutputAmount
will be sent to thefallbackRecipient
on thedestinationChainId
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 themessage
by encoding instructions correctly that will be executed by theMulticallHandler
on the destination chain.Multicall Handler contract deployments can be found here
Alongside the encoded instructions in the
message
, set afallbackRecipient
address to one that should receive any leftover tokens regardless of instruction execution success. In most cases, this should be the end user's addressThe
recipient
address is the multicall handler contract on thedestinationChainId
Use the Across API to get an estimate of the
outputAmount
you 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'shandleV3AcrossMessage
will be executedThe additional gas cost to execute the above function is compensated for in the difference between the deposit's
inputAmount
andoutputAmount
.
Security & Safety Considerations
Avoid making unvalidated assumptions about the
message
data 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
tokenSent
andamount
variables 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()
relayer
parameter. This implies a trust relationship with one or more relayers.
Last updated