Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
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
131 changes: 99 additions & 32 deletions contracts/HubPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.0;

import "./MerkleLib.sol";
import "./chain-adapters/AdapterInterface.sol";

import "@uma/core/contracts/common/implementation/Testable.sol";
import "@uma/core/contracts/common/implementation/Lockable.sol";
Expand Down Expand Up @@ -51,6 +52,13 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {

mapping(address => LPToken) public lpTokens; // Mapping of L1TokenAddress to the associated LPToken.

struct CrossChainContract {
AdapterInterface adapter;
address spokePool;
}

mapping(uint256 => CrossChainContract) public crossChainContracts; // Mapping of chainId to the associated adapter and spokePool contracts.

FinderInterface public finder;

bytes32 public identifier;
Expand Down Expand Up @@ -88,19 +96,23 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
uint64 requestExpirationTimestamp,
uint64 unclaimedPoolRebalanceLeafCount,
uint256[] bundleEvaluationBlockNumbers,
bytes32 poolRebalanceRoot,
bytes32 destinationDistributionRoot,
bytes32 indexed poolRebalanceRoot,
bytes32 indexed destinationDistributionRoot,
address indexed proposer
);
event RelayerRefundExecuted(uint256 relayerRefundId, MerkleLib.PoolRebalance poolRebalance, address caller);

event RelayerRefundDisputed(
address indexed disputer,
SkinnyOptimisticOracleInterface.Request ooPriceRequest,
RefundRequest refundRequest
event RelayerRefundExecuted(
Copy link
Member

Choose a reason for hiding this comment

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

are any of these events indexable?

Copy link
Member Author

@chrismaree chrismaree Feb 4, 2022

Choose a reason for hiding this comment

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

yup. will index appropriate ones.

uint256 indexed leafId,
uint256 indexed chainId,
address[] l1Token,
uint256[] bundleLpFees,
int256[] netSendAmount,
int256[] runningBalance,
address indexed caller
);

modifier onlyIfNoActiveRequest() {
event RelayerRefundDisputed(address indexed disputer, uint256 requestTime, bytes disputedAncillaryData);

modifier noActiveRequests() {
require(refundRequest.unclaimedPoolRebalanceLeafCount == 0, "Active request has unclaimed leafs");
_;
}
Expand All @@ -126,12 +138,21 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
* ADMIN FUNCTIONS *
*************************************************/

function setBond(IERC20 newBondToken, uint256 newBondAmount) public onlyOwner onlyIfNoActiveRequest {
function setBond(IERC20 newBondToken, uint256 newBondAmount) public onlyOwner noActiveRequests {
bondToken = newBondToken;
bondAmount = newBondAmount;
emit BondSet(address(newBondToken), newBondAmount);
}

function setCrossChainContracts(
uint256 chainId,
AdapterInterface adapter,
address spokePool
) public onlyOwner noActiveRequests {
require(address(crossChainContracts[chainId].adapter) == address(0), "Contract already set");
Copy link
Member

Choose a reason for hiding this comment

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

Should we not allow the owner to reset addresses for upgradeability?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

crossChainContracts[chainId] = CrossChainContract(adapter, spokePool);
}

/**
* @notice Whitelist an origin token <-> destination token route.
*/
Expand Down Expand Up @@ -220,12 +241,15 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
* DATA WORKER FUNCTIONS *
*************************************************/

// After initiateRelayerRefund is called, if the any props are wrong then this proposal can be challenged. Once the
// challenge period passes, then the roots are no longer disputable, and only executeRelayerRefund can be called and
// initiateRelayerRefund can't be called again until all leafs are executed.
function initiateRelayerRefund(
uint256[] memory bundleEvaluationBlockNumbers,
uint64 poolRebalanceLeafCount,
bytes32 poolRebalanceRoot,
bytes32 destinationDistributionRoot
) public onlyIfNoActiveRequest {
) public noActiveRequests {
Copy link
Member

Choose a reason for hiding this comment

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

As discussed IRL:

  • consider adding a comment that after initiateRelayerRefund is called, if the poolRebalanceRoot, destinationDistributionRoot, poolRebalanceLeafCount, and bundleEvaluationBlockNumbers set is disputable then this can be challenged. Once the challenge period passes, then the roots are no longer disputable, and only executeRelayerRefund can be called and initiateRelayerRefund can't be called again until all leafs are executed.
  • Examples of some characteristics of these roots that could be disputed: poolRebalanceRoot.netSendAmount != destinationDistributionRoot.amountToReturn for example

Copy link
Member

@nicholaspai nicholaspai Feb 4, 2022

Choose a reason for hiding this comment

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

Motivation is updated comments + interface for the spoke pool distributeRelayerRefund in my PR: 8fb750a

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 do!

require(poolRebalanceLeafCount > 0, "Bundle must have at least 1 leaf");

uint64 requestExpirationTimestamp = uint64(getCurrentTime() + refundProposalLiveness);
Expand All @@ -251,52 +275,56 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
);
}

function executeRelayerRefund(
uint256 relayerRefundRequestId,
MerkleLib.PoolRebalance memory poolRebalance,
bytes32[] memory proof
) public {
function executeRelayerRefund(MerkleLib.PoolRebalance memory poolRebalanceLeaf, bytes32[] memory proof) public {
require(getCurrentTime() >= refundRequest.requestExpirationTimestamp, "Not passed liveness");

// Verify the leafId in the poolRebalance has not yet been claimed.
require(!MerkleLib.isClaimed1D(refundRequest.claimedBitMap, poolRebalance.leafId), "Already claimed");
// Verify the leafId in the poolRebalanceLeaf has not yet been claimed.
require(!MerkleLib.isClaimed1D(refundRequest.claimedBitMap, poolRebalanceLeaf.leafId), "Already claimed");

// Verify the props provided generate a leaf that, along with the proof, are included in the merkle root.
require(MerkleLib.verifyPoolRebalance(refundRequest.poolRebalanceRoot, poolRebalance, proof), "Bad Proof");
require(MerkleLib.verifyPoolRebalance(refundRequest.poolRebalanceRoot, poolRebalanceLeaf, proof), "Bad Proof");

// Set the leafId in the claimed bitmap.
refundRequest.claimedBitMap = MerkleLib.setClaimed1D(refundRequest.claimedBitMap, poolRebalance.leafId);
refundRequest.claimedBitMap = MerkleLib.setClaimed1D(refundRequest.claimedBitMap, poolRebalanceLeaf.leafId);

// Decrement the unclaimedPoolRebalanceLeafCount.
refundRequest.unclaimedPoolRebalanceLeafCount--;

// Transfer the bondAmount to back to the proposer, if this was not done before for this refund bundle.
if (!refundRequest.proposerBondRepaid) {
refundRequest.proposerBondRepaid = true;
// Transfer the bondAmount to back to the proposer, if this the last executed leaf. Only sending this once all
// leafs have been executed acts to force the data worker to execute all bundles or they wont receive their bond.
//TODO: consider if we want to reward the proposer. if so, this is where we should do it.
if (refundRequest.unclaimedPoolRebalanceLeafCount == 0)
bondToken.safeTransfer(refundRequest.proposer, bondAmount);
}
Comment on lines +296 to -277
Copy link
Member Author

Choose a reason for hiding this comment

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

it's better to only pay the relayer back their bond one all bundles are executed.


// TODO call into canonical bridge to send PoolRebalance.netSendAmount for the associated
// PoolRebalance.tokenAddresses, to the target PoolRebalance.chainId. this will likely happen within a
// x_Messenger contract for each chain. these messengers will be registered in a separate process that will follow
// in a later PR.
_sendTokensToChain(poolRebalanceLeaf.chainId, poolRebalanceLeaf.l1Tokens, poolRebalanceLeaf.netSendAmounts);
_executeRelayerRefundOnChain(poolRebalanceLeaf.chainId);

// TODO: modify the associated utilized and pending reserves for each token sent.

emit RelayerRefundExecuted(relayerRefundRequestId, poolRebalance, msg.sender);
emit RelayerRefundExecuted(
poolRebalanceLeaf.leafId,
poolRebalanceLeaf.chainId,
poolRebalanceLeaf.l1Tokens,
poolRebalanceLeaf.bundleLpFees,
poolRebalanceLeaf.netSendAmounts,
poolRebalanceLeaf.runningBalances,
msg.sender
);
}

function disputeRelayerRefund() public {
require(getCurrentTime() <= refundRequest.requestExpirationTimestamp, "Request passed liveness");

// Request price from OO and dispute it.
uint256 totalBond = _getBondTokenFinalFee() + bondAmount;
bytes memory requestAncillaryData = _getRefundProposalAncillaryData();
bondToken.safeTransferFrom(msg.sender, address(this), totalBond);
// This contract needs to approve totalBond*2 against the OO contract. (for the price request and dispute).
bondToken.safeApprove(address(_getOptimisticOracle()), totalBond * 2);
_getOptimisticOracle().requestAndProposePriceFor(
identifier,
uint32(getCurrentTime()),
_getRefundProposalAncillaryData(),
requestAncillaryData,
bondToken,
// Set reward to 0, since we'll settle proposer reward payouts directly from this contract after a relay
// proposal has passed the challenge period.
Expand Down Expand Up @@ -328,13 +356,13 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
_getOptimisticOracle().disputePriceFor(
identifier,
uint32(getCurrentTime()),
_getRefundProposalAncillaryData(),
requestAncillaryData,
ooPriceRequest,
msg.sender,
address(this)
);

emit RelayerRefundDisputed(msg.sender, ooPriceRequest, refundRequest);
emit RelayerRefundDisputed(msg.sender, getCurrentTime(), requestAncillaryData);

// Finally, delete the state pertaining to the active refundRequest.
delete refundRequest;
Expand Down Expand Up @@ -405,6 +433,45 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
.rawValue;
}

function _sendTokensToChain(
uint256 chainId,
address[] memory l1Tokens,
int256[] memory netSendAmounts
) internal {
AdapterInterface adapter = crossChainContracts[chainId].adapter;
require(address(adapter) != address(0), "Adapter not set for target chain");

for (uint32 i = 0; i < l1Tokens.length; i++) {
// Validate the output L2 token is correctly whitelisted.
address l2Token = whitelistedRoutes[l1Tokens[i]][chainId];
require(l2Token != address(0), "Route not whitelisted");

int256 amount = netSendAmounts[i];

// TODO: Checking the amount is greater than 0 is not sufficient. we need to build an external library that
// makes the decision on if there should be an L1->L2 token transfer. this should come in a later PR.
if (amount > 0) {
// Send the adapter all the tokens it needs to bridge. This should be refined later to remove the extra
// token transfer through the use of delegate call.
IERC20(l1Tokens[i]).safeApprove(address(adapter), uint256(amount));
adapter.relayTokens(
Copy link
Member

Choose a reason for hiding this comment

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

is there any way, or even an advantage to, potentially batching these calls?

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 dont think we can batch these easily as this is the actual action that sends tokens from L1->L2 so we'd need a way to batch these across the canonical bridge.

l1Tokens[i], // l1Token
l2Token, // l2Token
uint256(amount), // amount
crossChainContracts[chainId].spokePool // to. This should be the spokePool.
);
}
}
}

function _executeRelayerRefundOnChain(uint256 chainId) internal {
AdapterInterface adapter = crossChainContracts[chainId].adapter;
adapter.relayMessage(
crossChainContracts[chainId].spokePool, // target. This should be the spokePool on the L2.
abi.encodeWithSignature("initializeRelayerRefund(bytes32)", refundRequest.destinationDistributionRoot) // message
);
}

// Added to enable the BridgePool to receive ETH. used when unwrapping Weth.
receive() external payable {}
}
16 changes: 8 additions & 8 deletions contracts/MerkleLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ library MerkleLib {
uint256 leafId;
// This is used to know which chain to send cross-chain transactions to (and which SpokePool to sent to).
uint256 chainId;
// The following arrays are required to be the same length. They are parallel arrays for the given chainId and should be ordered by the `tokenAddresses` field.
// The following arrays are required to be the same length. They are parallel arrays for the given chainId and should be ordered by the `l1Tokens` field.
// All whitelisted tokens with nonzero relays on this chain in this bundle in the order of whitelisting.
address[] tokenAddresses;
address[] l1Tokens;
uint256[] bundleLpFees; // Total LP fee amount per token in this bundle, encompassing all associated bundled relays.
// This array is grouped with the two above, and it represents the amount to send or request back from the
// SpokePool. If positive, the pool will pay the SpokePool. If negative the SpokePool will pay the HubPool.
// There can be arbitrarily complex rebalancing rules defined offchain. This number is only nonzero
// when the rules indicate that a rebalancing action should occur. When a rebalance does not occur,
// runningBalance for this token should change by the total relays - deposits in this bundle. When a rebalance
// does occur, runningBalance should be set to zero for this token and the netSendAmount should be set to the
// previous runningBalance + relays - deposits in this bundle.
int256[] netSendAmount;
// runningBalances for this token should change by the total relays - deposits in this bundle. When a rebalance
// does occur, runningBalances should be set to zero for this token and the netSendAmounts should be set to the
// previous runningBalances + relays - deposits in this bundle.
int256[] netSendAmounts;
// This is only here to be emitted in an event to track a running unpaid balance between the L2 pool and the L1 pool.
// A positive number indicates that the HubPool owes the SpokePool funds. A negative number indicates that the
// SpokePool owes the HubPool funds. See the comment above for the dynamics of this and netSendAmount
int256[] runningBalance;
// SpokePool owes the HubPool funds. See the comment above for the dynamics of this and netSendAmounts
int256[] runningBalances;
}

// This leaf is meant to be decoded in the SpokePool in order to pay out individual relayers for this bundle.
Expand Down
4 changes: 3 additions & 1 deletion contracts/SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ abstract contract SpokePool is Testable, Lockable, MultiCaller {
emit FilledRelay(relayHash, relayFills[relayHash], repaymentChain, amountToSend, msg.sender, relayData);
}

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public {}
Copy link
Member Author

Choose a reason for hiding this comment

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

nit to give me the required interface. @nicholaspai 's PR implements this actual logic.

function initializeRelayerRefund(bytes32 relayerRepaymentDistributionProof) public virtual {
return;
}

function distributeRelayerRefund(
uint256 relayerRefundId,
Expand Down
17 changes: 17 additions & 0 deletions contracts/chain-adapters/AdapterInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

/**
* @notice Sends cross chain messages and tokens to contracts on a specific L2 network.
*/

interface AdapterInterface {
function relayMessage(address target, bytes memory message) external payable;

function relayTokens(
address l1Token,
address l2Token,
uint256 amount,
address to
) external payable;
}
32 changes: 32 additions & 0 deletions contracts/chain-adapters/Mock_Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;

import "./AdapterInterface.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
* @notice Sends cross chain messages Optimism L2 network.
* @dev This contract's owner should be set to the BridgeAdmin deployed on the same L1 network so that only the
* BridgeAdmin can call cross-chain administrative functions on the L2 SpokePool via this messenger.
*/
contract Mock_Adapter is Ownable, AdapterInterface {
event RelayMessageCalled(address target, bytes message, address caller);

event RelayTokensCalled(address l1Token, address l2Token, uint256 amount, address to, address caller);

function relayMessage(address target, bytes memory message) external payable override onlyOwner {
emit RelayMessageCalled(target, message, msg.sender);
}

function relayTokens(
address l1Token,
address l2Token,
uint256 amount,
address to
) external payable override onlyOwner {
emit RelayTokensCalled(l1Token, l2Token, amount, to, msg.sender);
// Pull the tokens from the caller to mock the actions of an L1 bridge pulling tokens.
IERC20(l1Token).transferFrom(msg.sender, address(this), amount);
}
}
45 changes: 45 additions & 0 deletions contracts/chain-adapters/Optimism_Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: AGPL-3.0-only
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 tried to make this as simple as posible. I think it's pretty clean

pragma solidity ^0.8.0;

import "@eth-optimism/contracts/libraries/bridge/CrossDomainEnabled.sol";
import "@eth-optimism/contracts/L1/messaging/IL1ERC20Bridge.sol";
import "./AdapterInterface.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @notice Sends cross chain messages Optimism L2 network.
* @dev This contract's owner should be set to the BridgeAdmin deployed on the same L1 network so that only the
* BridgeAdmin can call cross-chain administrative functions on the L2 SpokePool via this messenger.
*/
contract Optimism_Messenger is Ownable, CrossDomainEnabled, AdapterInterface {
uint32 public gasLimit;

address l1Weth;

IL1ERC20Bridge l1ERC20Bridge;

constructor(
uint32 _gasLimit,
address _crossDomainMessenger,
address _IL1ERC20Bridge
) CrossDomainEnabled(_crossDomainMessenger) {
gasLimit = _gasLimit;
l1ERC20Bridge = IL1ERC20Bridge(_IL1ERC20Bridge);
}

function relayMessage(address target, bytes memory message) external payable override onlyOwner {
sendCrossDomainMessage(target, uint32(gasLimit), message);
}

// TODO: we should look into using delegate call as this current implementation assumes the caller
// transfers the tokens first to this contract.
function relayTokens(
address l1Token,
address l2Token,
uint256 amount,
address to
) external payable override onlyOwner {
//TODO: add weth support.
l1ERC20Bridge.depositERC20To(l1Token, l2Token, to, amount, gasLimit, "0x");
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 ETH/WETH weirdness sending tokens to optimism? Do they take WETH?

Copy link
Member Author

Choose a reason for hiding this comment

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

yup! this implementation right now will only work with ERC20s. I've added a todo to add extra WETH related logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense!

}
}
4 changes: 4 additions & 0 deletions contracts/test/MockSpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ contract MockSpokePool is SpokePool {
function setDepositQuoteTimeBuffer(uint64 buffer) public {
_setDepositQuoteTimeBuffer(buffer);
}

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