Skip to content
Merged
57 changes: 39 additions & 18 deletions contracts/Optimism_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity ^0.8.0;

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 @@ -11,27 +13,33 @@ import "./SpokePoolInterface.sol";
* @dev Uses OVM cross-domain-enabled logic for access control.
*/

contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool {
// Address of the L1 contract that acts as the owner of this SpokePool.
address public override crossDomainAdmin;
contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool, Ownable {
// "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via `IL2ERC20Bridge`.
uint32 l1Gas = 6_000_000;

event SetXDomainAdmin(address indexed newAdmin);
event OptimismTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged, uint256 l1Gas);

constructor(
address _crossDomainAdmin,
address _hubPool,
address _wethAddress,
uint64 _depositQuoteTimeBuffer,
address timerAddress
)
CrossDomainEnabled(Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER)
SpokePool(_wethAddress, _depositQuoteTimeBuffer, timerAddress)
{
_setCrossDomainAdmin(_crossDomainAdmin);
}
SpokePool(_crossDomainAdmin, _hubPool, _wethAddress, _depositQuoteTimeBuffer, 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.
Expand All @@ -44,37 +52,50 @@ contract Optimism_SpokePool is CrossDomainEnabled, SpokePoolInterface, SpokePool
public
override
onlyFromCrossDomainAccount(crossDomainAdmin)
nonReentrant
{
_setCrossDomainAdmin(newCrossDomainAdmin);
}

function setHubPool(address newHubPool) public override onlyFromCrossDomainAccount(crossDomainAdmin) nonReentrant {
_setHubPool(newHubPool);
}

function setEnableRoute(
address originToken,
uint256 destinationChainId,
bool enable
) public override onlyFromCrossDomainAccount(crossDomainAdmin) {
) public override onlyFromCrossDomainAccount(crossDomainAdmin) nonReentrant {
_setEnableRoute(originToken, destinationChainId, enable);
}

function setDepositQuoteTimeBuffer(uint64 buffer) public override onlyFromCrossDomainAccount(crossDomainAdmin) {
function setDepositQuoteTimeBuffer(uint64 buffer)
public
override
onlyFromCrossDomainAccount(crossDomainAdmin)
nonReentrant
{
_setDepositQuoteTimeBuffer(buffer);
}

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof)
public
override
onlyFromCrossDomainAccount(crossDomainAdmin)
nonReentrant
{
_initializeRelayerRefund(relayerRepaymentDistributionProof);
}

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

function _setCrossDomainAdmin(address newCrossDomainAdmin) internal {
require(newCrossDomainAdmin != address(0), "Bad bridge router address");
crossDomainAdmin = newCrossDomainAdmin;
emit SetXDomainAdmin(crossDomainAdmin);
function _bridgeTokensToHubPool(MerkleLib.DestinationDistribution memory distributionLeaf) internal override {
Copy link
Member

Choose a reason for hiding this comment

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

this will need branching logic for WETH. can do it in a future PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

added a TODO

// TODO: Handle WETH token unwrapping
Copy link
Member

Choose a reason for hiding this comment

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

yup! this part you can copy some of the work done in the other contracts, fortunately.

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 pool contract.
Copy link
Member

Choose a reason for hiding this comment

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

this might need to change in the context of weth.

distributionLeaf.amountToReturn, // _amount. Send the full balance of the deposit box to bridge.
l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
"" // _data. We don't need to send any data for the bridging action.
);
emit OptimismTokensBridged(distributionLeaf.l2TokenAddress, hubPool, distributionLeaf.amountToReturn, l1Gas);
}
}
113 changes: 102 additions & 11 deletions contracts/SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "@uma/core/contracts/common/implementation/Testable.sol";
import "@uma/core/contracts/common/implementation/Lockable.sol";
import "@uma/core/contracts/common/implementation/MultiCaller.sol";
import "./MerkleLib.sol";
import "./SpokePoolInterface.sol";

interface WETH9Like {
function withdraw(uint256 wad) external;
Expand All @@ -26,10 +27,16 @@ interface WETH9Like {
* on the destination chain. Locked source chain tokens are later sent over the canonical token bridge to L1.
* @dev This contract is designed to be deployed to L2's, not mainnet.
*/
abstract contract SpokePool is Testable, Lockable, MultiCaller {
abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCaller {
using SafeERC20 for IERC20;
using Address for address;

// Address of the L1 contract that acts as the owner of this SpokePool.
address public crossDomainAdmin;

// Address of the L1 contract that will send tokens to and receive tokens from this contract.
address public hubPool;

// Timestamp when contract was constructed. Relays cannot have a quote time before this.
uint64 public deploymentTime;

Expand All @@ -52,7 +59,7 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
bytes32 distributionRoot;
// This is a 2D bitmap tracking which leafs in the relayer refund root have been claimed, with max size of
// 256x256 leaves per root.
mapping(uint256 => uint256) claimsBitmap;
mapping(uint256 => uint256) claimedBitmap;
}
RelayerRefund[] public relayerRefunds;

Expand All @@ -75,6 +82,8 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
/****************************************
* EVENTS *
****************************************/
event SetXDomainAdmin(address indexed newAdmin);
event SetHubPool(address indexed newHubPool);
event EnabledDepositRoute(address indexed originToken, uint256 indexed destinationChainId, bool enabled);
event SetDepositQuoteTimeBuffer(uint64 newBuffer);
event FundsDeposited(
Expand Down Expand Up @@ -103,12 +112,33 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
address recipient
);
event InitializedRelayerRefund(uint256 indexed relayerRefundId, bytes32 relayerRepaymentDistributionProof);
event DistributedRelayerRefund(
uint256 indexed relayerRefundId,
uint256 indexed leafId,
uint256 chainId,
uint256 amountToReturn,
uint256[] refundAmounts,
address l2TokenAddress,
address[] refundAddresses,
address indexed caller
);
event TokensBridged(
uint256 indexed leafId,
uint256 indexed chainId,
uint256 amountToReturn,
address indexed l2TokenAddress,
address caller
);

constructor(
address _crossDomainAdmin,
address _hubPool,
address _wethAddress,
uint64 _depositQuoteTimeBuffer,
address timerAddress
) Testable(timerAddress) {
_setCrossDomainAdmin(_crossDomainAdmin);
_setHubPool(_hubPool);
deploymentTime = uint64(getCurrentTime());
depositQuoteTimeBuffer = _depositQuoteTimeBuffer;
weth = WETH9Like(_wethAddress);
Expand All @@ -127,6 +157,18 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
* ADMIN FUNCTIONS *
**************************************/

function _setCrossDomainAdmin(address newCrossDomainAdmin) internal {
require(newCrossDomainAdmin != address(0), "Bad bridge router address");
crossDomainAdmin = newCrossDomainAdmin;
emit SetXDomainAdmin(crossDomainAdmin);
}

function _setHubPool(address newHubPool) internal {
require(newHubPool != address(0), "Bad hub pool address");
hubPool = newHubPool;
emit SetHubPool(hubPool);
}

function _setEnableRoute(
address originToken,
uint256 destinationChainId,
Expand Down Expand Up @@ -156,7 +198,7 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
address recipient,
uint64 relayerFeePct,
uint64 quoteTimestamp
) public payable onlyEnabledRoute(originToken, destinationChainId) {
) public payable onlyEnabledRoute(originToken, destinationChainId) nonReentrant {
// We limit the relay fees to prevent the user spending all their funds on fees.
require(relayerFeePct <= 0.5e18, "invalid relayer fee");
// Note We assume that L2 timing cannot be compared accurately and consistently to L1 timing. Therefore,
Expand Down Expand Up @@ -210,7 +252,7 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
uint256 totalRelayAmount,
uint256 maxTokensToSend,
uint256 repaymentChain
) public {
) public nonReentrant {
// Each relay attempt is mapped to the hash of data uniquely identifying it, which includes the deposit data
// such as the origin chain ID and the deposit ID, and the data in a relay attempt such as who the recipient
// is, which chain and currency the recipient wants to receive funds on, and the relay fees.
Expand Down Expand Up @@ -244,11 +286,7 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
uint256 maxTokensToSend,
uint256 repaymentChain,
bytes memory depositorSignature
)
public
// public methods but I couldn't figure out a way to pass this in without encounering a stack too deep error.
nonReentrant
{
) public nonReentrant {
// Grouping the signature validation logic into brackets to address stack too deep error.
{
// Depositor should have signed a hash of the relayer fee % to update to and information uniquely identifying
Expand Down Expand Up @@ -302,8 +340,59 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
function distributeRelayerRefund(
uint256 relayerRefundId,
MerkleLib.DestinationDistribution memory distributionLeaf,
bytes32[] memory inclusionProof
) public {}
bytes32[] memory proof
) public override nonReentrant {
// Check integrity of leaf structure:
require(distributionLeaf.chainId == chainId(), "Invalid chainId");
require(distributionLeaf.refundAddresses.length == distributionLeaf.refundAmounts.length, "invalid leaf");
Comment on lines +346 to +347
Copy link
Member

Choose a reason for hiding this comment

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

I dont think any of these checks are needed. If either of these fail then the proof will fail. it's somewhat redundant, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think its better to be safe in case the bots miss something, we won't get into weird state

Copy link
Member Author

Choose a reason for hiding this comment

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

merging this comment: #24 (comment)


// Grab distribution root stored at `relayerRefundId`.
RelayerRefund storage refund = relayerRefunds[relayerRefundId];

// Check that `inclusionProof` proves that `distributionLeaf` is contained within the distribution root.
// Note: This should revert if the `distributionRoot` is uninitialized.
require(MerkleLib.verifyRelayerDistribution(refund.distributionRoot, distributionLeaf, proof), "Bad Proof");

// Verify the leafId in the leaf has not yet been claimed.
require(!MerkleLib.isClaimed(refund.claimedBitmap, distributionLeaf.leafId), "Already claimed");

// Set leaf as claimed in bitmap.
MerkleLib.setClaimed(refund.claimedBitmap, distributionLeaf.leafId);

// For each relayerRefundAddress in relayerRefundAddresses, send the associated refundAmount for the L2 token address.
// Note: Even if the L2 token is not enabled on this spoke pool, we should still refund relayers.
for (uint32 i = 0; i < distributionLeaf.refundAmounts.length; i++) {
uint256 amount = distributionLeaf.refundAmounts[i];
if (amount > 0)
IERC20(distributionLeaf.l2TokenAddress).safeTransfer(distributionLeaf.refundAddresses[i], amount);
}

// If `distributionLeaf.amountToReturn` is positive, then send L2 --> L1 message to bridge tokens back via
// chain-specific bridging method.
if (distributionLeaf.amountToReturn > 0) {
// Do we need to perform any check about the last time that funds were bridged from L2 to L1?
_bridgeTokensToHubPool(distributionLeaf);

emit TokensBridged(
distributionLeaf.leafId,
distributionLeaf.chainId,
distributionLeaf.amountToReturn,
distributionLeaf.l2TokenAddress,
msg.sender
);
}

emit DistributedRelayerRefund(
relayerRefundId,
distributionLeaf.leafId,
distributionLeaf.chainId,
distributionLeaf.amountToReturn,
distributionLeaf.refundAmounts,
distributionLeaf.l2TokenAddress,
distributionLeaf.refundAddresses,
msg.sender
);
}

/**************************************
* VIEW FUNCTIONS *
Expand All @@ -317,6 +406,8 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
* INTERNAL FUNCTIONS *
**************************************/

function _bridgeTokensToHubPool(MerkleLib.DestinationDistribution memory distributionLeaf) internal virtual;

function _computeAmountPreFees(uint256 amount, uint256 feesPct) private pure returns (uint256) {
return (1e18 * amount) / (1e18 - feesPct);
}
Expand Down
12 changes: 10 additions & 2 deletions contracts/SpokePoolInterface.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface SpokePoolInterface {
function crossDomainAdmin() external returns (address);
import "./MerkleLib.sol";

interface SpokePoolInterface {
function setCrossDomainAdmin(address newCrossDomainAdmin) external;

function setHubPool(address newHubPool) external;

function setEnableRoute(
address originToken,
uint256 destinationChainId,
Expand All @@ -15,4 +17,10 @@ interface SpokePoolInterface {
function setDepositQuoteTimeBuffer(uint64 buffer) external;

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) external;

function distributeRelayerRefund(
uint256 relayerRefundId,
MerkleLib.DestinationDistribution memory distributionLeaf,
bytes32[] memory inclusionProof
) external;
}
18 changes: 12 additions & 6 deletions contracts/test/MockSpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ import "../SpokePoolInterface.sol";
* @notice Implements admin internal methods to test internal logic.
*/
contract MockSpokePool is SpokePoolInterface, SpokePool {
address public override crossDomainAdmin;

constructor(
address _crossDomainAdmin,
address _hubPool,
address _wethAddress,
uint64 _depositQuoteTimeBuffer,
address timerAddress
) SpokePool(_wethAddress, _depositQuoteTimeBuffer, timerAddress) {}
) SpokePool(_crossDomainAdmin, _hubPool, _wethAddress, _depositQuoteTimeBuffer, timerAddress) {}

function setCrossDomainAdmin(address newCrossDomainAdmin) public override {
_setCrossDomainAdmin(newCrossDomainAdmin);
}

function setHubPool(address newHubPool) public override {
_setHubPool(newHubPool);
}

function setEnableRoute(
address originToken,
Expand All @@ -33,7 +41,5 @@ contract MockSpokePool is SpokePoolInterface, SpokePool {
_initializeRelayerRefund(relayerRepaymentDistributionProof);
}

function setCrossDomainAdmin(address newCrossDomainAdmin) public override {
crossDomainAdmin = newCrossDomainAdmin;
}
function _bridgeTokensToHubPool(MerkleLib.DestinationDistribution memory distributionLeaf) internal override {}
}
8 changes: 4 additions & 4 deletions test/HubPool.Fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TokenRolesEnum, interfaceName } from "@uma/common";
import { TokenRolesEnum } from "@uma/common";
import { getContractFactory, randomAddress, toBN, fromWei } from "./utils";

import { bondAmount, refundProposalLiveness, finalFee, identifier, repaymentChainId } from "./constants";
Expand All @@ -8,7 +8,7 @@ import hre from "hardhat";
import { umaEcosystemFixture } from "./UmaEcosystem.Fixture";

export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) => {
const [signer] = await ethers.getSigners();
const [signer, crossChainAdmin] = await ethers.getSigners();

// This fixture is dependent on the UMA ecosystem fixture. Run it first and grab the output. This is used in the
// deployments that follows. The output is spread when returning contract instances from this fixture.
Expand Down Expand Up @@ -44,8 +44,8 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) =
const mockAdapter = await (await getContractFactory("Mock_Adapter", signer)).deploy();
await mockAdapter.transferOwnership(hubPool.address);
const mockSpoke = await (
await getContractFactory("MockSpokePool", signer)
).deploy(weth.address, 0, parentFixtureOutput.timer.address);
await getContractFactory("MockSpokePool", { signer: signer, libraries: { MerkleLib: merkleLib.address } })
).deploy(crossChainAdmin.address, hubPool.address, weth.address, 0, parentFixtureOutput.timer.address);
await hubPool.setCrossChainContracts(repaymentChainId, mockAdapter.address, mockSpoke.address);

// Deploy mock l2 tokens for each token created before and whitelist the routes.
Expand Down
Loading