-
Notifications
You must be signed in to change notification settings - Fork 75
feat(spoke-pool): Distribute relayer refund #24
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
536fb7c
6a331a1
1a442c4
833d320
660a0e4
5dfbbe9
e1c5e8c
4e8a74e
125e08e
d96a275
eb4ea53
bde66ba
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 |
|---|---|---|
|
|
@@ -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"; | ||
|
|
||
|
|
@@ -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. | ||
|
|
@@ -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 { | ||
| // TODO: Handle WETH token unwrapping | ||
|
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. 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. | ||
|
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. 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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( | ||
|
|
@@ -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); | ||
|
|
@@ -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, | ||
|
|
@@ -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, | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
|
@@ -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
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. I dont think any of these checks are needed. If either of these fail then the proof will fail. it's somewhat redundant, no?
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 think its better to be safe in case the bots miss something, we won't get into weird state
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. 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 * | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
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.
this will need branching logic for WETH. can do it in a future PR.
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.
added a TODO