-
Notifications
You must be signed in to change notification settings - Fork 75
feat(HubPool): Add initial L1->L2 call plumbing #19
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
ba54cb9
84aad83
e971ff7
7a24fba
2f99c84
dcb052a
487e7ea
39ea052
febc4a6
1ba6779
1886cfd
439138b
b7e3ae2
1f6fc17
f64bf85
8bdcec7
89e642a
d6bc9bd
8de1b8f
98b2b52
eefd0c0
5de87a2
9d75d36
aca98e0
6307c53
6d87b1d
b03f37e
f45389b
b33a77b
12d0811
b02f2a8
03c4d00
b6cca33
804bb41
ce3f29c
e9c0cb9
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 |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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; | ||
|
|
@@ -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( | ||
| 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"); | ||
| _; | ||
| } | ||
|
|
@@ -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"); | ||
|
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. Should we not allow the owner to reset addresses for upgradeability?
Contributor
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. +1 |
||
| crossChainContracts[chainId] = CrossChainContract(adapter, spokePool); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Whitelist an origin token <-> destination token route. | ||
| */ | ||
|
|
@@ -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 { | ||
|
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. As discussed IRL:
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. Motivation is updated comments + interface for the spoke pool
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 do! |
||
| require(poolRebalanceLeafCount > 0, "Bundle must have at least 1 leaf"); | ||
nicholaspai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| uint64 requestExpirationTimestamp = uint64(getCurrentTime() + refundProposalLiveness); | ||
|
|
@@ -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
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. 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. | ||
|
|
@@ -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; | ||
|
|
@@ -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( | ||
|
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. is there any way, or even an advantage to, potentially batching these calls?
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. 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 {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 {} | ||
|
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. 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, | ||
|
|
||
| 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; | ||
| } |
| 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
|
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. 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"); | ||
|
Contributor
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. Is there any ETH/WETH weirdness sending tokens to optimism? Do they take WETH?
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. yup! this implementation right now will only work with ERC20s. I've added a todo to add extra WETH related logic.
Contributor
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. Makes sense! |
||
| } | ||
| } | ||
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.
are any of these events indexable?
Uh oh!
There was an error while loading. Please reload this page.
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.
yup. will index appropriate ones.