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
27 changes: 1 addition & 26 deletions contracts/chain-adapters/Arbitrum_RescueAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,11 @@
pragma solidity ^0.8.0;

import "../interfaces/AdapterInterface.sol";
import "./Arbitrum_Adapter.sol"; // Used to import `ArbitrumL1ERC20GatewayLike` and `ArbitrumL1InboxLike`

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

interface ArbitrumL1InboxLike {
function createRetryableTicket(
address destAddr,
uint256 arbTxCallValue,
uint256 maxSubmissionCost,
address submissionRefundAddress,
address valueRefundAddress,
uint256 maxGas,
uint256 gasPriceBid,
bytes calldata data
) external payable returns (uint256);
}

interface ArbitrumL1ERC20GatewayLike {
function outboundTransfer(
address _token,
address _to,
uint256 _amount,
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
) external payable returns (bytes memory);

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

/**
* @notice Meant to copy the Arbitrum_Adapter exactly in how it sends L1 --> L2 messages but is designed only to be
* used by the owner of the HubPool to retrieve ETH held by its aliased address on L2. This ETH builds up because
Expand Down
93 changes: 93 additions & 0 deletions contracts/chain-adapters/Arbitrum_SendTokensAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import "../interfaces/AdapterInterface.sol";
import "./Arbitrum_Adapter.sol"; // Used to import `ArbitrumL1ERC20GatewayLike`

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

/**
* @notice This adapter is built for emergencies to send funds from the Hub to a Spoke in the event that a spoke pool
* received a duplicate root bundle relay, due to some replay issue.
*/
// solhint-disable-next-line contract-name-camelcase
contract Arbitrum_SendTokensAdapter is AdapterInterface {
using SafeERC20 for IERC20;

uint256 public immutable l2MaxSubmissionCost = 0.01e18;
uint256 public immutable l2GasPrice = 5e9;
uint32 public immutable l2GasLimit = 2_000_000;

ArbitrumL1ERC20GatewayLike public immutable l1ERC20GatewayRouter;

/**
* @notice Constructs new Adapter.
* @param _l1ERC20GatewayRouter ERC20 gateway router contract to send tokens to Arbitrum.
*/
constructor(ArbitrumL1ERC20GatewayLike _l1ERC20GatewayRouter) {
l1ERC20GatewayRouter = _l1ERC20GatewayRouter;
}

/**
* @notice Send tokens to SpokePool. Enables HubPool admin to call relaySpokePoolAdminFunction that will trigger
* this function.
* @dev This performs similar logic to relayTokens in the normal Arbitrum_Adapter by sending tokens
* the Arbitrum_SpokePool out of the HubPool.
* @param message The encoded address of the ERC20 to send to the rescue address.
*/
function relayMessage(address target, bytes memory message) external payable override {
(address l1Token, uint256 amount) = abi.decode(message, (address, uint256));

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,
target,
amount,
l2GasLimit,
l2GasPrice,
data
);

// Purposefully not emitting any events so as not to confuse off-chain monitors that track this event.
// emit TokensRelayed(l1Token, l2Token, amount, to);
}

/**
* @notice Should never be called.
*/
function relayTokens(
address,
address,
uint256,
address
) external payable override {
revert("relayTokens disabled");
}

/**
* @notice Returns required amount of ETH to send a message via the Inbox.
* @return amount of ETH that this contract needs to hold in order for relayMessage to succeed.
*/
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");
}
}
68 changes: 68 additions & 0 deletions test/chain-adapters/Arbitrum_SendTokensAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as consts from "../constants";
import {
ethers,
expect,
Contract,
FakeContract,
SignerWithAddress,
createFake,
toWei,
defaultAbiCoder,
toBN,
} from "../utils";
import { getContractFactory, seedWallet, randomAddress } from "../utils";
import { hubPoolFixture } from "../fixtures/HubPool.Fixture";

let hubPool: Contract, arbitrumAdapter: Contract, weth: Contract, mockSpoke: Contract;
let gatewayAddress: string;
let owner: SignerWithAddress, liquidityProvider: SignerWithAddress;
let l1ERC20GatewayRouter: FakeContract;

const arbitrumChainId = 42161;

describe("Arbitrum Chain SendTokens Emergency Adapter", function () {
beforeEach(async function () {
[owner, liquidityProvider] = await ethers.getSigners();
({ weth, hubPool, mockSpoke } = await hubPoolFixture());

// Send tokens to HubPool directly.
await seedWallet(owner, [], weth, consts.amountToLp);
await weth.transfer(hubPool.address, consts.amountToLp);

l1ERC20GatewayRouter = await createFake("ArbitrumMockErc20GatewayRouter");
gatewayAddress = randomAddress();
l1ERC20GatewayRouter.getGateway.returns(gatewayAddress);

arbitrumAdapter = await (
await getContractFactory("Arbitrum_SendTokensAdapter", owner)
).deploy(l1ERC20GatewayRouter.address);

// Seed the HubPool some funds so it can send L1->L2 messages.
await hubPool.connect(liquidityProvider).loadEthForL2Calls({ value: toWei("1") });
await hubPool.setCrossChainContracts(arbitrumChainId, arbitrumAdapter.address, mockSpoke.address);
});

it("relayMessage sends desired ERC20 in specified amount to SpokePool", async function () {
const tokensToSendToL2 = consts.amountToLp;
const message = defaultAbiCoder.encode(["address", "uint256"], [weth.address, tokensToSendToL2]);

expect(await hubPool.relaySpokePoolAdminFunction(arbitrumChainId, message)).to.changeEtherBalances(
[l1ERC20GatewayRouter],
[toBN(consts.sampleL2MaxSubmissionCost).add(toBN(consts.sampleL2Gas).mul(consts.sampleL2GasPrice))]
);
expect(l1ERC20GatewayRouter.outboundTransfer).to.have.been.calledOnce;
expect(await weth.allowance(hubPool.address, gatewayAddress)).to.equal(tokensToSendToL2);
const maxSubmissionCostMessage = defaultAbiCoder.encode(
["uint256", "bytes"],
[consts.sampleL2MaxSubmissionCost, "0x"]
);
expect(l1ERC20GatewayRouter.outboundTransfer).to.have.been.calledWith(
weth.address,
mockSpoke.address,
tokensToSendToL2,
consts.sampleL2Gas,
consts.sampleL2GasPrice,
maxSubmissionCostMessage
);
});
});