Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: passthrough L1->L3 adapter to send messages to L3 via an L2 forwarder #607

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
71 changes: 71 additions & 0 deletions contracts/chain-adapters/Rerouter_Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { AdapterInterface } from "./interfaces/AdapterInterface.sol";

/**
* @notice Contract containing logic to send messages from L1 to a target (not necessarily a spoke pool) on L2.
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* @notice Since this adapter is normally called by the hub pool, the target of both `relayMessage` and `relayTokens`
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* will be the L3 spoke pool due to the constraints of `setCrossChainContracts` outlined in UMIP 157. However, this
* contract DOES NOT send anything to the L2 containing info on the target L3 spoke pool. The L3 spoke pool address
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* must instead be initialized on the `l2Target` contract as the same spoke pool address found in the hub pool's
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* `crossChainContracts` mapping.
* @notice There should be one of these adapters for each L3 spoke pool deployment, or equivalently, each L2
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* forwarder/adapter contract.
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* @notice The contract receiving messages on L2 will be "spoke pool like" functions, e.g. "relayRootBundle" and
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* "relaySpokePoolAdminFunction".
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* called via delegatecall, which will execute this contract's logic within the context of the originating contract.
* For example, the HubPool will delegatecall these functions, therefore its only necessary that the HubPool's methods
* that call this contract's logic guard against reentrancy.
* @custom:security-contact bugs@across.to
*/

// solhint-disable-next-line contract-name-camelcase
contract Rerouter_Adapter is AdapterInterface {
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
address public immutable l1Adapter;
address public immutable l2Target;

error RelayMessageFailed();
error RelayTokensFailed(address l1Token);

/**
* @notice Constructs new Adapter for sending tokens/messages to an L2 target.
* @param _l1Adapter Address of the adapter contract on mainnet which implements message transfers
* and token relays.
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* @param _l2Target Address of the L2 contract which receives the token and message relays.
*/
constructor(address _l1Adapter, address _l2Target) {
l1Adapter = _l1Adapter;
l2Target = _l2Target;
}

/**
* @notice Send cross-chain message to a target on L2.
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
* @notice The original target field is omitted since messages are unconditionally sent to `l2Target`.
* @param message Data to send to target.
*/
function relayMessage(address, bytes memory message) external payable override {
(bool success, ) = l1Adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Target, message)));
if (!success) revert RelayMessageFailed();
}

/**
* @notice Bridge tokens to a target on L2.
* @param l1Token L1 token to deposit.
* @param l2Token L2 token to receive.
* @param amount Amount of L1 tokens to deposit and L2 tokens to receive.
* @notice the "to" field is discarded since we unconditionally relay tokens to `l2Target`.
*/
function relayTokens(
address l1Token,
address l2Token,
uint256 amount,
address
) external payable override {
(bool success, ) = l1Adapter.delegatecall(
abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Target))
);
if (!success) revert RelayTokensFailed(l1Token);
}
}
141 changes: 141 additions & 0 deletions test/evm/foundry/local/Arbitrum_L3_Adapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { Test } from "forge-std/Test.sol";
import { MockERC20 } from "forge-std/mocks/MockERC20.sol";
import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol";
import { Rerouter_Adapter } from "../../../../contracts/chain-adapters/Rerouter_Adapter.sol";
import { Optimism_Adapter } from "../../../../contracts/chain-adapters/Optimism_Adapter.sol";
import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol";
import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol";

// We normally delegatecall these from the hub pool, which has receive(). In this test, we call the adapter
// directly, so in order to withdraw Weth, we need to have receive().
contract Mock_Rerouter_Adapter is Rerouter_Adapter {
constructor(address _l1Adapter, address _l2Target) Rerouter_Adapter(_l1Adapter, _l2Target) {}

receive() external payable {}
}

contract Token_ERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

function mint(address to, uint256 value) public virtual {
_mint(to, value);
}

function burn(address from, uint256 value) public virtual {
_burn(from, value);
}
}

contract MinimalWeth is Token_ERC20 {
constructor(string memory name, string memory symbol) Token_ERC20(name, symbol) {}

function withdraw(uint256 amount) public {
_burn(msg.sender, amount);
(bool success, ) = payable(msg.sender).call{ value: amount }("");
require(success);
}
}

contract CrossDomainMessenger {
event MessageSent(address indexed target);

function sendMessage(
address target,
bytes calldata,
uint32
) external {
emit MessageSent(target);
}
}

contract StandardBridge {
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
event ETHDepositInitiated(address indexed to, uint256 amount);

function depositERC20To(
address l1Token,
address l2Token,
address to,
uint256 amount,
uint32,
bytes calldata
) external {
Token_ERC20(l1Token).burn(msg.sender, amount);
Token_ERC20(l2Token).mint(to, amount);
}

function depositETHTo(
address to,
uint32,
bytes calldata
) external payable {
emit ETHDepositInitiated(to, msg.value);
}
}

contract ArbitrumL3AdapterTest is Test {
Rerouter_Adapter l3Adapter;
Optimism_Adapter optimismAdapter;

Token_ERC20 l1Token;
Token_ERC20 l2Token;
Token_ERC20 l1Weth;
Token_ERC20 l2Weth;
CrossDomainMessenger crossDomainMessenger;
StandardBridge standardBridge;

address l2Target;

function setUp() public {
l2Target = makeAddr("l2Target");

l1Token = new Token_ERC20("l1Token", "l1Token");
l2Token = new Token_ERC20("l2Token", "l2Token");
l1Weth = new MinimalWeth("l1Weth", "l1Weth");
l2Weth = new MinimalWeth("l2Weth", "l2Weth");

crossDomainMessenger = new CrossDomainMessenger();
standardBridge = new StandardBridge();

optimismAdapter = new Optimism_Adapter(
WETH9Interface(address(l1Weth)),
address(crossDomainMessenger),
IL1StandardBridge(address(standardBridge)),
IERC20(address(0)),
ITokenMessenger(address(0))
);
l3Adapter = new Mock_Rerouter_Adapter(address(optimismAdapter), l2Target);
}

// Messages should be indiscriminately sent to the l2Forwarder.
function testRelayMessage(address target, bytes memory message) public {
vm.expectEmit(address(crossDomainMessenger));
emit CrossDomainMessenger.MessageSent(l2Target);
l3Adapter.relayMessage(target, message);
nicholaspai marked this conversation as resolved.
Show resolved Hide resolved
}

// Sending Weth should call depositETHTo().
function testRelayWeth(uint256 amountToSend, address random) public {
vm.deal(address(l1Weth), amountToSend);
l1Weth.mint(address(l3Adapter), amountToSend);
assertEq(amountToSend, l1Weth.totalSupply());
vm.expectEmit(address(standardBridge));
emit StandardBridge.ETHDepositInitiated(l2Target, amountToSend);
l3Adapter.relayTokens(address(l1Weth), address(l2Weth), amountToSend, random);
assertEq(0, l1Weth.totalSupply());
}

// Sending any random token should call depositERC20To().
function testRelayToken(uint256 amountToSend, address random) public {
l1Token.mint(address(l3Adapter), amountToSend);
assertEq(amountToSend, l1Token.totalSupply());
l3Adapter.relayTokens(address(l1Token), address(l2Token), amountToSend, random);
assertEq(amountToSend, l2Token.balanceOf(l2Target));
assertEq(amountToSend, l2Token.totalSupply());
assertEq(0, l1Token.totalSupply());
}
}
Loading