MulticallHandler
0xa8ad2e87e2043711d8bec77e8bc3e2683c0ab6bd
Verification
Verified
v0.8.30+commit.73712a01
Type
Contract
2,905 bytes
ABI entries
12
0 read · 4 write
License
none
Contract information
- Address
- 0xa8ad2e87e2043711d8bec77e8bc3e2683c0ab6bd
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.30+commit.73712a01
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0x6ed347ae3e…083ff04803
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (0)
No read functions
The ABI is unavailable or exposes no view functions.
Events (2)
CallsFailedDrainedTokens
ABI
[
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"internalType": "struct MulticallHandler.Call[]",
"name": "calls",
"type": "tuple[]"
}
],
"name": "CallReverted",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "callDataLength",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "offset",
"type": "uint256"
}
],
"name": "CalldataTooShort",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"internalType": "struct MulticallHandler.Call[]",
"name": "calls",
"type": "tuple[]"
}
],
"name": "InvalidCall",
"type": "error"
},
{
"inputs": [],
"name": "NotSelf",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
}
],
"name": "ReplacementCallFailed",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"indexed": false,
"internalType": "struct MulticallHandler.Call[]",
"name": "calls",
"type": "tuple[]"
},
{
"indexed": true,
"internalType": "address",
"name": "fallbackRecipient",
"type": "address"
}
],
"name": "CallsFailed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DrainedTokens",
"type": "event"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"internalType": "struct MulticallHandler.Call[]",
"name": "calls",
"type": "tuple[]"
}
],
"name": "attemptCalls",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address payable",
"name": "destination",
"type": "address"
}
],
"name": "drainLeftoverTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "bytes",
"name": "message",
"type": "bytes"
}
],
"name": "handleV3AcrossMessage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"components": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "offset",
"type": "uint256"
}
],
"internalType": "struct MulticallHandler.Replacement[]",
"name": "replacement",
"type": "tuple[]"
}
],
"name": "makeCallWithBalance",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;
import "../interfaces/SpokePoolMessageHandler.sol";
import "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-v4/utils/Address.sol";
import "@openzeppelin/contracts-v4/security/ReentrancyGuard.sol";
/**
* @title Across Multicall contract that allows a user to specify a series of calls that should be made by the handler
* via the message field in the deposit.
* @dev This contract makes the calls blindly. The contract will send any remaining tokens The caller should ensure that the tokens recieved by the handler are completely consumed.
*/
contract MulticallHandler is AcrossMessageHandler, ReentrancyGuard {
using SafeERC20 for IERC20;
using Address for address payable;
struct Call {
address target;
bytes callData;
uint256 value;
}
struct Replacement {
address token;
uint256 offset;
}
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;
}
// Emitted when one of the calls fails. Note: all calls are reverted in this case.
event CallsFailed(Call[] calls, address indexed fallbackRecipient);
// Emitted when there are leftover tokens that are sent to the fallbackRecipient.
event DrainedTokens(address indexed recipient, address indexed token, uint256 indexed amount);
// Errors
error CallReverted(uint256 index, Call[] calls);
error NotSelf();
error InvalidCall(uint256 index, Call[] calls);
error ReplacementCallFailed(bytes callData);
error CalldataTooShort(uint256 callDataLength, uint256 offset);
modifier onlySelf() {
_requireSelf();
_;
}
/**
* @notice Main entrypoint for the handler called by the SpokePool contract.
* @dev This will execute all calls encoded in the msg. The caller is responsible for making sure all tokens are
* drained from this contract by the end of the series of calls. If not, they can be stolen.
* A drainLeftoverTokens call can be included as a way to drain any remaining tokens from this contract.
* @param token The token address that was received from the relay
* @param message abi encoded array of Call structs, containing a target, callData, and value for each call that
* the contract should make.
*/
function handleV3AcrossMessage(address token, uint256, address, bytes memory message) public virtual nonReentrant {
Instructions memory instructions = abi.decode(message, (Instructions));
// If there is no fallback recipient, call and revert if the inner call fails.
if (instructions.fallbackRecipient == address(0)) {
this.attemptCalls(instructions.calls);
return;
}
// Otherwise, try the call and send to the fallback recipient if any tokens are leftover.
(bool success, ) = address(this).call(abi.encodeCall(this.attemptCalls, (instructions.calls)));
if (!success) emit CallsFailed(instructions.calls, instructions.fallbackRecipient);
// If there are leftover tokens, send them to the fallback recipient regardless of execution success.
_drainRemainingTokens(token, payable(instructions.fallbackRecipient));
}
function attemptCalls(Call[] memory calls) external onlySelf {
uint256 length = calls.length;
for (uint256 i = 0; i < length; ++i) {
Call memory call = calls[i];
// If we are calling an EOA with calldata, assume target was incorrectly specified and revert.
if (call.callData.length > 0 && call.target.code.length == 0) {
revert InvalidCall(i, calls);
}
(bool success, ) = call.target.call{ value: call.value }(call.callData);
if (!success) revert CallReverted(i, calls);
}
}
function drainLeftoverTokens(address token, address payable destination) external onlySelf {
_drainRemainingTokens(token, destination);
}
function _drainRemainingTokens(address token, address payable destination) internal {
if (token != address(0)) {
// ERC20 token.
uint256 amount = IERC20(token).balanceOf(address(this));
if (amount > 0) {
IERC20(token).safeTransfer(destination, amount);
emit DrainedTokens(destination, token, amount);
}
} else {
// Send native token
uint256 amount = address(this).balance;
if (amount > 0) {
destination.sendValue(amount);
}
}
}
/**
* @notice Executes a call while replacing specified calldata offsets with current token/native balances.
* @dev Modifies calldata in-place using OR operations. Target calldata positions must be zeroed out.
* Cannot handle negative balances, making it incompatible with DEXs requiring negative input amounts.
* For native balance (token = address(0)), the entire balance is used as call value.
* @param target The contract address to call
* @param callData The calldata to execute, with zero values at replacement positions
* @param value The native token value to send (ignored if native balance replacement is used)
* @param replacement Array of Replacement structs specifying token addresses and byte offsets for balance injection
*/
function makeCallWithBalance(
address target,
bytes memory callData,
uint256 value,
Replacement[] calldata replacement
) external onlySelf {
for (uint256 i = 0; i < replacement.length; ++i) {
uint256 bal = 0;
if (replacement[i].token != address(0)) {
bal = IERC20(replacement[i].token).balanceOf(address(this));
} else {
bal = address(this).balance;
// If we're using the native balance, we assume that the caller wants to send the full value to the target.
value = bal;
}
// + 32 to skip the length of the calldata
uint256 offset = replacement[i].offset + 32;
// 32 has already been added to the offset, and the replacement value is 32 bytes long, so
// we don't need to add 32 here. We just directly compare the offset with the length of the calldata.
if (offset > callData.length) revert CalldataTooShort(callData.length, offset);
assembly ("memory-safe") {
// Get the pointer to the offset that the caller wants to overwrite.
let ptr := add(callData, offset)
// Get the current value at the offset.
let current := mload(ptr)
// Or the current value with the new value.
// Reasoning:
// - caller should 0-out any portion that they want overwritten.
// - if the caller is representing the balance in a smaller integer, like a uint160 or uint128,
// the higher bits will be 0 and not overwrite any other data in the calldata assuming
// the balance is small enough to fit in the smaller integer.
// - The catch: the smaller integer where they want to store the balance must end no
// earlier than the 32nd byte in their calldata. Otherwise, this would require a
// negative offset, which is not possible.
let val := or(bal, current)
// Store the new value at the offset.
mstore(ptr, val)
}
}
(bool success, ) = target.call{ value: value }(callData);
if (!success) revert ReplacementCallFailed(callData);
}
function _requireSelf() internal view {
// Must be called by this contract to ensure that this cannot be triggered without the explicit consent of the
// depositor (for a valid relay).
if (msg.sender != address(this)) revert NotSelf();
}
// Used if the caller is trying to unwrap the native token to this contract.
receive() external payable {}
}
Chain explorer1995msChain node85ms