|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | +pragma solidity ^0.8.0; |
| 3 | + |
| 4 | +import "./SpokePool.sol"; |
| 5 | +import "./external/interfaces/IPolygonZkEVMBridge.sol"; |
| 6 | + |
| 7 | +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| 9 | + |
| 10 | +/** |
| 11 | + * @notice Define interface for PolygonZkEVM Bridge message receiver |
| 12 | + * See https://github.com/0xPolygonHermez/zkevm-contracts/blob/53e95f3a236d8bea87c27cb8714a5d21496a3b20/contracts/interfaces/IBridgeMessageReceiver.sol |
| 13 | + */ |
| 14 | +interface IBridgeMessageReceiver { |
| 15 | + /** |
| 16 | + * @notice This will be called by the Polygon zkEVM Bridge on L2 to relay a message sent from the HubPool. |
| 17 | + * @param originAddress Address of the original message sender on L1. |
| 18 | + * @param originNetwork Polygon zkEVM's internal network id of source chain. |
| 19 | + * @param data Data to be received and executed on this contract. |
| 20 | + */ |
| 21 | + function onMessageReceived( |
| 22 | + address originAddress, |
| 23 | + uint32 originNetwork, |
| 24 | + bytes memory data |
| 25 | + ) external payable; |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * @notice Polygon zkEVM Spoke pool. |
| 30 | + */ |
| 31 | +contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver { |
| 32 | + using SafeERC20 for IERC20; |
| 33 | + |
| 34 | + // Address of Polygon zkEVM's Canonical Bridge on L2. |
| 35 | + IPolygonZkEVMBridge public l2PolygonZkEVMBridge; |
| 36 | + |
| 37 | + // Polygon zkEVM's internal network id for L1. |
| 38 | + uint32 public constant l1NetworkId = 0; |
| 39 | + |
| 40 | + // Warning: this variable should _never_ be touched outside of this contract. It is intentionally set to be |
| 41 | + // private. Leaving it set to true can permanently disable admin calls. |
| 42 | + bool private adminCallValidated; |
| 43 | + |
| 44 | + /************************************** |
| 45 | + * ERRORS * |
| 46 | + **************************************/ |
| 47 | + error AdminCallValidatedAlreadySet(); |
| 48 | + error CallerNotBridge(); |
| 49 | + error OriginSenderNotCrossDomain(); |
| 50 | + error SourceChainNotHubChain(); |
| 51 | + error AdminCallNotValidated(); |
| 52 | + |
| 53 | + /************************************** |
| 54 | + * EVENTS * |
| 55 | + **************************************/ |
| 56 | + event SetPolygonZkEVMBridge(address indexed newPolygonZkEVMBridge, address indexed oldPolygonZkEVMBridge); |
| 57 | + event PolygonZkEVMTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged); |
| 58 | + event ReceivedMessageFromL1(address indexed caller, address indexed originAddress); |
| 59 | + |
| 60 | + // Note: validating calls this way ensures that strange calls coming from the onMessageReceived won't be |
| 61 | + // misinterpreted. Put differently, just checking that originAddress == crossDomainAdmint is not sufficient. |
| 62 | + // All calls that have admin privileges must be fired from within the onMessageReceived method that's gone |
| 63 | + // through validation where the sender is checked and the sender from the other chain is also validated. |
| 64 | + // This modifier sets the adminCallValidated variable so this condition can be checked in _requireAdminSender(). |
| 65 | + modifier validateInternalCalls() { |
| 66 | + // Make sure adminCallValidated is set to True only once at beginning of onMessageReceived, which prevents |
| 67 | + // onMessageReceived from being re-entered. |
| 68 | + if (adminCallValidated) { |
| 69 | + revert AdminCallValidatedAlreadySet(); |
| 70 | + } |
| 71 | + |
| 72 | + // This sets a variable indicating that we're now inside a validated call. |
| 73 | + // Note: this is used by other methods to ensure that this call has been validated by this method and is not |
| 74 | + // spoofed. |
| 75 | + adminCallValidated = true; |
| 76 | + |
| 77 | + _; |
| 78 | + |
| 79 | + // Reset adminCallValidated to false to disallow admin calls after this method exits. |
| 80 | + adminCallValidated = false; |
| 81 | + } |
| 82 | + |
| 83 | + /** |
| 84 | + * @notice Construct Polygon zkEVM specific SpokePool. |
| 85 | + * @param _wrappedNativeTokenAddress Address of WETH on Polygon zkEVM. |
| 86 | + * @param _depositQuoteTimeBuffer Quote timestamps can't be set more than this amount |
| 87 | + * into the past from the block time of the deposit. |
| 88 | + * @param _fillDeadlineBuffer Fill deadlines can't be set more than this amount |
| 89 | + * into the future from the block time of the deposit. |
| 90 | + */ |
| 91 | + /// @custom:oz-upgrades-unsafe-allow constructor |
| 92 | + constructor( |
| 93 | + address _wrappedNativeTokenAddress, |
| 94 | + uint32 _depositQuoteTimeBuffer, |
| 95 | + uint32 _fillDeadlineBuffer |
| 96 | + ) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks |
| 97 | + |
| 98 | + /** |
| 99 | + * @notice Construct the Polygon zkEVM SpokePool. |
| 100 | + * @param _l2PolygonZkEVMBridge Address of Polygon zkEVM's canonical bridge contract on L2. |
| 101 | + * @param _initialDepositId Starting deposit ID. Set to 0 unless this is a re-deployment in order to mitigate |
| 102 | + * @param _crossDomainAdmin Cross domain admin to set. Can be changed by admin. |
| 103 | + * @param _hubPool Hub pool address to set. Can be changed by admin. |
| 104 | + */ |
| 105 | + function initialize( |
| 106 | + IPolygonZkEVMBridge _l2PolygonZkEVMBridge, |
| 107 | + uint32 _initialDepositId, |
| 108 | + address _crossDomainAdmin, |
| 109 | + address _hubPool |
| 110 | + ) public initializer { |
| 111 | + __SpokePool_init(_initialDepositId, _crossDomainAdmin, _hubPool); |
| 112 | + _setL2PolygonZkEVMBridge(_l2PolygonZkEVMBridge); |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * @notice Admin can reset the Polygon zkEVM bridge contract address. |
| 117 | + * @param _l2PolygonZkEVMBridge Address of the new canonical bridge. |
| 118 | + */ |
| 119 | + function setL2PolygonZkEVMBridge(IPolygonZkEVMBridge _l2PolygonZkEVMBridge) external onlyAdmin { |
| 120 | + _setL2PolygonZkEVMBridge(_l2PolygonZkEVMBridge); |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * @notice This will be called by the Polygon zkEVM Bridge on L2 to relay a message sent from the HubPool. |
| 125 | + * @param _originAddress Address of the original message sender on L1. |
| 126 | + * @param _originNetwork Polygon zkEVM's internal network id of source chain. |
| 127 | + * @param _data Data to be received and executed on this contract. |
| 128 | + */ |
| 129 | + function onMessageReceived( |
| 130 | + address _originAddress, |
| 131 | + uint32 _originNetwork, |
| 132 | + bytes memory _data |
| 133 | + ) external payable override validateInternalCalls { |
| 134 | + if (msg.sender != address(l2PolygonZkEVMBridge)) { |
| 135 | + revert CallerNotBridge(); |
| 136 | + } |
| 137 | + if (_originAddress != crossDomainAdmin) { |
| 138 | + revert OriginSenderNotCrossDomain(); |
| 139 | + } |
| 140 | + if (_originNetwork != l1NetworkId) { |
| 141 | + revert SourceChainNotHubChain(); |
| 142 | + } |
| 143 | + |
| 144 | + /// @custom:oz-upgrades-unsafe-allow delegatecall |
| 145 | + (bool success, ) = address(this).delegatecall(_data); |
| 146 | + require(success, "delegatecall failed"); |
| 147 | + |
| 148 | + emit ReceivedMessageFromL1(msg.sender, _originAddress); |
| 149 | + } |
| 150 | + |
| 151 | + /************************************** |
| 152 | + * INTERNAL FUNCTIONS * |
| 153 | + **************************************/ |
| 154 | + |
| 155 | + /** |
| 156 | + * @notice Wraps any ETH into WETH before executing base function. This is necessary because SpokePool receives |
| 157 | + * ETH over the canonical token bridge instead of WETH. |
| 158 | + */ |
| 159 | + function _preExecuteLeafHook(address l2TokenAddress) internal override { |
| 160 | + if (l2TokenAddress == address(wrappedNativeToken)) _depositEthToWeth(); |
| 161 | + } |
| 162 | + |
| 163 | + // Wrap any ETH owned by this contract so we can send expected L2 token to recipient. This is necessary because |
| 164 | + // this SpokePool will receive ETH from the canonical token bridge instead of WETH. This may not be neccessary |
| 165 | + // if ETH on Polygon zkEVM is treated as ETH and the fallback() function is triggered when this contract receives |
| 166 | + // ETH. We will have to test this but this function for now allows the contract to safely convert all of its |
| 167 | + // held ETH into WETH at the cost of higher gas costs. |
| 168 | + function _depositEthToWeth() internal { |
| 169 | + //slither-disable-next-line arbitrary-send-eth |
| 170 | + if (address(this).balance > 0) wrappedNativeToken.deposit{ value: address(this).balance }(); |
| 171 | + } |
| 172 | + |
| 173 | + function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override { |
| 174 | + // SpokePool is expected to receive ETH from the L1 HubPool, then we need to first unwrap it to ETH and then |
| 175 | + // send ETH directly via the native L2 bridge. |
| 176 | + if (l2TokenAddress == address(wrappedNativeToken)) { |
| 177 | + WETH9Interface(l2TokenAddress).withdraw(amountToReturn); // Unwrap into ETH. |
| 178 | + l2PolygonZkEVMBridge.bridgeAsset{ value: amountToReturn }( |
| 179 | + l1NetworkId, |
| 180 | + hubPool, |
| 181 | + amountToReturn, |
| 182 | + address(0), |
| 183 | + true, // Indicates if the new global exit root is updated or not, which is true for asset bridges |
| 184 | + "" |
| 185 | + ); |
| 186 | + } else { |
| 187 | + IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2PolygonZkEVMBridge), amountToReturn); |
| 188 | + l2PolygonZkEVMBridge.bridgeAsset( |
| 189 | + l1NetworkId, |
| 190 | + hubPool, |
| 191 | + amountToReturn, |
| 192 | + l2TokenAddress, |
| 193 | + true, // Indicates if the new global exit root is updated or not, which is true for asset bridges |
| 194 | + "" |
| 195 | + ); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + // Check that the onMessageReceived method has validated the method to ensure the sender is authenticated. |
| 200 | + function _requireAdminSender() internal view override { |
| 201 | + if (!adminCallValidated) { |
| 202 | + revert AdminCallNotValidated(); |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + function _setL2PolygonZkEVMBridge(IPolygonZkEVMBridge _newL2PolygonZkEVMBridge) internal { |
| 207 | + address oldL2PolygonZkEVMBridge = address(l2PolygonZkEVMBridge); |
| 208 | + l2PolygonZkEVMBridge = _newL2PolygonZkEVMBridge; |
| 209 | + emit SetPolygonZkEVMBridge(address(_newL2PolygonZkEVMBridge), oldL2PolygonZkEVMBridge); |
| 210 | + } |
| 211 | +} |
0 commit comments