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: add Arbitrum forwarder contracts #610

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions contracts/Arbitrum_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pragma solidity ^0.8.19;
import "./SpokePool.sol";
import "./libraries/CircleCCTPAdapter.sol";
import { ArbitrumL2ERC20GatewayLike } from "./interfaces/ArbitrumBridgeInterfaces.sol";
import { AddressUtils } from "./libraries/AddressUtils.sol";

/**
* @notice AVM specific SpokePool. Uses AVM cross-domain-enabled logic to implement admin only access to functions.
Expand Down Expand Up @@ -54,7 +55,7 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
}

modifier onlyFromCrossDomainAdmin() {
require(msg.sender == _applyL1ToL2Alias(crossDomainAdmin), "ONLY_COUNTERPART_GATEWAY");
require(msg.sender == AddressUtils._applyL1ToL2Alias(crossDomainAdmin), "ONLY_COUNTERPART_GATEWAY");
_;
}

Expand Down Expand Up @@ -111,17 +112,6 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
emit WhitelistedTokens(_l2Token, _l1Token);
}

// L1 addresses are transformed during l1->l2 calls.
// See https://developer.offchainlabs.com/docs/l1_l2_messages#address-aliasing for more information.
// This cannot be pulled directly from Arbitrum contracts because their contracts are not 0.8.X compatible and
// this operation takes advantage of overflows, whose behavior changed in 0.8.0.
function _applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) {
// Allows overflows as explained above.
unchecked {
l2Address = address(uint160(l1Address) + uint160(0x1111000000000000000000000000000000001111));
}
}

// Apply AVM-specific transformation to cross domain admin address on L1.
function _requireAdminSender() internal override onlyFromCrossDomainAdmin {}
}
12 changes: 2 additions & 10 deletions contracts/ZkSync_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.0;

import "./SpokePool.sol";
import { AddressUtils } from "./libraries/AddressUtils.sol";

// https://github.com/matter-labs/era-contracts/blob/6391c0d7bf6184d7f6718060e3991ba6f0efe4a7/zksync/contracts/bridge/L2ERC20Bridge.sol#L104
interface ZkBridgeLike {
Expand Down Expand Up @@ -62,7 +63,7 @@ contract ZkSync_SpokePool is SpokePool {
}

modifier onlyFromCrossDomainAdmin() {
require(msg.sender == _applyL1ToL2Alias(crossDomainAdmin), "ONLY_COUNTERPART_GATEWAY");
require(msg.sender == AddressUtils._applyL1ToL2Alias(crossDomainAdmin), "ONLY_COUNTERPART_GATEWAY");
_;
}

Expand Down Expand Up @@ -118,14 +119,5 @@ contract ZkSync_SpokePool is SpokePool {
emit SetZkBridge(address(_zkErc20Bridge), oldErc20Bridge);
}

// L1 addresses are transformed during l1->l2 calls.
// See https://github.com/matter-labs/era-contracts/blob/main/docs/Overview.md#mailboxfacet for more information.
// Another source: https://github.com/matter-labs/era-contracts/blob/41c25aa16d182f757c3fed1463c78a81896f65e6/ethereum/contracts/vendor/AddressAliasHelper.sol#L28
function _applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) {
unchecked {
l2Address = address(uint160(l1Address) + uint160(0x1111000000000000000000000000000000001111));
}
}

function _requireAdminSender() internal override onlyFromCrossDomainAdmin {}
}
153 changes: 30 additions & 123 deletions contracts/chain-adapters/Arbitrum_Adapter.sol
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";
Copy link
Contributor

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?

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,
Copy link
Member

Choose a reason for hiding this comment

The 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

View workflow job for this annotation

GitHub Actions / Lint (20)

Code contains empty blocks

Check warning on line 56 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Code contains empty blocks

/**
* @notice Send cross-chain message to target on Arbitrum.
Expand All @@ -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);
}

Expand All @@ -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;
}
}
Loading
Loading