-
Notifications
You must be signed in to change notification settings - Fork 75
feat: Add Arbitrum Spokepool and add generic cross-chain admin relayer function on HubPool #33
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
830bcdf
cfc18cb
d8b0552
b95bfbb
ebee27e
a1cd61e
9c7a71b
cc034f1
3b92a3b
b8de414
1a40713
b0efeb0
be20625
7adc509
938cb44
d54aa2f
66fb38b
3bb8a2a
a39be0e
815d384
ac5d1fd
f9744b8
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,117 @@ | ||
| //SPDX-License-Identifier: Unlicense | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "./SpokePool.sol"; | ||
| import "./SpokePoolInterface.sol"; | ||
|
|
||
| interface StandardBridgeLike { | ||
| function outboundTransfer( | ||
| address _l1Token, | ||
| address _to, | ||
| uint256 _amount, | ||
| bytes calldata _data | ||
| ) external payable returns (bytes memory); | ||
| } | ||
|
|
||
| /** | ||
| * @notice AVM specific SpokePool. | ||
| * @dev Uses AVM cross-domain-enabled logic for access control. | ||
| */ | ||
|
|
||
| contract Arbitrum_SpokePool is SpokePoolInterface, SpokePool { | ||
| // Address of the Arbitrum L2 token gateway. | ||
| address public l2GatewayRouter; | ||
|
|
||
| // Admin controlled mapping of arbitrum tokens to L1 counterpart. L1 counterpart addresses | ||
| // are neccessary to bridge tokens to L1. | ||
| mapping(address => address) public whitelistedTokens; | ||
|
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. pain that we need this just for arbitrum but works kinda well given we have separate contracts for each chain. |
||
|
|
||
| event ArbitrumTokensBridged(address indexed l1Token, address target, uint256 numberOfTokensBridged); | ||
| event SetL2GatewayRouter(address indexed newL2GatewayRouter); | ||
| event WhitelistedTokens(address indexed l2Token, address indexed l1Token); | ||
|
|
||
| constructor( | ||
| address _l2GatewayRouter, | ||
| address _crossDomainAdmin, | ||
| address _hubPool, | ||
| address _wethAddress, | ||
| address timerAddress | ||
| ) SpokePool(_crossDomainAdmin, _hubPool, _wethAddress, timerAddress) { | ||
| _setL2GatewayRouter(_l2GatewayRouter); | ||
| } | ||
|
|
||
| modifier onlyFromCrossDomainAdmin() { | ||
| require(msg.sender == _applyL1ToL2Alias(crossDomainAdmin), "ONLY_COUNTERPART_GATEWAY"); | ||
| _; | ||
| } | ||
|
|
||
| /************************************** | ||
| * CROSS-CHAIN ADMIN FUNCTIONS * | ||
| **************************************/ | ||
|
|
||
| function setL2GatewayRouter(address newL2GatewayRouter) public onlyFromCrossDomainAdmin nonReentrant { | ||
| _setL2GatewayRouter(newL2GatewayRouter); | ||
| } | ||
|
|
||
| function whitelistToken(address l2Token, address l1Token) public onlyFromCrossDomainAdmin nonReentrant { | ||
| _whitelistToken(l2Token, l1Token); | ||
| } | ||
|
|
||
| function setCrossDomainAdmin(address newCrossDomainAdmin) public override onlyFromCrossDomainAdmin nonReentrant { | ||
| _setCrossDomainAdmin(newCrossDomainAdmin); | ||
| } | ||
|
|
||
| function setHubPool(address newHubPool) public override onlyFromCrossDomainAdmin nonReentrant { | ||
| _setHubPool(newHubPool); | ||
| } | ||
|
|
||
| function setEnableRoute( | ||
| address originToken, | ||
| uint32 destinationChainId, | ||
| bool enable | ||
| ) public override onlyFromCrossDomainAdmin nonReentrant { | ||
| _setEnableRoute(originToken, destinationChainId, enable); | ||
| } | ||
|
|
||
| function setDepositQuoteTimeBuffer(uint32 buffer) public override onlyFromCrossDomainAdmin nonReentrant { | ||
| _setDepositQuoteTimeBuffer(buffer); | ||
| } | ||
|
|
||
| function initializeRelayerRefund(bytes32 relayerRepaymentDistributionRoot, bytes32 slowRelayRoot) | ||
| public | ||
| override | ||
| onlyFromCrossDomainAdmin | ||
| nonReentrant | ||
| { | ||
| _initializeRelayerRefund(relayerRepaymentDistributionRoot, slowRelayRoot); | ||
| } | ||
|
|
||
| /************************************** | ||
| * INTERNAL FUNCTIONS * | ||
| **************************************/ | ||
|
|
||
| function _bridgeTokensToHubPool(DestinationDistributionLeaf memory distributionLeaf) internal override { | ||
| StandardBridgeLike(l2GatewayRouter).outboundTransfer( | ||
| whitelistedTokens[distributionLeaf.l2TokenAddress], // _l1Token. Address of the L1 token to bridge over. | ||
| hubPool, // _to. Withdraw, over the bridge, to the l1 hub pool contract. | ||
| distributionLeaf.amountToReturn, // _amount. | ||
| "" // _data. We don't need to send any data for the bridging action. | ||
| ); | ||
| emit ArbitrumTokensBridged(address(0), hubPool, distributionLeaf.amountToReturn); | ||
| } | ||
|
|
||
| function _setL2GatewayRouter(address _l2GatewayRouter) internal { | ||
| l2GatewayRouter = _l2GatewayRouter; | ||
| emit SetL2GatewayRouter(l2GatewayRouter); | ||
| } | ||
|
|
||
| function _whitelistToken(address _l2Token, address _l1Token) internal { | ||
| whitelistedTokens[_l2Token] = _l1Token; | ||
| 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. | ||
| function _applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { | ||
| l2Address = address(uint160(l1Address) + uint160(0x1111000000000000000000000000000000001111)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,6 +143,7 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| int256[] runningBalance, | ||
| address indexed caller | ||
| ); | ||
| event SpokePoolAdminFunctionTriggered(uint256 indexed chainId, bytes message); | ||
|
|
||
| event RelayerRefundDisputed(address indexed disputer, uint256 requestTime, bytes disputedAncillaryData); | ||
|
|
||
|
|
@@ -167,6 +168,12 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| * ADMIN FUNCTIONS * | ||
| *************************************************/ | ||
|
|
||
| // This function has permission to call onlyFromCrossChainAdmin functions on the SpokePool, so its imperative | ||
| // that this contract only allows the owner to call this method directly or indirectly. | ||
| function relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData) public onlyOwner nonReentrant { | ||
| _relaySpokePoolAdminFunction(chainId, functionData); | ||
| } | ||
|
|
||
| function setProtocolFeeCapture(address newProtocolFeeCaptureAddress, uint256 newProtocolFeeCapturePct) | ||
| public | ||
| onlyOwner | ||
|
|
@@ -211,9 +218,16 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| address originToken, | ||
| address destinationToken | ||
| ) public onlyOwner { | ||
| // Note that this method makes no L1->L1 call to whitelist the route. The assumption is that the origin chain's | ||
| // SpokePool Owner will call setEnableRoute to enable the route. This removes the need for an L1->L2 call. | ||
| whitelistedRoutes[originToken][destinationChainId] = destinationToken; | ||
| relaySpokePoolAdminFunction( | ||
| destinationChainId, | ||
| abi.encodeWithSignature( | ||
| "setEnableRoute(address,uint32,bool)", | ||
| originToken, | ||
| uint32(destinationChainId), | ||
|
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. unrelated to this PR but I think that the unit32 is too small.
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. Hm what should we use?
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. EIP 155 doesn't list the highest value for this so I guess its safe to just use 256
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. Will fix in #35 |
||
| true | ||
| ) | ||
| ); | ||
| emit WhitelistRoute(destinationChainId, originToken, destinationToken); | ||
| } | ||
|
|
||
|
|
@@ -509,7 +523,6 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| uint256[] memory bundleLpFees | ||
| ) internal { | ||
| AdapterInterface adapter = crossChainContracts[chainId].adapter; | ||
| require(address(adapter) != address(0), "Adapter not set for target chain"); | ||
|
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. do we need this check? elsewhere in |
||
|
|
||
| for (uint32 i = 0; i < l1Tokens.length; i++) { | ||
| // Validate the L1 -> L2 token route is whitelisted. If it is not then the output of the bridging action | ||
|
|
@@ -637,6 +650,15 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| if (protocolFeesCaptured > 0) unclaimedAccumulatedProtocolFees[l1Token] += protocolFeesCaptured; | ||
| } | ||
|
|
||
| function _relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData) internal { | ||
| AdapterInterface adapter = crossChainContracts[chainId].adapter; | ||
| adapter.relayMessage( | ||
| crossChainContracts[chainId].spokePool, // target. This should be the spokePool on the L2. | ||
| functionData | ||
| ); | ||
| emit SpokePoolAdminFunctionTriggered(chainId, functionData); | ||
| } | ||
|
|
||
| // If functionCallStackOriginatesFromOutsideThisContract is true then this was called by the callback function | ||
| // by dropping ETH onto the contract. In this case, deposit the ETH into WETH. This would happen if ETH was sent | ||
| // over the optimism bridge, for example. If false then this was set as a result of unwinding LP tokens, with the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // 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"; | ||
| import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
|
||
| contract L1_Adapter is Base_Adapter, Lockable { | ||
|
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. @mrice32 @chrismaree I originally added this because I wanted to test |
||
| using SafeERC20 for IERC20; | ||
|
|
||
| constructor(address _hubPool) Base_Adapter(_hubPool) {} | ||
|
|
||
| function relayMessage(address target, bytes memory message) external payable override nonReentrant onlyHubPool { | ||
| _executeCall(target, message); | ||
| emit MessageRelayed(target, message); | ||
| } | ||
|
|
||
| function relayTokens( | ||
| address l1Token, | ||
| address l2Token, // l2Token is unused for L1. | ||
| uint256 amount, | ||
| address to | ||
| ) external payable override nonReentrant onlyHubPool { | ||
| IERC20(l1Token).safeTransfer(to, amount); | ||
| emit TokensRelayed(l1Token, l2Token, amount, to); | ||
| } | ||
|
|
||
| // Note: this snippet of code is copied from Governor.sol. | ||
| function _executeCall(address to, bytes memory data) private { | ||
| // Note: this snippet of code is copied from Governor.sol and modified to not include any "value" field. | ||
| // solhint-disable-next-line no-inline-assembly | ||
|
|
||
| bool success; | ||
| assembly { | ||
| let inputData := add(data, 0x20) | ||
| let inputDataSize := mload(data) | ||
| // Hardcode value to be 0 for relayed governance calls in order to avoid addressing complexity of bridging | ||
| // value cross-chain. | ||
| success := call(gas(), to, 0, inputData, inputDataSize, 0, 0) | ||
| } | ||
| require(success, "execute call failed"); | ||
| } | ||
|
|
||
| receive() external payable {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,13 @@ import { getContractFactory, SignerWithAddress, seedWallet, expect, Contract, et | |
| import { destinationChainId, bondAmount, zeroAddress, mockTreeRoot, mockSlowRelayFulfillmentRoot } from "./constants"; | ||
| import { hubPoolFixture } from "./HubPool.Fixture"; | ||
|
|
||
| let hubPool: Contract, weth: Contract, usdc: Contract; | ||
| let hubPool: Contract, weth: Contract, usdc: Contract, mockSpoke: Contract, mockAdapter: Contract; | ||
| let owner: SignerWithAddress, other: SignerWithAddress; | ||
|
|
||
| describe("HubPool Admin functions", function () { | ||
| beforeEach(async function () { | ||
| [owner, other] = await ethers.getSigners(); | ||
| ({ weth, hubPool, usdc } = await hubPoolFixture()); | ||
| ({ weth, hubPool, usdc, mockAdapter, mockSpoke } = await hubPoolFixture()); | ||
| }); | ||
|
|
||
| it("Can add L1 token to whitelisted lpTokens mapping", async function () { | ||
|
|
@@ -35,6 +35,7 @@ describe("HubPool Admin functions", function () { | |
| await expect(hubPool.connect(other).disableL1TokenForLiquidityProvision(weth.address)).to.be.reverted; | ||
| }); | ||
| it("Can whitelist route for deposits and rebalances", async function () { | ||
| await hubPool.setCrossChainContracts(destinationChainId, mockAdapter.address, mockSpoke.address); | ||
|
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. Neccessary now since |
||
| await expect(hubPool.whitelistRoute(destinationChainId, weth.address, usdc.address)) | ||
| .to.emit(hubPool, "WhitelistRoute") | ||
| .withArgs(destinationChainId, weth.address, usdc.address); | ||
|
|
||
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 does not have any tests. shall we add that in this PR or a separate one? we also need to write OP tests, which can happen either at the same time, if we choose to wait on adding the tests here.
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.
Let's do separate PR