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
117 changes: 117 additions & 0 deletions contracts/Arbitrum_SpokePool.sol
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 {
Copy link
Member

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.

Copy link
Member Author

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

// 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;
Copy link
Member

Choose a reason for hiding this comment

The 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));
}
}
28 changes: 25 additions & 3 deletions contracts/HubPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand Down Expand Up @@ -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),
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm what should we use?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix in #35

true
)
);
emit WhitelistRoute(destinationChainId, originToken, destinationToken);
}

Expand Down Expand Up @@ -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");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this check? elsewhere in _executeRelayerRefundOnChain for example, we don't check that the adapter address is non0


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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 17 additions & 20 deletions contracts/Optimism_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import "./interfaces/WETH9.sol";
import "@eth-optimism/contracts/libraries/bridge/CrossDomainEnabled.sol";
import "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol";
import "@eth-optimism/contracts/L2/messaging/IL2ERC20Bridge.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

import "./SpokePool.sol";
import "./SpokePoolInterface.sol";

Expand All @@ -17,7 +14,7 @@ import "./SpokePoolInterface.sol";
* @dev Uses OVM cross-domain-enabled logic for access control.
*/

contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool, Ownable {
contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool {
// "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via `IL2ERC20Bridge`.
uint32 public l1Gas = 5_000_000;

Expand All @@ -26,6 +23,7 @@ contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool
address public l2Eth;

event OptimismTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged, uint256 l1Gas);
event SetL1Gas(uint32 indexed newL1Gas);

constructor(
address _l1EthWrapper,
Expand All @@ -39,24 +37,14 @@ contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool
SpokePool(_crossDomainAdmin, _hubPool, _wethAddress, timerAddress)
{}

/**************************************
* ADMIN FUNCTIONS *
**************************************/
function setL1GasLimit(uint32 newl1Gas) public onlyOwner nonReentrant {
l1Gas = newl1Gas;
}

/**************************************
* CROSS-CHAIN ADMIN FUNCTIONS *
**************************************/

/**
* @notice Changes the L1 contract that can trigger admin functions on this contract.
* @dev This should be set to the address of the L1 contract that ultimately relays a cross-domain message, which
* is expected to be the Optimism_Adapter.
* @dev Only callable by the existing admin via the Optimism cross domain messenger.
* @param newCrossDomainAdmin address of the new L1 admin contract.
*/
function setL1GasLimit(uint32 newl1Gas) public onlyFromCrossDomainAccount(crossDomainAdmin) {
_setL1GasLimit(newl1Gas);
}

function setCrossDomainAdmin(address newCrossDomainAdmin)
public
override
Expand Down Expand Up @@ -96,6 +84,15 @@ contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool
_initializeRelayerRefund(relayerRepaymentDistributionRoot, slowRelayRoot);
}

/**************************************
* INTERNAL FUNCTIONS *
**************************************/

function _setL1GasLimit(uint32 _l1Gas) internal {
l1Gas = _l1Gas;
emit SetL1Gas(l1Gas);
}

function _bridgeTokensToHubPool(DestinationDistributionLeaf memory distributionLeaf) internal override {
// If the token being bridged is WETH then we need to first unwrap it to ETH and then send ETH over the
// canonical bridge. On Optimism, this is address 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000.
Expand All @@ -105,8 +102,8 @@ contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool
}
IL2ERC20Bridge(Lib_PredeployAddresses.L2_STANDARD_BRIDGE).withdrawTo(
distributionLeaf.l2TokenAddress, // _l2Token. Address of the L2 token to bridge over.
hubPool, // _to. Withdraw, over the bridge, to the l1 withdraw contract.
distributionLeaf.amountToReturn, // _amount. Send the full balance of the deposit box to bridge.
hubPool, // _to. Withdraw, over the bridge, to the l1 pool contract.
distributionLeaf.amountToReturn, // _amount.
l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
"" // _data. We don't need to send any data for the bridging action.
);
Expand Down
50 changes: 50 additions & 0 deletions contracts/chain-adapters/L1_Adapter.sol
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 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mrice32 @chrismaree I originally added this because I wanted to test HubPool.relaySpokePoolAdminFunction() in the simplest way and check that it can delegate a call to another contract, so I figured why not just build this L1 adapter to kill two birds with one stone

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 {}
}
5 changes: 3 additions & 2 deletions test/HubPool.Admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neccessary now since whitelistRoute calls relayMessage

await expect(hubPool.whitelistRoute(destinationChainId, weth.address, usdc.address))
.to.emit(hubPool, "WhitelistRoute")
.withArgs(destinationChainId, weth.address, usdc.address);
Expand Down
14 changes: 13 additions & 1 deletion test/HubPool.Fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,19 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) =
await hubPool.whitelistRoute(repaymentChainId, dai.address, l2Dai);
await hubPool.whitelistRoute(repaymentChainId, usdc.address, l2Usdc);

return { weth, usdc, dai, hubPool, mockAdapter, mockSpoke, l2Weth, l2Dai, l2Usdc, ...parentFixtureOutput };
return {
weth,
usdc,
dai,
hubPool,
mockAdapter,
mockSpoke,
l2Weth,
l2Dai,
l2Usdc,
crossChainAdmin,
...parentFixtureOutput,
};
});

export async function enableTokensForLP(owner: Signer, hubPool: Contract, weth: Contract, tokens: Contract[]) {
Expand Down
7 changes: 4 additions & 3 deletions test/HubPool.RefundExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ describe("HubPool Relayer Refund Execution", function () {

// Check the mockAdapter was called with the correct arguments for each method.
const relayMessageEvents = await mockAdapter.queryFilter(mockAdapter.filters.RelayMessageCalled());
expect(relayMessageEvents.length).to.equal(1); // Exactly one message send from L1->L2.
expect(relayMessageEvents[0].args?.target).to.equal(mockSpoke.address);
expect(relayMessageEvents[0].args?.message).to.equal(
expect(relayMessageEvents.length).to.equal(4); // Exactly four message send from L1->L2. 3 for each whitelist route
// and 1 for the initiateRelayerRefund.
expect(relayMessageEvents[relayMessageEvents.length - 1].args?.target).to.equal(mockSpoke.address);
expect(relayMessageEvents[relayMessageEvents.length - 1].args?.message).to.equal(
mockSpoke.interface.encodeFunctionData("initializeRelayerRefund", [
consts.mockDestinationDistributionRoot,
consts.mockSlowRelayFulfillmentRoot,
Expand Down
Loading