-
Notifications
You must be signed in to change notification settings - Fork 56
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
Changes from 5 commits
15e815b
f9c0d62
b360237
2daf57b
cea0337
0de9303
0371348
fd94012
93d40cf
00aeb40
f2e3c41
f251354
1e984cc
fe66b78
814498b
43f03b8
cc386a7
a9cf8b1
6cbbfd4
6d8a633
4076b6b
110a8e9
ac7b0de
19813c6
d87d09c
d78ad6f
2c97c2a
8ee13c0
9544a96
f914722
a282d83
1339b5f
c3950ac
2aac0f7
06c8877
01b1a38
77bc3d5
af95f41
64d3503
1274920
d330993
5271df8
7aa7d11
20c89a7
e8590d4
cbe2953
14c5c41
242775d
87cdbe6
41b4856
ce3a425
1b1ceb5
074acc7
8796c40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// 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 Arbitrum-like L3s using an intermediate L2 message forwarder. | ||
* @notice This contract requires an L2 forwarder contract to be deployed, since we overwrite the target field to this new target. | ||
* @dev Public functions calling external contracts do not guard against reentrancy because they are expected to be | ||
* 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. | ||
*/ | ||
|
||
// solhint-disable-next-line contract-name-camelcase | ||
contract Arbitrum_L3_Adapter is AdapterInterface { | ||
address public immutable adapter; | ||
nicholaspai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
address public immutable l2Forwarder; | ||
|
||
error RelayMessageFailed(); | ||
error RelayTokensFailed(address l1Token); | ||
|
||
/** | ||
* @notice Constructs new Adapter for sending tokens/messages to Arbitrum-like L3s. | ||
* @param _adapter Address of the adapter contract on mainnet which implements message transfers | ||
* and token relays. | ||
* @param _l2Forwarder Address of the l2 forwarder contract which relays messages up to the L3 spoke pool. | ||
*/ | ||
constructor(address _adapter, address _l2Forwarder) { | ||
adapter = _adapter; | ||
l2Forwarder = _l2Forwarder; | ||
} | ||
|
||
/** | ||
* @notice Send cross-chain message to target on L2, which is forwarded to the Arbitrum-like L3. | ||
* @dev there is a bijective mapping of L3 adapters (on L1) to L2 forwarders to L3 spoke pools. The | ||
* spoke pool address is stored by the L2 forwarder and the L2 forwarder address is stored in this contract. | ||
* @param message Data to send to target. | ||
*/ | ||
function relayMessage(address, bytes memory message) external payable override { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC, why did you decide to do this via delegatecall rather than inheritance? To avoid having to have a different contract for an L2 with custom gas vs non custom gas? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a delegatecall so this can work with any L2 by piggybacking off of the adapter logic. Naming is admittedly poor here, but the idea was for this (L3) adapter to send messages and tokens to L2 using the exact same logic as the corresponding L2 adapter (e.g. an Arbitrum_*_Adapter, Optimism_Adapter, ZkSync_Adapter, etc) and overwrite the target (since the hub pool will specify the target as the L3 spoke pool due to |
||
(bool success, ) = adapter.delegatecall(abi.encodeCall(AdapterInterface.relayMessage, (l2Forwarder, message))); | ||
if (!success) revert RelayMessageFailed(); | ||
} | ||
|
||
/** | ||
* @notice Bridge tokens to an Arbitrum-like L3, using an L2 forwarder. | ||
* @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. | ||
* @dev we discard the "to" field since tokens are always sent to the l2Forwarder. | ||
*/ | ||
function relayTokens( | ||
address l1Token, | ||
address l2Token, | ||
uint256 amount, | ||
address | ||
) external payable override { | ||
(bool success, ) = adapter.delegatecall( | ||
abi.encodeCall(AdapterInterface.relayTokens, (l1Token, l2Token, amount, l2Forwarder)) | ||
); | ||
if (!success) revert RelayTokensFailed(l1Token); | ||
} | ||
} |
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 { Arbitrum_L3_Adapter } from "../../../../contracts/chain-adapters/Arbitrum_L3_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_L3_Adapter is Arbitrum_L3_Adapter { | ||
constructor(address _adapter, address _l2Forwarder) Arbitrum_L3_Adapter(_adapter, _l2Forwarder) {} | ||
|
||
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 { | ||
Arbitrum_L3_Adapter l3Adapter; | ||
Optimism_Adapter optimismAdapter; | ||
|
||
Token_ERC20 l1Token; | ||
Token_ERC20 l2Token; | ||
Token_ERC20 l1Weth; | ||
Token_ERC20 l2Weth; | ||
CrossDomainMessenger crossDomainMessenger; | ||
StandardBridge standardBridge; | ||
|
||
address l2Forwarder; | ||
|
||
function setUp() public { | ||
l2Forwarder = vm.addr(1); | ||
|
||
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_L3_Adapter(address(optimismAdapter), l2Forwarder); | ||
} | ||
|
||
// Messages should be indiscriminately sent to the l2Forwarder. | ||
function testRelayMessage(address target, bytes memory message) public { | ||
vm.expectEmit(address(crossDomainMessenger)); | ||
emit CrossDomainMessenger.MessageSent(l2Forwarder); | ||
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(l2Forwarder, 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(l2Forwarder)); | ||
assertEq(amountToSend, l2Token.totalSupply()); | ||
assertEq(0, l1Token.totalSupply()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this contract is specific to arbitrum, right? It's really just an adapter for any forwarder, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is likely due to my poor naming, but I did this to emphasize that the L3 is Arbitrum-like. There is no assumption about the structure of the L2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming is still a sticking point for me. I think it makes sense to spend some more time thinking about this. This contract actually functions more like a (re-)router I guess, but maybe that's just my networking bias.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree this naming is not useful right now. Because it has "L3" in it its non-intuitive that its actually meant to be deployed on L1. Let's also remove the Arbitrum mention from the name because its clearly not specific to Arbitrum. Please change all the comments also to make it clear this can be used generically.
I think a rerouter in the name could work, and clearly to be consistent with other L1 contracts it should end with Adapter. What about RerouterAdapter?
@pxrl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimistically changing it to
RerouterAdapter
and updating the comments to emphasize that it can be used in other contexts 1e984cc