-
Notifications
You must be signed in to change notification settings - Fork 75
chrismaree/arbitrum l1 adapter #30
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
Changes from all commits
ba54cb9
84aad83
e971ff7
7a24fba
2f99c84
dcb052a
487e7ea
39ea052
febc4a6
1ba6779
1886cfd
439138b
b7e3ae2
1f6fc17
f64bf85
8bdcec7
89e642a
d6bc9bd
8de1b8f
98b2b52
eefd0c0
5de87a2
9d75d36
aca98e0
6307c53
6d87b1d
b03f37e
f45389b
b33a77b
12d0811
b02f2a8
03c4d00
b6cca33
d4d27fc
1480614
804bb41
ce3f29c
4d2e566
3736565
bea5162
8c1278b
da8b83d
96cc1a4
66a75f2
e0c3307
ad372f6
4c986c4
ff1f10c
c46e94f
b884d2f
505ee77
986df76
2238088
5bd1473
408223c
24928d5
3d74944
08281ac
0a79da6
13b022b
edce0cc
9a75854
3ded224
d6e55d8
112b322
485e9bd
7becd28
b2656c7
fe81d40
58c6e67
ac8ad23
cc4513e
cb35588
52d3ce5
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,130 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "./Base_Adapter.sol"; | ||
| import "../interfaces/AdapterInterface.sol"; | ||
| import "../interfaces/WETH9.sol"; | ||
|
|
||
| import "@uma/core/contracts/common/implementation/Lockable.sol"; | ||
|
|
||
| import "@openzeppelin/contracts/token/ERC20/IERC20.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); | ||
| } | ||
|
|
||
| contract Arbitrum_Adapter is Base_Adapter, Lockable { | ||
| // Gas limit for immediate L2 execution attempt (can be estimated via NodeInterface.estimateRetryableTicket). | ||
| // NodeInterface precompile interface exists at L2 address 0x00000000000000000000000000000000000000C8 | ||
| uint32 public l2GasLimit = 5_000_000; | ||
|
|
||
| // 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 l2MaxSubmissionCost = 0.1e18; | ||
|
|
||
| // L2 Gas price bid for immediate L2 execution attempt (queryable via standard eth*gasPrice RPC) | ||
| uint256 public l2GasPrice = 10e9; // 10 gWei | ||
|
|
||
| // This address on L2 receives extra ETH that is left over after relaying a message via the inbox. | ||
| address public l2RefundL2Address; | ||
|
|
||
| ArbitrumL1InboxLike public l1Inbox; | ||
|
|
||
| ArbitrumL1ERC20GatewayLike public l1ERC20Gateway; | ||
|
|
||
| event L2GasLimitSet(uint32 newL2GasLimit); | ||
|
|
||
| event L2MaxSubmissionCostSet(uint256 newL2MaxSubmissionCost); | ||
|
|
||
| event L2GasPriceSet(uint256 newL2GasPrice); | ||
|
|
||
| event L2RefundL2AddressSet(address newL2RefundL2Address); | ||
|
|
||
| constructor( | ||
| address _hubPool, | ||
| ArbitrumL1InboxLike _l1ArbitrumInbox, | ||
| ArbitrumL1ERC20GatewayLike _l1ERC20Gateway | ||
| ) Base_Adapter(_hubPool) { | ||
| l1Inbox = _l1ArbitrumInbox; | ||
| l1ERC20Gateway = _l1ERC20Gateway; | ||
|
|
||
| l2RefundL2Address = owner(); | ||
| } | ||
|
|
||
| function setL2GasLimit(uint32 _l2GasLimit) public onlyOwner { | ||
| l2GasLimit = _l2GasLimit; | ||
| emit L2GasLimitSet(l2GasLimit); | ||
| } | ||
|
|
||
| function setL2MaxSubmissionCost(uint256 _l2MaxSubmissionCost) public onlyOwner { | ||
| l2MaxSubmissionCost = _l2MaxSubmissionCost; | ||
| emit L2MaxSubmissionCostSet(l2MaxSubmissionCost); | ||
| } | ||
|
|
||
| function setL2GasPrice(uint256 _l2GasPrice) public onlyOwner { | ||
| l2GasPrice = _l2GasPrice; | ||
| emit L2GasPriceSet(l2GasPrice); | ||
| } | ||
|
|
||
| function setL2RefundL2Address(address _l2RefundL2Address) public onlyOwner { | ||
| l2RefundL2Address = _l2RefundL2Address; | ||
| emit L2RefundL2AddressSet(l2RefundL2Address); | ||
| } | ||
|
|
||
| function relayMessage(address target, bytes memory message) external payable override nonReentrant onlyHubPool { | ||
| uint256 requiredL1CallValue = getL1CallValue(); | ||
| require(address(this).balance >= requiredL1CallValue, "Insufficient ETH balance"); | ||
|
Member
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. WDYT about making this required L1 call value publicly queryable, for a potential EOA's convenience? We do this with the cross-chain oracle here but it may not be useful for this case if the HubPool deterministically decides when to send over ETH
Member
Author
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. ye, that's a good idea. it will also let us automate when to top up the contract. I'll refactor now.
Member
Author
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 dont, however, think this needs to be |
||
|
|
||
| l1Inbox.createRetryableTicket{ value: requiredL1CallValue }( | ||
| target, // destAddr destination L2 contract address | ||
| 0, // l2CallValue call value for retryable L2 message | ||
| l2MaxSubmissionCost, // maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee | ||
| l2RefundL2Address, // excessFeeRefundAddress maxgas x gasprice - execution cost gets credited here on L2 balance | ||
| l2RefundL2Address, // callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled | ||
| l2GasLimit, // maxGas Max gas deducted from user's L2 balance to cover L2 execution | ||
| l2GasPrice, // gasPriceBid price bid for L2 execution | ||
| message // data ABI encoded data of L2 message | ||
| ); | ||
|
|
||
| emit MessageRelayed(target, message); | ||
| } | ||
|
|
||
| function relayTokens( | ||
| address l1Token, | ||
| address l2Token, // l2Token is unused for Arbitrum. | ||
| uint256 amount, | ||
| address to | ||
| ) external payable override nonReentrant onlyHubPool { | ||
| l1ERC20Gateway.outboundTransfer(l1Token, to, amount, l2GasLimit, l2GasPrice, ""); | ||
| emit TokensRelayed(l1Token, l2Token, amount, to); | ||
| } | ||
|
|
||
| function getL1CallValue() public view returns (uint256) { | ||
| return l2MaxSubmissionCost + l2GasPrice * l2GasLimit; | ||
| } | ||
|
|
||
| receive() external payable {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import * as consts from "../constants"; | ||
| import { ethers, expect, Contract, FakeContract, SignerWithAddress, createFake, toWei } from "../utils"; | ||
| import { getContractFactory, seedWallet } from "../utils"; | ||
| import { hubPoolFixture, enableTokensForLP } from "../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 owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; | ||
| let l1ERC20Gateway: FakeContract, l1Inbox: FakeContract; | ||
|
|
||
| const arbitrumChainId = 42161; | ||
|
|
||
| describe("Arbitrum Chain Adapter", function () { | ||
| beforeEach(async function () { | ||
| [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); | ||
| ({ weth, dai, l2Weth, l2Dai, hubPool, mockSpoke, timer } = await hubPoolFixture()); | ||
| await seedWallet(dataWorker, [dai], weth, consts.amountToLp); | ||
| await seedWallet(liquidityProvider, [dai], weth, consts.amountToLp.mul(10)); | ||
|
|
||
| await enableTokensForLP(owner, hubPool, weth, [weth, dai]); | ||
| await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp); | ||
| await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10)); | ||
| await dai.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(dai.address, consts.amountToLp); | ||
| await dai.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10)); | ||
|
|
||
| l1Inbox = await createFake("Inbox"); | ||
| l1ERC20Gateway = await createFake("TokenGateway"); | ||
|
|
||
| arbitrumAdapter = await ( | ||
| await getContractFactory("Arbitrum_Adapter", owner) | ||
| ).deploy(hubPool.address, l1Inbox.address, l1ERC20Gateway.address); | ||
|
|
||
| // Seed the Arbitrum adapter with some funds so it can send L1->L2 messages. | ||
| await liquidityProvider.sendTransaction({ to: arbitrumAdapter.address, value: toWei("1") }); | ||
|
|
||
| await hubPool.setCrossChainContracts(arbitrumChainId, arbitrumAdapter.address, mockSpoke.address); | ||
|
|
||
| await hubPool.whitelistRoute(arbitrumChainId, weth.address, l2Weth); | ||
|
|
||
| await hubPool.whitelistRoute(arbitrumChainId, dai.address, l2Dai); | ||
| }); | ||
|
|
||
| it("Only owner can set l2GasValues", async function () { | ||
| expect(await arbitrumAdapter.callStatic.l2GasLimit()).to.equal(consts.sampleL2Gas); | ||
| await expect(arbitrumAdapter.connect(liquidityProvider).setL2GasLimit(consts.sampleL2Gas + 1)).to.be.reverted; | ||
| await arbitrumAdapter.connect(owner).setL2GasLimit(consts.sampleL2Gas + 1); | ||
| expect(await arbitrumAdapter.callStatic.l2GasLimit()).to.equal(consts.sampleL2Gas + 1); | ||
| }); | ||
|
|
||
| it("Only owner can set l2MaxSubmissionCost", async function () { | ||
| expect(await arbitrumAdapter.callStatic.l2MaxSubmissionCost()).to.equal(consts.sampleL2MaxSubmissionCost); | ||
| await expect(arbitrumAdapter.connect(liquidityProvider).setL2MaxSubmissionCost(consts.sampleL2Gas + 1)).to.be | ||
| .reverted; | ||
| await arbitrumAdapter.connect(owner).setL2MaxSubmissionCost(consts.sampleL2Gas + 1); | ||
| expect(await arbitrumAdapter.callStatic.l2MaxSubmissionCost()).to.equal(consts.sampleL2Gas + 1); | ||
| }); | ||
|
|
||
| it("Only owner can set l2GasPrice", async function () { | ||
| expect(await arbitrumAdapter.callStatic.l2GasPrice()).to.equal(consts.sampleL2GasPrice); | ||
| await expect(arbitrumAdapter.connect(liquidityProvider).setL2GasPrice(consts.sampleL2Gas + 1)).to.be.reverted; | ||
| await arbitrumAdapter.connect(owner).setL2GasPrice(consts.sampleL2Gas + 1); | ||
| expect(await arbitrumAdapter.callStatic.l2GasPrice()).to.equal(consts.sampleL2Gas + 1); | ||
| }); | ||
|
|
||
| it("Only owner can set l2RefundL2Address", async function () { | ||
| expect(await arbitrumAdapter.callStatic.l2RefundL2Address()).to.equal(owner.address); | ||
| await expect(arbitrumAdapter.connect(liquidityProvider).setL2RefundL2Address(liquidityProvider.address)).to.be | ||
| .reverted; | ||
| await arbitrumAdapter.connect(owner).setL2RefundL2Address(liquidityProvider.address); | ||
| expect(await arbitrumAdapter.callStatic.l2RefundL2Address()).to.equal(liquidityProvider.address); | ||
| }); | ||
| it("Correctly calls appropriate arbitrum bridge functions when making ERC20 cross chain calls", async function () { | ||
| // Create an action that will send an L1->L2 tokens transfer and bundle. For this, create a relayer repayment bundle | ||
| // and check that at it's finalization the L2 bridge contracts are called as expected. | ||
| const { leafs, tree, tokensSendToL2 } = await constructSingleChainTree(dai, 1, arbitrumChainId); | ||
| await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), consts.mockTreeRoot); | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness); | ||
| await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); | ||
| // 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( | ||
| dai.address, | ||
| mockSpoke.address, | ||
| tokensSendToL2, | ||
| consts.sampleL2Gas, | ||
| consts.sampleL2GasPrice, | ||
| "0x" | ||
| ); | ||
| expect(l1Inbox.createRetryableTicket).to.have.been.calledOnce; // only 1 L1->L2 message sent. | ||
| expect(l1Inbox.createRetryableTicket).to.have.been.calledWith( | ||
| mockSpoke.address, | ||
| 0, | ||
| consts.sampleL2MaxSubmissionCost, | ||
| owner.address, | ||
| owner.address, | ||
| consts.sampleL2Gas, | ||
| consts.sampleL2GasPrice, | ||
| mockSpoke.interface.encodeFunctionData("initializeRelayerRefund", [consts.mockTreeRoot]) | ||
| ); | ||
| }); | ||
| }); |
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.
Do you think we'' have an EOA send ETH to this contract prior to cross chain calls, or will the HubPool send ETH over?
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.
EOA just drops funds on it I think.