Skip to content
Merged
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
57 changes: 46 additions & 11 deletions contracts/chain-adapters/Arbitrum_Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ pragma solidity ^0.8.0;

import "../interfaces/AdapterInterface.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface ArbitrumL1InboxLike {
function createRetryableTicket(
address destAddr,
Expand All @@ -25,6 +28,8 @@ interface ArbitrumL1ERC20GatewayLike {
uint256 _gasPriceBid,
bytes calldata _data
) external payable returns (bytes memory);

function getGateway(address _token) external view returns (address);
}

/**
Expand All @@ -35,35 +40,37 @@ interface ArbitrumL1ERC20GatewayLike {
* that call this contract's logic guard against reentrancy.
*/
contract Arbitrum_Adapter is AdapterInterface {
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 immutable l2MaxSubmissionCost = 0.1e18;
uint256 public immutable l2MaxSubmissionCost = 0.01e18;

// L2 Gas price bid for immediate L2 execution attempt (queryable via standard eth*gasPrice RPC)
uint256 public immutable l2GasPrice = 10e9; // 10 gWei
uint256 public immutable l2GasPrice = 5e9; // 5 gWei

// Gas limit for immediate L2 execution attempt (can be estimated via NodeInterface.estimateRetryableTicket).
// NodeInterface precompile interface exists at L2 address 0x00000000000000000000000000000000000000C8
uint32 public immutable l2GasLimit = 5_000_000;
uint32 public immutable l2GasLimit = 2_000_000;

// This address on L2 receives extra ETH that is left over after relaying a message via the inbox.
address public immutable l2RefundL2Address;

ArbitrumL1InboxLike public immutable l1Inbox;

ArbitrumL1ERC20GatewayLike public immutable l1ERC20Gateway;
ArbitrumL1ERC20GatewayLike public immutable l1ERC20GatewayRouter;

/**
* @notice Constructs new Adapter.
* @param _l1ArbitrumInbox Inbox helper contract to send messages to Arbitrum.
* @param _l1ERC20Gateway ERC20 gateway contract to send tokens to Arbitrum.
* @param _l1ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum.
*/
constructor(ArbitrumL1InboxLike _l1ArbitrumInbox, ArbitrumL1ERC20GatewayLike _l1ERC20Gateway) {
constructor(ArbitrumL1InboxLike _l1ArbitrumInbox, ArbitrumL1ERC20GatewayLike _l1ERC20GatewayRouter) {
l1Inbox = _l1ArbitrumInbox;
l1ERC20Gateway = _l1ERC20Gateway;
l1ERC20GatewayRouter = _l1ERC20GatewayRouter;

l2RefundL2Address = msg.sender;
}
Expand All @@ -75,9 +82,8 @@ contract Arbitrum_Adapter is AdapterInterface {
* @param target Contract on Arbitrum that will receive message.
* @param message Data to send to target.
*/
function relayMessage(address target, bytes calldata message) external payable override {
uint256 requiredL1CallValue = getL1CallValue();
require(address(this).balance >= requiredL1CallValue, "Insufficient ETH balance");
function relayMessage(address target, bytes memory message) external payable override {
uint256 requiredL1CallValue = _contractHasSufficientEthBalance();

l1Inbox.createRetryableTicket{ value: requiredL1CallValue }(
target, // destAddr destination L2 contract address
Expand All @@ -95,6 +101,8 @@ contract Arbitrum_Adapter is AdapterInterface {

/**
* @notice Bridge tokens to Arbitrum.
* @notice This contract must hold at least getL1CallValue() amount of ETH to send a message via the Inbox
* successfully, or the message will get stuck.
* @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.
Expand All @@ -106,7 +114,29 @@ contract Arbitrum_Adapter is AdapterInterface {
uint256 amount,
address to
) external payable override {
l1ERC20Gateway.outboundTransfer(l1Token, to, amount, l2GasLimit, l2GasPrice, "");
uint256 requiredL1CallValue = _contractHasSufficientEthBalance();

// 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 = l1ERC20GatewayRouter.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(l2MaxSubmissionCost, "");

// Note: outboundTransfer() will ultimately create a retryable ticket and set this contract's address as the
// refund address. This means that the excess ETH to pay for the L2 transaction will be sent to the aliased
// contract address on L2 and lost.
l1ERC20GatewayRouter.outboundTransfer{ value: requiredL1CallValue }(
l1Token,
to,
amount,
l2GasLimit,
l2GasPrice,
data
);

emit TokensRelayed(l1Token, l2Token, amount, to);
}

Expand All @@ -117,4 +147,9 @@ contract Arbitrum_Adapter is AdapterInterface {
function getL1CallValue() public pure returns (uint256) {
return l2MaxSubmissionCost + l2GasPrice * l2GasLimit;
}

function _contractHasSufficientEthBalance() internal view returns (uint256 requiredL1CallValue) {
requiredL1CallValue = getL1CallValue();
require(address(this).balance >= requiredL1CallValue, "Insufficient ETH balance");
}
}
2 changes: 1 addition & 1 deletion contracts/chain-adapters/Optimism_Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
*/
contract Optimism_Adapter is CrossDomainEnabled, AdapterInterface {
using SafeERC20 for IERC20;
uint32 public immutable l2GasLimit = 5_000_000;
uint32 public immutable l2GasLimit = 2_000_000;

WETH9 public immutable l1Weth;

Expand Down
19 changes: 19 additions & 0 deletions contracts/test/ArbitrumMocks.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

contract ArbitrumMockErc20GatewayRouter {
function outboundTransfer(
address _token,
address _to,
uint256 _amount,
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
) external payable returns (bytes memory) {
return _data;
}

function getGateway(address _token) external view returns (address) {
return address(this);
}
}
21 changes: 11 additions & 10 deletions scripts/buildSampleTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// test net.
// @dev Modify constants to modify merkle leaves. Command: `yarn hardhat run ./scripts/buildSampleTree.ts`

import { toWei, toBN, toBNWei, getParamType, defaultAbiCoder, keccak256 } from "../test/utils";
import { toBN, getParamType, defaultAbiCoder, keccak256, toBNWeiWithDecimals } from "../test/utils";
import { MerkleTree } from "../utils/MerkleTree";
import { RelayData } from "../test/fixtures/SpokePool.Fixture";

Expand All @@ -15,13 +15,14 @@ const RELAYER_REFUND_LEAF_COUNT = 1;
const SLOW_RELAY_LEAF_COUNT = 1;
const POOL_REBALANCE_NET_SEND_AMOUNT = 0.1; // Amount of tokens to send from HubPool to SpokePool
const RELAYER_REFUND_AMOUNT_TO_RETURN = 0.1; // Amount of tokens to send from SpokePool to HubPool
const L1_TOKEN = "0xd0A1E359811322d97991E03f863a0C30C2cF029C";
const L2_TOKEN = "0x4200000000000000000000000000000000000006";
const L1_TOKEN = "0x4dbcdf9b62e891a7cec5a2568c3f4faf9e8abe2b";
const L2_TOKEN = "0x1E77ad77925Ac0075CF61Fb76bA35D884985019d";
const DECIMALS = 6;
const RELAYER_REFUND_ADDRESS_TO_REFUND = "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d";
const RELAYER_REFUND_AMOUNT_TO_REFUND = 0.1; // Amount of tokens to send out of SpokePool to relayer refund recipient
const SLOW_RELAY_RECIPIENT_ADDRESS = "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d";
const SLOW_RELAY_AMOUNT = 0.1; // Amount of tokens to send out of SpokePool to slow relay recipient address
const SPOKE_POOL_CHAIN_ID = 69;
const SPOKE_POOL_CHAIN_ID = 421611;

function tuplelifyLeaf(leaf: Object) {
return JSON.stringify(
Expand All @@ -40,9 +41,9 @@ async function main() {
for (let i = 0; i < POOL_REBALANCE_LEAF_COUNT; i++) {
leaves.push({
chainId: toBN(SPOKE_POOL_CHAIN_ID),
bundleLpFees: [toBNWei(0.1)],
netSendAmounts: [toBNWei(POOL_REBALANCE_NET_SEND_AMOUNT)],
runningBalances: [toWei(0)],
bundleLpFees: [toBN(0)],
netSendAmounts: [toBNWeiWithDecimals(POOL_REBALANCE_NET_SEND_AMOUNT, DECIMALS)],
runningBalances: [toBN(0)],
groupIndex: toBN(0),
leafId: toBN(i),
l1Tokens: [L1_TOKEN],
Expand Down Expand Up @@ -81,9 +82,9 @@ async function main() {
const leaves: RelayerRefundLeaf[] = [];
for (let i = 0; i < RELAYER_REFUND_LEAF_COUNT; i++) {
leaves.push({
amountToReturn: toBNWei(RELAYER_REFUND_AMOUNT_TO_RETURN),
amountToReturn: toBNWeiWithDecimals(RELAYER_REFUND_AMOUNT_TO_RETURN, DECIMALS),
chainId: toBN(SPOKE_POOL_CHAIN_ID),
refundAmounts: [toBNWei(RELAYER_REFUND_AMOUNT_TO_REFUND)],
refundAmounts: [toBNWeiWithDecimals(RELAYER_REFUND_AMOUNT_TO_REFUND, DECIMALS)],
leafId: toBN(i),
l2TokenAddress: L2_TOKEN,
refundAddresses: [RELAYER_REFUND_ADDRESS_TO_REFUND],
Expand Down Expand Up @@ -125,7 +126,7 @@ async function main() {
depositor: SLOW_RELAY_RECIPIENT_ADDRESS,
recipient: SLOW_RELAY_RECIPIENT_ADDRESS,
destinationToken: L2_TOKEN,
amount: toBNWei(SLOW_RELAY_AMOUNT).toString(),
amount: toBNWeiWithDecimals(SLOW_RELAY_AMOUNT, DECIMALS).toString(),
originChainId: SPOKE_POOL_CHAIN_ID.toString(),
destinationChainId: SPOKE_POOL_CHAIN_ID.toString(),
realizedLpFeePct: "0",
Expand Down
10 changes: 9 additions & 1 deletion scripts/setupArbitrumSpokePool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @notice Logs ABI-encoded function data that can be relayed from HubPool to ArbitrumSpokePool to set it up.

import { getContractFactory, ethers } from "../test/utils";
import { getContractFactory, ethers, hre } from "../test/utils";
import * as consts from "../test/constants";

async function main() {
const [signer] = await ethers.getSigners();
Expand All @@ -12,6 +13,13 @@ async function main() {
"0xc778417e063141139fce010982780140aa0cd5ab", // L1 WETH
]);
console.log(`(WETH) whitelistToken: `, whitelistWeth);

// USDC is also not verified on the rinkeby explorer so we should approve it to be spent by the spoke pool.
const ERC20 = await getContractFactory("ExpandedERC20", { signer });
const usdc = await ERC20.attach("0x4dbcdf9b62e891a7cec5a2568c3f4faf9e8abe2b");
const deployedHubPool = await hre.deployments.get("HubPool");
const approval = await usdc.approve(deployedHubPool.address, consts.maxUint256);
console.log(`Approved USDC to be spent by HubPool @ ${deployedHubPool.address}: `, approval.hash);
}

main().then(
Expand Down
51 changes: 37 additions & 14 deletions test/chain-adapters/Arbitrum_Adapter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import * as consts from "../constants";
import { ethers, expect, Contract, FakeContract, SignerWithAddress, createFake, toWei, hre } from "../utils";
import {
ethers,
expect,
Contract,
FakeContract,
SignerWithAddress,
createFake,
toWei,
hre,
defaultAbiCoder,
toBN,
} from "../utils";
import { getContractFactory, seedWallet, randomAddress } from "../utils";
import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture";
import { constructSingleChainTree } from "../MerkleLib.utils";

let hubPool: Contract, arbitrumAdapter: Contract, weth: Contract, dai: Contract, timer: Contract, mockSpoke: Contract;
let l2Weth: string, l2Dai: string;
let l2Weth: string, l2Dai: string, gatewayAddress: string;
let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress;
let l1ERC20Gateway: FakeContract, l1Inbox: FakeContract;
let l1ERC20GatewayRouter: FakeContract, l1Inbox: FakeContract;

const arbitrumChainId = 42161;
let l1ChainId: number;

describe("Arbitrum Chain Adapter", function () {
beforeEach(async function () {
Expand All @@ -28,12 +38,13 @@ describe("Arbitrum Chain Adapter", function () {
await dai.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10));

l1Inbox = await createFake("Inbox");
l1ERC20Gateway = await createFake("TokenGateway");
l1ChainId = Number(await hre.getChainId());
l1ERC20GatewayRouter = await createFake("ArbitrumMockErc20GatewayRouter");
gatewayAddress = randomAddress();
l1ERC20GatewayRouter.getGateway.returns(gatewayAddress);

arbitrumAdapter = await (
await getContractFactory("Arbitrum_Adapter", owner)
).deploy(l1Inbox.address, l1ERC20Gateway.address);
).deploy(l1Inbox.address, l1ERC20GatewayRouter.address);

// Seed the HubPool some funds so it can send L1->L2 messages.
await hubPool.connect(liquidityProvider).loadEthForL2Calls({ value: toWei("1") });
Expand All @@ -48,9 +59,10 @@ describe("Arbitrum Chain Adapter", function () {
const newAdmin = randomAddress();
const functionCallData = mockSpoke.interface.encodeFunctionData("setCrossDomainAdmin", [newAdmin]);

expect(await hubPool.relaySpokePoolAdminFunction(arbitrumChainId, functionCallData))
.to.emit(arbitrumAdapter.attach(hubPool.address), "MessageRelayed")
.withArgs(mockSpoke.address, functionCallData);
expect(await hubPool.relaySpokePoolAdminFunction(arbitrumChainId, functionCallData)).to.changeEtherBalances(
[l1Inbox],
[toBN(consts.sampleL2MaxSubmissionCost).add(toBN(consts.sampleL2Gas).mul(consts.sampleL2GasPrice))]
);
expect(l1Inbox.createRetryableTicket).to.have.been.calledOnce;
expect(l1Inbox.createRetryableTicket).to.have.been.calledWith(
mockSpoke.address,
Expand All @@ -71,16 +83,27 @@ describe("Arbitrum Chain Adapter", function () {
.connect(dataWorker)
.proposeRootBundle([3117], 1, tree.getHexRoot(), consts.mockRelayerRefundRoot, consts.mockSlowRelayRoot);
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness + 1);
await hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0]));
expect(
await hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0]))
).to.changeEtherBalances(
[l1ERC20GatewayRouter],
[toBN(consts.sampleL2MaxSubmissionCost).add(toBN(consts.sampleL2Gas).mul(consts.sampleL2GasPrice))]
);

// The correct functions should have been called on the arbitrum contracts.
expect(l1ERC20Gateway.outboundTransfer).to.have.been.calledOnce; // One token transfer over the canonical bridge.
expect(l1ERC20Gateway.outboundTransfer).to.have.been.calledWith(
expect(l1ERC20GatewayRouter.outboundTransfer).to.have.been.calledOnce; // One token transfer over the canonical bridge.

// Adapter should have approved gateway to spend its ERC20.
expect(await dai.allowance(hubPool.address, gatewayAddress)).to.equal(tokensSendToL2);

const message = defaultAbiCoder.encode(["uint256", "bytes"], [consts.sampleL2MaxSubmissionCost, "0x"]);
expect(l1ERC20GatewayRouter.outboundTransfer).to.have.been.calledWith(
dai.address,
mockSpoke.address,
tokensSendToL2,
consts.sampleL2Gas,
consts.sampleL2GasPrice,
"0x"
message
);
expect(l1Inbox.createRetryableTicket).to.have.been.calledOnce; // only 1 L1->L2 message sent.
expect(l1Inbox.createRetryableTicket).to.have.been.calledWith(
Expand Down
2 changes: 0 additions & 2 deletions test/chain-adapters/Optimism_Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider:
let l1CrossDomainMessenger: FakeContract, l1StandardBridge: FakeContract;

const optimismChainId = 10;
let l1ChainId: number;

describe("Optimism Chain Adapter", function () {
beforeEach(async function () {
[owner, dataWorker, liquidityProvider] = await ethers.getSigners();
({ weth, dai, l2Weth, l2Dai, hubPool, mockSpoke, timer, mockAdapter } = await hubPoolFixture());
l1ChainId = Number(await hre.getChainId());
await seedWallet(dataWorker, [dai], weth, amountToLp);
await seedWallet(liquidityProvider, [dai], weth, amountToLp.mul(10));

Expand Down
7 changes: 4 additions & 3 deletions test/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ export const amountToReturn = toWei("1");

export const mockTreeRoot = createRandomBytes32();

export const sampleL2Gas = 5000000;
// Following should match variables set in Arbitrum_Adapter
export const sampleL2Gas = 2000000;

export const sampleL2MaxSubmissionCost = toWei("0.1");
export const sampleL2MaxSubmissionCost = toWei("0.01");

export const sampleL2GasPrice = 10e9; // 10 gWei
export const sampleL2GasPrice = 5e9;
6 changes: 6 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,14 @@ export function getAllFilesInPath(dirPath: string, arrayOfFiles: string[] = []):

export const toWei = (num: string | number | BigNumber) => ethers.utils.parseEther(num.toString());

export const toWeiWithDecimals = (num: string | number | BigNumber, decimals: number) =>
ethers.utils.parseUnits(num.toString(), decimals);

export const toBNWei = (num: string | number | BigNumber) => BigNumber.from(toWei(num));

export const toBNWeiWithDecimals = (num: string | number | BigNumber, decimals: number) =>
BigNumber.from(toWeiWithDecimals(num, decimals));

export const fromWei = (num: string | number | BigNumber) => ethers.utils.formatUnits(num.toString());

export const toBN = (num: string | number | BigNumber) => {
Expand Down