RelayRouterV3
0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
4,720 bytes
ABI entries
19
0 read · 8 write
License
none
Contract information
- Address
- 0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0xcbc76b33bc…ce77f0aab8
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 (3)
FundsMovementSolverCallExecutedSolverNativeTransfer
ABI
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ArrayLengthsMismatch",
"type": "error"
},
{
"inputs": [],
"name": "CallFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "storedSender",
"type": "address"
},
{
"internalType": "address",
"name": "actualSender",
"type": "address"
}
],
"name": "InvalidMsgSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "InvalidRecipient",
"type": "error"
},
{
"inputs": [],
"name": "NativeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "NoRecipientSet",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "currency",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes",
"name": "metadata",
"type": "bytes"
}
],
"name": "FundsMovement",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes",
"name": "data",
"type": "bytes"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "SolverCallExecuted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "SolverNativeTransfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "bytes",
"name": "metadata",
"type": "bytes"
}
],
"name": "cleanupErc20s",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "address[]",
"name": "tos",
"type": "address[]"
},
{
"internalType": "bytes[]",
"name": "datas",
"type": "bytes[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"name": "cleanupErc20sViaCall",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "bytes",
"name": "metadata",
"type": "bytes"
}
],
"name": "cleanupNative",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "cleanupNativeViaCall",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bool",
"name": "allowFailure",
"type": "bool"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
}
],
"internalType": "struct Call3Value[]",
"name": "calls",
"type": "tuple[]"
},
{
"internalType": "address",
"name": "refundTo",
"type": "address"
},
{
"internalType": "address",
"name": "nftRecipient",
"type": "address"
},
{
"internalType": "bytes",
"name": "metadata",
"type": "bytes"
}
],
"name": "multicall",
"outputs": [
{
"components": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
},
{
"internalType": "bytes",
"name": "returnData",
"type": "bytes"
}
],
"internalType": "struct Result[]",
"name": "returnData",
"type": "tuple[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "_ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "_values",
"type": "uint256[]"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "onERC1155BatchReceived",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "_id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "onERC1155Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol";
import {Call3Value, Multicall3, Result} from "../common/Multicall3.sol";
import {ReentrancyGuardMsgSender} from "../common/ReentrancyGuardMsgSender.sol";
contract RelayRouterV3 is Multicall3, ReentrancyGuardMsgSender {
using SafeTransferLib for address;
/// @notice Revert if this contract is set as the recipient
error InvalidRecipient(address recipient);
/// @notice Revert if the native transfer failed
error NativeTransferFailed();
/// @notice Revert if no recipient is set
error NoRecipientSet();
/// @notice Revert if the array lengths do not match
error ArrayLengthsMismatch();
/// @notice Revert if a call fails
error CallFailed();
/// @notice Protocol event to be emitted when transferring native tokens
event SolverNativeTransfer(address to, uint256 amount);
/// @notice Emitted on any explicit movement of funds
event FundsMovement(
address from,
address to,
address currency,
uint256 amount,
bytes metadata
);
uint256 private constant RECIPIENT_STORAGE_SLOT =
uint256(keccak256("RelayRouter.recipient")) - 1;
constructor() {}
receive() external payable {
emit SolverNativeTransfer(address(this), msg.value);
}
/// @notice Execute a multicall with the RelayRouter as msg.sender.
/// @dev If a multicall is expecting to mint ERC721s or ERC1155s, the recipient must be explicitly set
/// All calls to ERC721s and ERC1155s in the multicall will have the same recipient set in recipient
/// Be sure to transfer ERC20s or native tokens out of the router as part of the multicall
/// @param calls The calls to perform
/// @param refundTo The address to refund any leftover native tokens to
/// @param nftRecipient The address to set as recipient of ERC721/ERC1155 mints
/// @param metadata Additional data to associate the call to
function multicall(
Call3Value[] calldata calls,
address refundTo,
address nftRecipient,
bytes calldata metadata
) public payable nonReentrant returns (Result[] memory returnData) {
if (msg.value > 0) {
emit FundsMovement(
msg.sender,
address(this),
address(0),
msg.value,
metadata
);
}
// Set the NFT recipient if provided
if (nftRecipient != address(0)) {
_setRecipient(nftRecipient);
}
// Perform the multicall
returnData = _aggregate3Value(calls);
// Clear the recipient in storage
_clearRecipient();
// Refund any leftover native tokens to the sender
cleanupNative(0, refundTo, metadata);
}
/// @notice Send leftover ERC20 tokens to recipients
/// @dev Should be included in the multicall if the router is expecting to receive tokens
/// Set amount to 0 to transfer the full balance
/// @param tokens The addresses of the ERC20 tokens
/// @param recipients The addresses to refund the tokens to
/// @param amounts The amounts to send
/// @param metadata Additional data to associate the call to
function cleanupErc20s(
address[] calldata tokens,
address[] calldata recipients,
uint256[] calldata amounts,
bytes calldata metadata
) public nonReentrant {
// Revert if array lengths do not match
if (
tokens.length != amounts.length ||
amounts.length != recipients.length
) {
revert ArrayLengthsMismatch();
}
for (uint256 i; i < tokens.length; i++) {
address token = tokens[i];
address recipient = recipients[i];
// Get the amount to transfer
uint256 amount = amounts[i] == 0
? IERC20(token).balanceOf(address(this))
: amounts[i];
if (amount > 0) {
// Transfer the token to the recipient address
token.safeTransfer(recipient, amount);
emit FundsMovement(
address(this),
recipient,
token,
amount,
metadata
);
}
}
}
/// @notice Send leftover ERC20 tokens via explicit method calls
/// @dev Should be included in the multicall if the router is expecting to receive tokens
/// Set amount to 0 to transfer the full balance
/// @param tokens The addresses of the ERC20 tokens
/// @param tos The target addresses for the calls
/// @param datas The data for the calls
/// @param amounts The amounts to send
function cleanupErc20sViaCall(
address[] calldata tokens,
address[] calldata tos,
bytes[] calldata datas,
uint256[] calldata amounts
) public nonReentrant {
// Revert if array lengths do not match
if (
tokens.length != amounts.length ||
amounts.length != tos.length ||
tos.length != datas.length
) {
revert ArrayLengthsMismatch();
}
for (uint256 i; i < tokens.length; i++) {
address token = tokens[i];
address to = tos[i];
bytes calldata data = datas[i];
// Get the amount to transfer
uint256 amount = amounts[i] == 0
? IERC20(token).balanceOf(address(this))
: amounts[i];
if (amount > 0) {
// First approve the target address for the call
IERC20(token).approve(to, amount);
// Make the call
(bool success, ) = to.call(data);
if (!success) {
revert CallFailed();
}
}
}
}
/// @notice Send leftover native tokens to the recipient address
/// @dev Set amount to 0 to transfer the full balance. Set recipient to address(0) to transfer to msg.sender
/// @param amount The amount of native tokens to transfer
/// @param recipient The recipient address
/// @param metadata Additional data to associate the call to
function cleanupNative(
uint256 amount,
address recipient,
bytes calldata metadata
) public nonReentrant {
// If recipient is address(0), set to msg.sender
address recipientAddr = recipient == address(0)
? msg.sender
: recipient;
uint256 amountToTransfer = amount == 0 ? address(this).balance : amount;
if (amountToTransfer > 0) {
recipientAddr.safeTransferETH(amountToTransfer);
emit SolverNativeTransfer(recipientAddr, amountToTransfer);
emit FundsMovement(
address(this),
recipientAddr,
address(0),
amountToTransfer,
metadata
);
}
}
/// @notice Send leftover native tokens via an explicit method call
/// @dev Set amount to 0 to transfer the full balance
/// @param amount The amount of native tokens to transfer
/// @param to The target address of the call
/// @param data The data for the call
function cleanupNativeViaCall(
uint256 amount,
address to,
bytes calldata data
) public nonReentrant {
uint256 amountToTransfer = amount == 0 ? address(this).balance : amount;
if (amountToTransfer > 0) {
(bool success, ) = to.call{value: amountToTransfer}(data);
if (!success) {
revert CallFailed();
}
}
}
/// @notice Internal function to set the recipient address for ERC721 or ERC1155 mint
/// @dev If the chain does not support tstore, recipient will be saved in storage
/// @param recipient The address of the recipient
function _setRecipient(address recipient) internal {
// For safety, revert if the recipient is this contract
// Tokens should either be minted directly to recipient, or transferred to recipient through the onReceived hooks
if (recipient == address(this)) {
revert InvalidRecipient(address(this));
}
// Set the recipient in storage
uint256 recipientStorageSlot = RECIPIENT_STORAGE_SLOT;
uint256 recipientValue = uint256(uint160(recipient));
assembly {
sstore(recipientStorageSlot, recipientValue)
}
}
/// @notice Internal function to get the recipient address for ERC721 or ERC1155 mint
function _getRecipient() internal view returns (address) {
uint256 recipientStorageSlot = RECIPIENT_STORAGE_SLOT;
uint256 value;
assembly {
value := sload(recipientStorageSlot)
}
// Get the recipient from storage
return address(uint160(value));
}
/// @notice Internal function to clear the recipient address for ERC721 or ERC1155 mint
function _clearRecipient() internal {
// Return if recipient hasn't been set
if (_getRecipient() == address(0)) {
return;
}
// Clear the recipient in storage
uint256 recipientStorageSlot = RECIPIENT_STORAGE_SLOT;
assembly {
sstore(recipientStorageSlot, 0)
}
}
function onERC721Received(
address,
/*_operator*/ address,
/*_from*/ uint256 _tokenId,
bytes calldata _data
) external returns (bytes4) {
// Get the recipient from storage
address recipient = _getRecipient();
// Revert if no recipient is set
// Note this means transferring NFTs to this contract via `safeTransferFrom` will revert,
// unless the transfer is part of a multicall that sets the recipient in storage
if (recipient == address(0)) {
revert NoRecipientSet();
}
// Transfer the NFT to the recipient
IERC721(msg.sender).safeTransferFrom(
address(this),
recipient,
_tokenId,
_data
);
return this.onERC721Received.selector;
}
function onERC1155Received(
address /*_operator*/,
address /*_from*/,
uint256 _id,
uint256 _value,
bytes calldata _data
) external returns (bytes4) {
// Get the recipient from storage
address recipient = _getRecipient();
// Revert if no recipient is set
// Note this means transferring NFTs to this contract via `safeTransferFrom` will revert,
// unless the transfer is part of a multicall that sets the recipient in storage
if (recipient == address(0)) {
revert NoRecipientSet();
}
// Transfer the tokens to the recipient
IERC1155(msg.sender).safeTransferFrom(
address(this),
recipient,
_id,
_value,
_data
);
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address /*_operator*/,
address /*_from*/,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
) external returns (bytes4) {
// Get the recipient from storage
address recipient = _getRecipient();
// Revert if no recipient is set
// Note this means transferring NFTs to this contract via `safeTransferFrom` will revert,
// unless the transfer is part of a multicall that sets the recipient in storage
if (recipient == address(0)) {
revert NoRecipientSet();
}
// Transfer the tokens to the recipient
IERC1155(msg.sender).safeBatchTransferFrom(
address(this),
recipient,
_ids,
_values,
_data
);
return this.onERC1155BatchReceived.selector;
}
}
Chain explorer2238msChain node85ms