-
Notifications
You must be signed in to change notification settings - Fork 50
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: add Arbitrum forwarder contracts #610
Changes from all commits
975ff35
11d28b1
be13700
b733f14
5dc9890
4a4ebb3
ea2b6ca
15ff8f5
4433749
05dfc6e
7cd46da
9cffd55
253310d
8201f86
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 |
---|---|---|
@@ -1,78 +1,59 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity ^0.8.0; | ||
|
||
import "./interfaces/AdapterInterface.sol"; | ||
import { AdapterInterface } from "./interfaces/AdapterInterface.sol"; | ||
|
||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "../external/interfaces/CCTPInterfaces.sol"; | ||
import "../libraries/CircleCCTPAdapter.sol"; | ||
import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { Arbitrum_AdapterBase } from "./Arbitrum_AdapterBase.sol"; | ||
import { ITokenMessenger } from "../external/interfaces/CCTPInterfaces.sol"; | ||
import { ArbitrumInboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridgeInterfaces.sol"; | ||
|
||
/** | ||
* @notice Contract containing logic to send messages from L1 to Arbitrum. | ||
* @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. | ||
* @custom:security-contact [email protected] | ||
*/ | ||
|
||
// solhint-disable-next-line contract-name-camelcase | ||
contract Arbitrum_Adapter is AdapterInterface, CircleCCTPAdapter { | ||
contract Arbitrum_Adapter is AdapterInterface, Arbitrum_AdapterBase { | ||
using SafeERC20 for IERC20; | ||
|
||
// Amount of ETH allocated to pay for the base submission fee. The base submission fee is a parameter unique to | ||
// retryable transactions; the user is charged the base submission fee to cover the storage costs of keeping their | ||
// ticket’s calldata in the retry buffer. (current base submission fee is queryable via | ||
// ArbRetryableTx.getSubmissionPrice). ArbRetryableTicket precompile interface exists at L2 address | ||
// 0x000000000000000000000000000000000000006E. | ||
uint256 public constant L2_MAX_SUBMISSION_COST = 0.01e18; | ||
|
||
// L2 Gas price bid for immediate L2 execution attempt (queryable via standard eth*gasPrice RPC) | ||
uint256 public constant L2_GAS_PRICE = 5e9; // 5 gWei | ||
|
||
// Native token expected to be sent in L2 message. Should be 0 for only use case of this constant, which | ||
// includes is sending messages from L1 to L2. | ||
uint256 public constant L2_CALL_VALUE = 0; | ||
|
||
// Gas limit for L2 execution of a cross chain token transfer sent via the inbox. | ||
uint32 public constant RELAY_TOKENS_L2_GAS_LIMIT = 300_000; | ||
// Gas limit for L2 execution of a message sent via the inbox. | ||
uint32 public constant RELAY_MESSAGE_L2_GAS_LIMIT = 2_000_000; | ||
|
||
address public constant L1_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; | ||
|
||
// This address on L2 receives extra ETH that is left over after relaying a message via the inbox. | ||
address public immutable L2_REFUND_L2_ADDRESS; | ||
|
||
// Inbox system contract to send messages to Arbitrum. Token bridges use this to send tokens to L2. | ||
// https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol | ||
ArbitrumL1InboxLike public immutable L1_INBOX; | ||
|
||
// Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this | ||
// contract calls the Inbox. | ||
// Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol | ||
ArbitrumL1ERC20GatewayLike public immutable L1_ERC20_GATEWAY_ROUTER; | ||
|
||
/** | ||
* @notice Constructs new Adapter. | ||
* @param _l1ArbitrumInbox Inbox helper contract to send messages to Arbitrum. | ||
* @param _l1ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum. | ||
* @param _l2RefundL2Address L2 address to receive gas refunds on after a message is relayed. | ||
* @param _l1Usdc USDC address on L1. | ||
* @param _cctpTokenMessenger TokenMessenger contract to bridge via CCTP. | ||
* @param _circleDomainId Circle CCTP domain ID for the target network (3 for Arbitrum). | ||
* @param _l2MaxSubmissionCost maximum amount of ETH to send with a transaction for it to execute on L2. | ||
* @param _l2GasPrice gas price bid for a message to be executed on L2. | ||
*/ | ||
constructor( | ||
ArbitrumL1InboxLike _l1ArbitrumInbox, | ||
ArbitrumInboxLike _l1ArbitrumInbox, | ||
ArbitrumL1ERC20GatewayLike _l1ERC20GatewayRouter, | ||
address _l2RefundL2Address, | ||
IERC20 _l1Usdc, | ||
ITokenMessenger _cctpTokenMessenger | ||
) CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Arbitrum) { | ||
L1_INBOX = _l1ArbitrumInbox; | ||
L1_ERC20_GATEWAY_ROUTER = _l1ERC20GatewayRouter; | ||
L2_REFUND_L2_ADDRESS = _l2RefundL2Address; | ||
} | ||
ITokenMessenger _cctpTokenMessenger, | ||
uint32 _circleDomainId, | ||
uint256 _l2MaxSubmissionCost, | ||
uint256 _l2GasPrice | ||
) | ||
Arbitrum_AdapterBase( | ||
_l1ArbitrumInbox, | ||
_l1ERC20GatewayRouter, | ||
_l2RefundL2Address, | ||
_l1Usdc, | ||
_cctpTokenMessenger, | ||
_circleDomainId, | ||
_l2MaxSubmissionCost, | ||
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. Note for future work: anytime you make changes to existing contract code where its understood that you're refactoring, you should leave a comment saying something like "I changed this code here" |
||
_l2GasPrice | ||
) | ||
{} | ||
Check warning on line 56 in contracts/chain-adapters/Arbitrum_Adapter.sol GitHub Actions / Lint (20)
|
||
|
||
/** | ||
* @notice Send cross-chain message to target on Arbitrum. | ||
|
@@ -82,19 +63,7 @@ | |
* @param message Data to send to target. | ||
*/ | ||
function relayMessage(address target, bytes memory message) external payable override { | ||
uint256 requiredL1CallValue = _contractHasSufficientEthBalance(RELAY_MESSAGE_L2_GAS_LIMIT); | ||
|
||
L1_INBOX.createRetryableTicket{ value: requiredL1CallValue }( | ||
target, // destAddr destination L2 contract address | ||
L2_CALL_VALUE, // l2CallValue call value for retryable L2 message | ||
L2_MAX_SUBMISSION_COST, // maxSubmissionCost Max gas deducted from user's L2 balance to cover base fee | ||
L2_REFUND_L2_ADDRESS, // excessFeeRefundAddress maxgas * gasprice - execution cost gets credited here on L2 | ||
L2_REFUND_L2_ADDRESS, // callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled | ||
RELAY_MESSAGE_L2_GAS_LIMIT, // maxGas Max gas deducted from user's L2 balance to cover L2 execution | ||
L2_GAS_PRICE, // gasPriceBid price bid for L2 execution | ||
message // data ABI encoded data of L2 message | ||
); | ||
|
||
_relayMessage(target, message); | ||
emit MessageRelayed(target, message); | ||
} | ||
|
||
|
@@ -113,69 +82,7 @@ | |
uint256 amount, | ||
address to | ||
) external payable override { | ||
// Check if this token is USDC, which requires a custom bridge via CCTP. | ||
if (_isCCTPEnabled() && l1Token == address(usdcToken)) { | ||
_transferUsdc(to, amount); | ||
} | ||
// If not, we can use the Arbitrum gateway | ||
else { | ||
uint256 requiredL1CallValue = _contractHasSufficientEthBalance(RELAY_TOKENS_L2_GAS_LIMIT); | ||
|
||
// Approve the gateway, not the router, to spend the hub pool's balance. The gateway, which is different | ||
// per L1 token, will temporarily escrow the tokens to be bridged and pull them from this contract. | ||
address erc20Gateway = L1_ERC20_GATEWAY_ROUTER.getGateway(l1Token); | ||
IERC20(l1Token).safeIncreaseAllowance(erc20Gateway, amount); | ||
|
||
// `outboundTransfer` expects that the caller includes a bytes message as the last param that includes the | ||
// maxSubmissionCost to use when creating an L2 retryable ticket: https://github.com/OffchainLabs/arbitrum/blob/e98d14873dd77513b569771f47b5e05b72402c5e/packages/arb-bridge-peripherals/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol#L232 | ||
bytes memory data = abi.encode(L2_MAX_SUBMISSION_COST, ""); | ||
|
||
// Note: Legacy routers don't have the outboundTransferCustomRefund method, so default to using | ||
// outboundTransfer(). Legacy routers are used for the following tokens that are currently enabled: | ||
// - DAI: the implementation of `outboundTransfer` at the current DAI custom gateway | ||
// (https://etherscan.io/address/0xD3B5b60020504bc3489D6949d545893982BA3011#writeContract) sets the | ||
// sender as the refund address so the aliased HubPool should receive excess funds. Implementation here: | ||
// https://github.com/makerdao/arbitrum-dai-bridge/blob/11a80385e2622968069c34d401b3d54a59060e87/contracts/l1/L1DaiGateway.sol#L109 | ||
if (l1Token == L1_DAI) { | ||
// This means that the excess ETH to pay for the L2 transaction will be sent to the aliased | ||
// contract address on L2, which we'd have to retrieve via a custom adapter, the Arbitrum_RescueAdapter. | ||
// To do so, in a single transaction: 1) setCrossChainContracts to Arbitrum_RescueAdapter, 2) relayMessage | ||
// with function data = abi.encode(amountToRescue), 3) setCrossChainContracts back to this adapter. | ||
L1_ERC20_GATEWAY_ROUTER.outboundTransfer{ value: requiredL1CallValue }( | ||
l1Token, | ||
to, | ||
amount, | ||
RELAY_TOKENS_L2_GAS_LIMIT, | ||
L2_GAS_PRICE, | ||
data | ||
); | ||
} else { | ||
L1_ERC20_GATEWAY_ROUTER.outboundTransferCustomRefund{ value: requiredL1CallValue }( | ||
l1Token, | ||
L2_REFUND_L2_ADDRESS, | ||
to, | ||
amount, | ||
RELAY_TOKENS_L2_GAS_LIMIT, | ||
L2_GAS_PRICE, | ||
data | ||
); | ||
} | ||
} | ||
_relayTokens(l1Token, l2Token, amount, to); | ||
emit TokensRelayed(l1Token, l2Token, amount, to); | ||
} | ||
|
||
/** | ||
* @notice Returns required amount of ETH to send a message via the Inbox. | ||
* @param l2GasLimit L2 gas limit for the message. | ||
* @return amount of ETH that this contract needs to hold in order for relayMessage to succeed. | ||
*/ | ||
function getL1CallValue(uint32 l2GasLimit) public pure returns (uint256) { | ||
return L2_MAX_SUBMISSION_COST + L2_GAS_PRICE * l2GasLimit; | ||
} | ||
|
||
function _contractHasSufficientEthBalance(uint32 l2GasLimit) internal view returns (uint256) { | ||
uint256 requiredL1CallValue = getL1CallValue(l2GasLimit); | ||
require(address(this).balance >= requiredL1CallValue, "Insufficient ETH balance"); | ||
return requiredL1CallValue; | ||
} | ||
} |
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 change was only just introduced back in this (not yer merged) PR: https://github.com/across-protocol/contracts/pull/609/files#diff-9986d5b9b57b18f73b5c629869f6d8a6cfdbfa978964dd76de7f4f37d4b5d561
Is it worth backporting the change to that PR, or is the intermediate step necessary?