|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | + |
| 3 | +pragma solidity ^0.8.0; |
| 4 | + |
| 5 | +import { WithdrawalHelperBase } from "./WithdrawalHelperBase.sol"; |
| 6 | +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 7 | +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| 8 | +import { WETH9Interface } from "../../external/interfaces/WETH9Interface.sol"; |
| 9 | +import { ITokenMessenger } from "../../external/interfaces/CCTPInterfaces.sol"; |
| 10 | +import { Lib_PredeployAddresses } from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol"; |
| 11 | +import { LibOptimismUpgradeable } from "@openzeppelin/contracts-upgradeable/crosschain/optimism/LibOptimismUpgradeable.sol"; |
| 12 | +import { IL2ERC20Bridge } from "../../Ovm_SpokePool.sol"; |
| 13 | + |
| 14 | +/** |
| 15 | + * @notice Minimal interface for the Ovm_SpokePool contract. This interface is called to pull state from the network's |
| 16 | + * spoke pool contract to be used by this withdrawal adapter. |
| 17 | + */ |
| 18 | +interface IOvm_SpokePool { |
| 19 | + // Returns the address of the token bridge for the input l2 token. |
| 20 | + function tokenBridges(address token) external view returns (address); |
| 21 | + |
| 22 | + // Returns the address of the l1 token set in the spoke pool for the input l2 token. |
| 23 | + function remoteL1Tokens(address token) external view returns (address); |
| 24 | + |
| 25 | + // Returns the address for the representation of ETH on the l2. |
| 26 | + function l2Eth() external view returns (address); |
| 27 | + |
| 28 | + // Returns the address of the wrapped native token for the L2. |
| 29 | + function wrappedNativeToken() external view returns (WETH9Interface); |
| 30 | + |
| 31 | + // Returns the amount of gas the contract allocates for a token withdrawal. |
| 32 | + function l1Gas() external view returns (uint32); |
| 33 | +} |
| 34 | + |
| 35 | +/** |
| 36 | + * @title Ovm_WithdrawalAdapter |
| 37 | + * @notice This contract interfaces with L2-L1 token bridges and withdraws tokens to a single address on L1. |
| 38 | + * @dev This contract should be deployed on OpStack L2s which both have a Ovm_SpokePool contract deployed to the L2 |
| 39 | + * network AND only use token bridges defined in the Ovm_SpokePool. A notable exception to this requirement is Optimism, |
| 40 | + * which has a special SNX bridge (and thus this adapter will NOT work for Optimism). |
| 41 | + * @custom:security-contact [email protected] |
| 42 | + */ |
| 43 | +contract Ovm_WithdrawalHelper is WithdrawalHelperBase { |
| 44 | + using SafeERC20 for IERC20; |
| 45 | + |
| 46 | + // Address for the wrapped native token on this chain. For Ovm standard bridges, we need to unwrap |
| 47 | + // this token before initiating the withdrawal. Normally, it is 0x42..006, but there are instances |
| 48 | + // where this address is different. |
| 49 | + WETH9Interface public immutable wrappedNativeToken; |
| 50 | + // Address of the corresponding spoke pool on L2. This is to piggyback off of the spoke pool's supported |
| 51 | + // token routes/defined token bridges. |
| 52 | + IOvm_SpokePool public immutable spokePool; |
| 53 | + // Address of native ETH on the l2. For OpStack chains, this address is used to indicate a native ETH withdrawal. |
| 54 | + // In general, this address is 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000. |
| 55 | + address public immutable l2Eth; |
| 56 | + // Address of the messenger contract on L2. This is by default defined in Lib_PredeployAddresses. |
| 57 | + address public constant MESSENGER = Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER; |
| 58 | + |
| 59 | + /* |
| 60 | + * @notice Constructs the Ovm_WithdrawalAdapter. |
| 61 | + * @param _l2Usdc Address of native USDC on the L2. |
| 62 | + * @param _cctpTokenMessenger Address of the CCTP token messenger contract on L2. |
| 63 | + * @param _destinationCircleDomainId Circle's assigned CCTP domain ID for the destination network. For Ethereum, this |
| 64 | + * is 0. |
| 65 | + * @param _l2Gateway Address of the Optimism ERC20 L2 standard bridge contract. |
| 66 | + * @param _tokenRecipient The L1 address which will unconditionally receive tokens from withdrawals by this contract. |
| 67 | + * @param _crossDomainAdmin Address of the admin on L1. This address is the only one which may tell this contract to send tokens to an |
| 68 | + * L2 address. |
| 69 | + * @param _spokePool The contract address of the Ovm_SpokePool which is deployed on this L2 network. |
| 70 | + */ |
| 71 | + constructor( |
| 72 | + IERC20 _l2Usdc, |
| 73 | + ITokenMessenger _cctpTokenMessenger, |
| 74 | + uint32 _destinationCircleDomainId, |
| 75 | + address _l2Gateway, |
| 76 | + address _tokenRecipient, |
| 77 | + IOvm_SpokePool _spokePool |
| 78 | + ) WithdrawalHelperBase(_l2Usdc, _cctpTokenMessenger, _destinationCircleDomainId, _l2Gateway, _tokenRecipient) { |
| 79 | + spokePool = _spokePool; |
| 80 | + |
| 81 | + // These addresses should only change network-by-network, or after a bridge upgrade, so we define them once in the constructor. |
| 82 | + wrappedNativeToken = spokePool.wrappedNativeToken(); |
| 83 | + l2Eth = spokePool.l2Eth(); |
| 84 | + } |
| 85 | + |
| 86 | + /** |
| 87 | + * @notice Initializes the withdrawal helper contract. |
| 88 | + * @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract. |
| 89 | + */ |
| 90 | + function initialize(address _crossDomainAdmin) public initializer { |
| 91 | + __WithdrawalHelper_init(_crossDomainAdmin); |
| 92 | + } |
| 93 | + |
| 94 | + /* |
| 95 | + * @notice Calls CCTP or the Optimism token gateway to withdraw tokens back to the recipient. |
| 96 | + * @param l2Token address of the l2Token to send back. |
| 97 | + * @param amountToReturn amount of l2Token to send back. |
| 98 | + * @dev The l1Token parameter is unused since we obtain the l1Token to receive by querying the state of the Ovm_SpokePool deployed |
| 99 | + * to this network. |
| 100 | + * @dev This function is a copy of the `_bridgeTokensToHubPool` function found on the Ovm_SpokePool contract here: |
| 101 | + * https://github.com/across-protocol/contracts/blob/65191dbcded95c8fe050e0f95eb7848e3784e61f/contracts/Ovm_SpokePool.sol#L148. |
| 102 | + * New lines of code correspond to instances where this contract queries state from the spoke pool, such as determining |
| 103 | + * the appropriate token bridge for the withdrawal or finding the remoteL1Token to withdraw. |
| 104 | + */ |
| 105 | + function withdrawToken( |
| 106 | + address, |
| 107 | + address l2Token, |
| 108 | + uint256 amountToReturn |
| 109 | + ) public override { |
| 110 | + // Fetch the current l1Gas defined in the Ovm_SpokePool. |
| 111 | + uint32 l1Gas = spokePool.l1Gas(); |
| 112 | + // If the token being bridged is WETH then we need to first unwrap it to ETH and then send ETH over the |
| 113 | + // canonical bridge. On Optimism, this is address 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000. |
| 114 | + if (l2Token == address(wrappedNativeToken)) { |
| 115 | + WETH9Interface(l2Token).withdraw(amountToReturn); // Unwrap into ETH. |
| 116 | + l2Token = l2Eth; // Set the l2Token to ETH. |
| 117 | + IL2ERC20Bridge(Lib_PredeployAddresses.L2_STANDARD_BRIDGE).withdrawTo{ value: amountToReturn }( |
| 118 | + l2Token, // _l2Token. Address of the L2 token to bridge over. |
| 119 | + TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the l1 pool contract. |
| 120 | + amountToReturn, // _amount. |
| 121 | + l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations |
| 122 | + "" // _data. We don't need to send any data for the bridging action. |
| 123 | + ); |
| 124 | + } |
| 125 | + // If the token is USDC && CCTP bridge is enabled, then bridge USDC via CCTP. |
| 126 | + else if (l2Token == address(usdcToken) && _isCCTPEnabled()) { |
| 127 | + _transferUsdc(TOKEN_RECIPIENT, amountToReturn); |
| 128 | + } |
| 129 | + // Note we'll default to withdrawTo instead of bridgeERC20To unless the remoteL1Tokens mapping is set for |
| 130 | + // the l2Token. withdrawTo should be used to bridge back non-native L2 tokens |
| 131 | + // (i.e. non-native L2 tokens have a canonical L1 token). If we should bridge "native L2" tokens then |
| 132 | + // we'd need to call bridgeERC20To and give allowance to the tokenBridge to spend l2Token from this contract. |
| 133 | + // Therefore for native tokens we should set ensure that remoteL1Tokens is set for the l2Token. |
| 134 | + else { |
| 135 | + IL2ERC20Bridge tokenBridge = IL2ERC20Bridge( |
| 136 | + spokePool.tokenBridges(l2Token) == address(0) |
| 137 | + ? Lib_PredeployAddresses.L2_STANDARD_BRIDGE |
| 138 | + : spokePool.tokenBridges(l2Token) |
| 139 | + ); |
| 140 | + address remoteL1Token = spokePool.remoteL1Tokens(l2Token); |
| 141 | + if (remoteL1Token != address(0)) { |
| 142 | + // If there is a mapping for this L2 token to an L1 token, then use the L1 token address and |
| 143 | + // call bridgeERC20To. |
| 144 | + IERC20(l2Token).safeIncreaseAllowance(address(tokenBridge), amountToReturn); |
| 145 | + tokenBridge.bridgeERC20To( |
| 146 | + l2Token, // _l2Token. Address of the L2 token to bridge over. |
| 147 | + remoteL1Token, // Remote token to be received on L1 side. If the |
| 148 | + // remoteL1Token on the other chain does not recognize the local token as the correct |
| 149 | + // pair token, the ERC20 bridge will fail and the tokens will be returned to sender on |
| 150 | + // this chain. |
| 151 | + TOKEN_RECIPIENT, // _to |
| 152 | + amountToReturn, // _amount |
| 153 | + l1Gas, // _l1Gas |
| 154 | + "" // _data |
| 155 | + ); |
| 156 | + } else { |
| 157 | + tokenBridge.withdrawTo( |
| 158 | + l2Token, // _l2Token. Address of the L2 token to bridge over. |
| 159 | + TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the l1 pool contract. |
| 160 | + amountToReturn, // _amount. |
| 161 | + l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations |
| 162 | + "" // _data. We don't need to send any data for the bridging action. |
| 163 | + ); |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + function _requireAdminSender() internal view override { |
| 169 | + if (LibOptimismUpgradeable.crossChainSender(MESSENGER) != crossDomainAdmin) revert NotCrossDomainAdmin(); |
| 170 | + } |
| 171 | +} |
0 commit comments