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
2 changes: 2 additions & 0 deletions contracts/HubPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
) public onlyOwner {
whitelistedRoutes[originToken][destinationChainId] = destinationToken;

// TODO: Should relay message to L2 for destinationChainId and call setEnableRoute(originToken, destinationChainId, true)

emit WhitelistRoute(originToken, destinationChainId, destinationToken);
}

Expand Down
80 changes: 80 additions & 0 deletions contracts/Optimism_SpokePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@eth-optimism/contracts/libraries/bridge/CrossDomainEnabled.sol";
import "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol";
import "./SpokePool.sol";
import "./SpokePoolInterface.sol";

/**
* @notice OVM specific SpokePool.
* @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;
Copy link
Member Author

Choose a reason for hiding this comment

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

@chrismaree @mrice32 should crossDomainAdmin be in the SpokePool or do you think this notion of a cross domain admin will be different for each. L2?

Copy link
Contributor

Choose a reason for hiding this comment

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

What is the cross-domain admin? The optimism bridge address on L2? Or is it the BridgeAdmin address on L1?

Copy link
Member Author

Choose a reason for hiding this comment

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

Its the contract that calls relayMessage on the canonical bridge, so its either the BridgeAdmin or the HubPool

Copy link
Member

Choose a reason for hiding this comment

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

for now HubPool me thinks. we might want to change this later but I think lets leave it there optimistically at the moment.


event SetXDomainAdmin(address indexed newAdmin);

constructor(
address _crossDomainAdmin,
address _wethAddress,
uint64 _depositQuoteTimeBuffer,
address timerAddress
)
CrossDomainEnabled(Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER)
SpokePool(_wethAddress, _depositQuoteTimeBuffer, timerAddress)
{
_setCrossDomainAdmin(_crossDomainAdmin);
}

/**************************************
* 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 setCrossDomainAdmin(address newCrossDomainAdmin)
public
override
onlyFromCrossDomainAccount(crossDomainAdmin)
{
_setCrossDomainAdmin(newCrossDomainAdmin);
}

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

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

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

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

function _setCrossDomainAdmin(address newCrossDomainAdmin) internal {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any way to call this function after construction? Is this something we'd like to be able to change? (genuine question, not suggesting a change)

Copy link
Member

Choose a reason for hiding this comment

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

there is a setCrossDomainAdmin that can call this after construction enabling us to change it. I think we'd want to change it in the event something broke within the L1<->L2 logic. one problem, though, that I can see with this pattern is it assumes that the cross domain owner implements sufficient methods to call setCrossDomainAdmin over the bridge. this should be doable as this is going to be sitting within the "Adapter" logic that I've created for the L1->L2 calls. We can create a base contract (CrossDomainAdminBase) or something equivalent that these adapters will inherit from. can think more on this in the next few PRs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah i added in this function without telling you. We should allow this to be changed from L1

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm a little confused. Are there two functions that allow you to setCrossDomainAdmin? If so, can we consolidate them into a single function?

Copy link
Member Author

Choose a reason for hiding this comment

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

No there's only one, implemented in this contract. setCrossDomainAdmin is included in the SpokePoolInterface

require(newCrossDomainAdmin != address(0), "Bad bridge router address");
crossDomainAdmin = newCrossDomainAdmin;
emit SetXDomainAdmin(crossDomainAdmin);
}
}
31 changes: 24 additions & 7 deletions contracts/SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/utils/Address.sol";
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";

interface WETH9Like {
function withdraw(uint256 wad) external;
Expand Down Expand Up @@ -43,6 +44,15 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
// Origin token to destination token routings can be turned on or off.
mapping(address => mapping(uint256 => bool)) public enabledDepositRoutes;

struct RelayerRefund {
// Merkle root of relayer refunds.
bytes32 distributionRoot;
Copy link
Contributor

Choose a reason for hiding this comment

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

One interesting tradeoff about doing claims via merkle proofs one-by-one is that this will result in increased calldata costs on L2 over passing in all the leaves and storing (or maybe even executing) in a single call. This is because you will have to submit each leaf individually with another bytes32 element for each layer in the tree. This means that your overall calldata might be significantly larger than just passing in the leaves alone. I'm not sure how significant this would end up being, but I wonder how hard it would be for us to take in all the leaves and generate the proofs internally on-chain to show that they roll up into the root.

Not suggesting a change, just wanted to mention this.

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 so you think this could work by the HubPool relaying all leafs to the SpokePool here and storing them on-chain, as opposed to just relaying the root. @chrismaree wdyt?

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 @mrice32 is proposing the HubPool passing the leafs; it cant do that in a efficient way. I think @mrice32 is proposing a technique where you can execute multiple leafs at once in a compressed structure and prove they are within the provided root. @mrice32 what you describe would fit into the execution stage of the process, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

How do you imagine we improve on just making the contract multicall and assuming distributeRelayerRefund is called multiple times in one batched txn?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wanna be super clear that I don't think we should do this now, and I think before we add this complexity, we should try to better understand the amount of calldata/gas savings we'd see from something like this with different tree sizes.

Agree @chrismaree. I'm not suggesting the HubPool do anything different since any added data anywhere on mainnet is going to be expensive.

If you did multicall, you would effectively be passing:

[{distributionStruct1, proofArray1}, {distributionStruct2, proofArray2}...]

What I'm suggesting is an additional method that allows you to pass all leaves this way:

[{distriubtionStruct1}, {distributionStruct2}, ...]

If we get the logic right in the contract, the leaf data could be used to generate the proofs so you wouldn't need to pass them in, thereby reducing calldata.

// 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;
}
RelayerRefund[] public relayerRefunds;

struct RelayData {
address depositor;
address recipient;
Expand Down Expand Up @@ -89,6 +99,7 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
address depositor,
address recipient
);
event InitializedRelayerRefund(uint256 indexed relayerRefundId, bytes32 relayerRepaymentDistributionProof);

constructor(
address _wethAddress,
Expand Down Expand Up @@ -257,17 +268,23 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
);
}

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public virtual {
return;
// This internal method should be called by an external "initializeRelayerRefund" function that validates the
// cross domain sender is the HubPool. This validation step differs for each L2, which is why the implementation
// specifics are left to the implementor of this abstract contract.
// Once this method is executed and a distribution root is stored in this contract, then `distributeRelayerRefund`
// can be called to execute each leaf in the root.
function _initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) internal {
uint256 relayerRefundId = relayerRefunds.length;
relayerRefunds.push().distributionRoot = relayerRepaymentDistributionProof;
emit InitializedRelayerRefund(relayerRefundId, relayerRepaymentDistributionProof);
}

// Call this method to execute a leaf within the `distributionRoot` stored on this contract. Caller must include a
// valid `inclusionProof` to verify that the leaf is contained within the root. The `relayerRefundId` is the index
// of the specific distribution root containing the passed in leaf.
function distributeRelayerRefund(
uint256 relayerRefundId,
uint256 leafId,
address l2TokenAddress,
uint256 netSendAmount,
address[] memory relayerRefundAddresses,
uint256[] memory relayerRefundAmounts,
MerkleLib.DestinationDistribution memory distributionLeaf,
bytes32[] memory inclusionProof
) public {}

Expand Down
18 changes: 18 additions & 0 deletions contracts/SpokePoolInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface SpokePoolInterface {
function crossDomainAdmin() external returns (address);

function setCrossDomainAdmin(address newCrossDomainAdmin) external;

function setEnableRoute(
address originToken,
uint256 destinationChainId,
bool enable
) external;

function setDepositQuoteTimeBuffer(uint64 buffer) external;

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) external;
}
15 changes: 11 additions & 4 deletions contracts/test/MockSpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
pragma solidity ^0.8.0;

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

/**
* @title MockSpokePool
* @notice Implements admin internal methods to test internal logic.
*/
contract MockSpokePool is SpokePool {
contract MockSpokePool is SpokePoolInterface, SpokePool {
address public override crossDomainAdmin;

constructor(
address _wethAddress,
uint64 _depositQuoteTimeBuffer,
Expand All @@ -18,15 +21,19 @@ contract MockSpokePool is SpokePool {
address originToken,
uint256 destinationChainId,
bool enable
) public {
) public override {
_setEnableRoute(originToken, destinationChainId, enable);
}

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

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public override {
return;
_initializeRelayerRefund(relayerRepaymentDistributionProof);
}

function setCrossDomainAdmin(address newCrossDomainAdmin) public override {
crossDomainAdmin = newCrossDomainAdmin;
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test": "yarn hardhat test"
},
"dependencies": {
"@eth-optimism/contracts": "^0.5.11",
"@openzeppelin/contracts": "^4.4.2",
"@uma/common": "^2.17.0",
"@uma/contracts-node": "^0.2.0",
Expand Down
1 change: 1 addition & 0 deletions test/Optimism_SpokePool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TODO: Test OVM specific functionality and implementation of SpokePool internal methods.
22 changes: 22 additions & 0 deletions test/SpokePool.RefundInitialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect } from "chai";
import { Contract } from "ethers";
import { ethers } from "hardhat";
import { SignerWithAddress } from "./utils";
import { spokePoolFixture } from "./SpokePool.Fixture";
import { mockDestinationDistributionRoot } from "./constants";

let spokePool: Contract;
let caller: SignerWithAddress;

describe("SpokePool Initialize Relayer Refund Logic", async function () {
beforeEach(async function () {
[caller] = await ethers.getSigners();
({ spokePool } = await spokePoolFixture());
});
it("Initializing root stores root and emits event", async function () {
await expect(spokePool.connect(caller).initializeRelayerRefund(mockDestinationDistributionRoot))
.to.emit(spokePool, "InitializedRelayerRefund")
.withArgs(0, mockDestinationDistributionRoot);
expect(await spokePool.relayerRefunds(0)).to.equal(mockDestinationDistributionRoot);
});
});
Loading