diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..99a93420 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 + +[*.{js,yml,json,cjs}] +indent_size = 2 +max_line_length = 120 diff --git a/.env.example b/.env.example index 2f00c733..3838caf4 100644 --- a/.env.example +++ b/.env.example @@ -25,11 +25,41 @@ ETHERSCAN_API_KEY_OPT= # Bridge/Gateway Deployment # ############################ -# Address of the token to deploy the bridge/gateway for +# Address of the token on L1 to deploy the bridge/gateway for TOKEN= +# Address of the rebasable token on L1 to deploy the bridge/gateway for +REBASABLE_TOKEN= + +# Address of token rate pusher. Required to config TokenRateOracle. +L1_OP_STACK_TOKEN_RATE_PUSHER= + +# Gas limit required to complete pushing token rate on L2. +# Default is: 300_000. +# This value was calculated by formula: +# l2GasLimit = (gas cost of L2Bridge.finalizeDeposit() + OptimismPortal.minimumGasLimit(depositData.length)) * 1.5 +L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE= + +# A time period when token rate can be considered outdated. +TOKEN_RATE_OUTDATED_DELAY=86400 # default is 86400 (24 hours) + +# Address of L1 token bridge proxy. +L1_TOKEN_BRIDGE= + +# Address of L2 token bridge proxy. +L2_TOKEN_BRIDGE= + +# Address of the non-rebasable token proxy on L2. +L2_TOKEN= + +# Address of token rate oracle on L2 +L2_TOKEN_RATE_ORACLE= + +# Address of bridge executor. +GOV_BRIDGE_EXECUTOR= + # Name of the network environments used by deployment scripts. -# Might be one of: "mainnet", "goerli". +# Might be one of: "mainnet", "sepolia". NETWORK=mainnet # Run deployment in the forking network instead of public ones @@ -74,6 +104,10 @@ TESTING_ARB_L2_GATEWAY_ROUTER=0x57f54f87C44d816f60b92864e23b8c0897D4d81D TESTING_OPT_NETWORK= TESTING_OPT_L1_TOKEN=0xaF8a2F0aE374b03376155BF745A3421Dac711C12 TESTING_OPT_L2_TOKEN=0xAED5F9aaF167923D34174b8E636aaF040A11f6F7 +TESTING_OPT_L1_TOKEN_RATE_NOTIFIER=0x554f2C7D58522c050d38Ebea4FF072ED7C4e61cb +TESTING_OPT_L1_REBASABLE_TOKEN=0xB82381A3fBD3FaFA77B3a7bE693342618240067b +TESTING_OPT_L2_REBASABLE_TOKEN=0x6696Cb7bb602FC744254Ad9E07EfC474FBF78857 +TESTING_OPT_L2_TOKEN_RATE_ORACLE=0x8ea513d1e5Be31fb5FC2f2971897594720de9E70 TESTING_OPT_L1_ERC20_TOKEN_BRIDGE=0x243b661276670bD17399C488E7287ea4D416115b TESTING_OPT_L2_ERC20_TOKEN_BRIDGE=0x447CD1794d209Ac4E6B4097B34658bc00C4d0a51 diff --git a/.env.wsteth.opt_mainnet b/.env.wsteth.opt_mainnet index 6bf3ae55..86ebcad0 100644 --- a/.env.wsteth.opt_mainnet +++ b/.env.wsteth.opt_mainnet @@ -21,6 +21,9 @@ ETHERSCAN_API_KEY_OPT= # Address of the token to deploy the bridge/gateway for TOKEN=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 +# Address of the rebasable token to deploy the bridge/gateway for +REBASABLE_TOKEN= + # Name of the network environments used by deployment scripts. # Might be one of: "mainnet", "goerli". NETWORK=mainnet diff --git a/.storage-layout b/.storage-layout index 393bb17e..e7f10d83 100644 --- a/.storage-layout +++ b/.storage-layout @@ -58,12 +58,12 @@ |------|------|------|--------|-------|----------| ======================= -➡ L1ERC20TokenBridge +➡ L1ERC20ExtendedTokensBridge ======================= | Name | Type | Slot | Offset | Bytes | Contract | |--------|---------------------------------------------------|------|--------|-------|--------------------------------------------------------------| -| _roles | mapping(bytes32 => struct AccessControl.RoleData) | 0 | 0 | 32 | contracts/optimism/L1ERC20TokenBridge.sol:L1ERC20TokenBridge | +| _roles | mapping(bytes32 => struct AccessControl.RoleData) | 0 | 0 | 32 | contracts/optimism/L1ERC20ExtendedTokensBridge.sol:L1ERC20ExtendedTokensBridge | ======================= ➡ L1ERC20TokenGateway @@ -81,12 +81,12 @@ |------|------|------|--------|-------|----------| ======================= -➡ L2ERC20TokenBridge +➡ L2ERC20ExtendedTokensBridge ======================= | Name | Type | Slot | Offset | Bytes | Contract | |--------|---------------------------------------------------|------|--------|-------|--------------------------------------------------------------| -| _roles | mapping(bytes32 => struct AccessControl.RoleData) | 0 | 0 | 32 | contracts/optimism/L2ERC20TokenBridge.sol:L2ERC20TokenBridge | +| _roles | mapping(bytes32 => struct AccessControl.RoleData) | 0 | 0 | 32 | contracts/optimism/L2ERC20ExtendedTokensBridge.sol:L2ERC20ExtendedTokensBridge | ======================= ➡ L2ERC20TokenGateway diff --git a/README.md b/README.md index 5f9ca478..ce8ce02d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ To retrieve more detailed info about the bridging process, see the specification - [Lido's Arbitrum Gateway](https://github.com/lidofinance/lido-l2/blob/main/contracts/arbitrum/README.md). - [Lido's Optimism Bridge](https://github.com/lidofinance/lido-l2/blob/main/contracts/optimism/README.md). +- [wstETH Bridging Guide](https://docs.lido.fi/token-guides/wsteth-bridging-guide/#r-5-bridging-l1-lido-dao-decisions) ## Project setup @@ -42,8 +43,17 @@ Fill the newly created `.env` file with the required variables. See the [Project The configuration of the deployment scripts happens via the ENV variables. The following variables are required: -- [`TOKEN`](#TOKEN) - address of the token to deploy a new bridge on the Ethereum chain. -- [`NETWORK`](#NETWORK) - name of the network environments used by deployment scripts. Allowed values: `mainnet`, `goerli`. +- [`TOKEN`](#TOKEN) - address of the non-rebasable token to deploy a new bridge on the Ethereum chain. +- [`REBASABLE_TOKEN`] (#REBASABLE_TOKEN) - address of the rebasable token to deploy new bridge on the Ethereum chain. +- [`L1_OP_STACK_TOKEN_RATE_PUSHER`](#L1_OP_STACK_TOKEN_RATE_PUSHER) - address of token rate pusher. Required to config TokenRateOracle. +- [`L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE`](#L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE) - gas limit required to complete pushing token rate on L2.This value was calculated by formula: l2GasLimit = (gas cost of L2Bridge.finalizeDeposit() + OptimismPortal.minimumGasLimit(depositData.length)) * 1.5 +- [`TOKEN_RATE_OUTDATED_DELAY`](#TOKEN_RATE_OUTDATED_DELAY) - a time period when token rate can be considered outdated. Default is 86400 (24 hours). +- [`L1_TOKEN_BRIDGE`](#L1_TOKEN_BRIDGE) - address of L1 token bridge. +- [`L2_TOKEN_BRIDGE`](#L2_TOKEN_BRIDGE) - address of L2 token bridge. +- [`L2_TOKEN`](#L2_TOKEN) - address of the non-rebasable token on L2. +- [`L2_TOKEN_RATE_ORACLE`](#L2_TOKEN_RATE_ORACLE) - address of token rate oracle on L2. +- [`GOV_BRIDGE_EXECUTOR`](#GOV_BRIDGE_EXECUTOR) - address of bridge executor. +- [`NETWORK`](#NETWORK) - name of the network environments used by deployment scripts. Allowed values: `mainnet`, `sepolia`. - [`FORKING`](#FORKING) - run deployment in the forking network instead of real ones - [`ETH_DEPLOYER_PRIVATE_KEY`](#ETH_DEPLOYER_PRIVATE_KEY) - The private key of the deployer account in the Ethereum network is used during the deployment process. - [`ARB_DEPLOYER_PRIVATE_KEY`](#ARB_DEPLOYER_PRIVATE_KEY) - The private key of the deployer account in the Arbitrum network is used during the deployment process. @@ -314,13 +324,17 @@ Below variables used in the Arbitrum/Optimism bridge deployment process. #### `TOKEN` -Address of the token to deploy a new bridge on the Ethereum chain. +Address of the existing non-rebasable token to deploy a new bridge for on the Ethereum chain. + +#### `REBASABLE_TOKEN` + +Address of the existing rebasable token to deploy new bridge for on the Ethereum chain. #### `NETWORK` > Default value: `mainnet` -Name of the network environments used by deployment scripts. Might be one of: `mainnet`, `goerli`. +Name of the network environments used by deployment scripts. Might be one of: `mainnet`, `sepolia`. #### `FORKING` @@ -442,7 +456,7 @@ The following variables are used in the process of the Integration & E2E testing #### `TESTING_ARB_NETWORK` -Name of the network environments used for Arbitrum Integration & E2E testing. Might be one of: `mainnet`, `goerli`. +Name of the network environments used for Arbitrum Integration & E2E testing. Might be one of: `mainnet`, `sepolia`. #### `TESTING_ARB_L1_TOKEN` @@ -482,7 +496,7 @@ Address of the L2 gateway router used in the Acceptance Integration & E2E (when #### `TESTING_OPT_NETWORK` -Name of the network environments used for Optimism Integration & E2E testing. Might be one of: `mainnet`, `goerli`. +Name of the network environments used for Optimism Integration & E2E testing. Might be one of: `mainnet`, `sepolia`. #### `TESTING_OPT_L1_TOKEN` diff --git a/artifacts-opt.json b/artifacts-opt.json index b97c0d24..7a302e37 100644 --- a/artifacts-opt.json +++ b/artifacts-opt.json @@ -2,13 +2,13 @@ { "artifactPath": "artifacts/contracts/proxy/OssifiableProxy.sol/OssifiableProxy.json", "sourcePath": "contracts/proxy/OssifiableProxy.sol", - "name": "L2ERC20TokenBridge proxy", + "name": "L2ERC20ExtendedTokensBridge proxy", "address": "0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957" }, { - "artifactPath": "artifacts/contracts/optimism/L2ERC20TokenBridge.sol/L2ERC20TokenBridge.json", - "sourcePath": "contracts/optimism/L2ERC20TokenBridge.sol", - "name": "L2ERC20TokenBridge", + "artifactPath": "artifacts/contracts/optimism/L2ERC20ExtendedTokensBridge.sol/L2ERC20ExtendedTokensBridge.json", + "sourcePath": "contracts/optimism/L2ERC20ExtendedTokensBridge.sol", + "name": "L2ERC20ExtendedTokensBridge", "address": "0x23B96aDD54c479C6784Dd504670B5376B808f4C7", "txHash": "0x5d69e9c6ec1d634f0d90812c2189c925993d1fffbc9b0b416fdc123e15407c56" }, diff --git a/contracts/arbitrum/L2ERC20TokenGateway.sol b/contracts/arbitrum/L2ERC20TokenGateway.sol index 5853d0ac..f7850448 100644 --- a/contracts/arbitrum/L2ERC20TokenGateway.sol +++ b/contracts/arbitrum/L2ERC20TokenGateway.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.10; -import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; +import {IERC20Bridged} from "../token/ERC20Bridged.sol"; import {IL2TokenGateway, IInterchainTokenGateway} from "./interfaces/IL2TokenGateway.sol"; import {L2CrossDomainEnabled} from "./L2CrossDomainEnabled.sol"; diff --git a/contracts/arbitrum/README.md b/contracts/arbitrum/README.md index cf76c5c1..4197694f 100644 --- a/contracts/arbitrum/README.md +++ b/contracts/arbitrum/README.md @@ -707,7 +707,6 @@ Transfers `amount` of token from the `from_` account to `to_` using the allowanc > - **`addedValue_`** - a number to increase allowance > > **Emits:** `Approval(address indexed owner, address indexed spender, uint256 value)` - Atomically increases the allowance granted to `spender` by the caller. Returns a `bool` value indicating whether the operation succeed. #### `decreaseAllowance(address,uint256)` @@ -722,7 +721,6 @@ Atomically increases the allowance granted to `spender` by the caller. Returns a > - **`subtractedValue_`** - a number to decrease allowance > > **Emits:** `Approval(address indexed owner, address indexed spender, uint256 value)` - Atomically decreases the allowance granted to `spender` by the caller. Returns a `bool` value indicating whether the operation succeed. ## `ERC20Bridged` diff --git a/contracts/lib/DepositDataCodec.sol b/contracts/lib/DepositDataCodec.sol new file mode 100644 index 00000000..af8a9910 --- /dev/null +++ b/contracts/lib/DepositDataCodec.sol @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice encodes and decodes DepositData for crosschain transfering. +library DepositDataCodec { + + uint8 internal constant RATE_FIELD_SIZE = 12; + uint8 internal constant TIMESTAMP_FIELD_SIZE = 5; + + struct DepositData { + uint96 rate; + uint40 timestamp; + bytes data; + } + + function encodeDepositData(DepositData memory depositData) internal pure returns (bytes memory) { + bytes memory data = bytes.concat( + abi.encodePacked(depositData.rate), + abi.encodePacked(depositData.timestamp), + abi.encodePacked(depositData.data) + ); + return data; + } + + function decodeDepositData(bytes calldata buffer) internal pure returns (DepositData memory) { + + if (buffer.length < RATE_FIELD_SIZE + TIMESTAMP_FIELD_SIZE) { + revert ErrorDepositDataLength(); + } + + DepositData memory depositData = DepositData({ + rate: uint96(bytes12(buffer[0:RATE_FIELD_SIZE])), + timestamp: uint40(bytes5(buffer[RATE_FIELD_SIZE:RATE_FIELD_SIZE + TIMESTAMP_FIELD_SIZE])), + data: buffer[RATE_FIELD_SIZE + TIMESTAMP_FIELD_SIZE:] + }); + + return depositData; + } + + error ErrorDepositDataLength(); +} diff --git a/contracts/lib/ECDSA.sol b/contracts/lib/ECDSA.sol new file mode 100644 index 00000000..0f694d8d --- /dev/null +++ b/contracts/lib/ECDSA.sol @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: MIT + +// Extracted from: +// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/cryptography/ECDSA.sol#L53 +// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/541e821/contracts/utils/cryptography/ECDSA.sol#L112 + +pragma solidity 0.8.10; + +library ECDSA { + /** + * @dev Returns the address that signed a hashed message (`hash`). + * This address can then be used for verification purposes. + * Receives the `v`, `r` and `s` signature fields separately. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) + { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "ECDSA: invalid signature 's' value"); + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + require(signer != address(0), "ECDSA: invalid signature"); + + return signer; + } + + /** + * @dev Overload of `recover` that receives the `r` and `vs` short-signature fields separately. + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + bytes32 s; + uint8 v; + assembly { + s := and(vs, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) + v := add(shr(255, vs), 27) + } + return recover(hash, v, r, s); + } +} diff --git a/contracts/lib/SignatureChecker.sol b/contracts/lib/SignatureChecker.sol new file mode 100644 index 00000000..183e0266 --- /dev/null +++ b/contracts/lib/SignatureChecker.sol @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {ECDSA} from "./ECDSA.sol"; + +/// @dev A copy of SignatureUtils.sol contract from Lido Core Protocol +/// https://github.com/lidofinance/lido-dao/blob/master/contracts/common/lib/SignatureUtils.sol +library SignatureChecker { + /** + * @dev The selector of the ERC1271's `isValidSignature(bytes32 hash, bytes signature)` function, + * serving at the same time as the magic value that the function should return upon success. + * + * See https://eips.ethereum.org/EIPS/eip-1271. + * + * bytes4(keccak256("isValidSignature(bytes32,bytes)") + */ + bytes4 internal constant ERC1271_IS_VALID_SIGNATURE_SELECTOR = 0x1626ba7e; + + /** + * @dev Checks signature validity. + * + * If the signer address doesn't contain any code, assumes that the address is externally owned + * and the signature is a ECDSA signature generated using its private key. Otherwise, issues a + * static call to the signer address to check the signature validity using the ERC-1271 standard. + */ + function isValidSignature( + address signer, + bytes32 msgHash, + uint8 v, + bytes32 r, + bytes32 s + ) internal view returns (bool) { + if (_hasCode(signer)) { + bytes memory sig = abi.encodePacked(r, s, v); + // Solidity <0.5 generates a regular CALL instruction even if the function being called + // is marked as `view`, and the only way to perform a STATICCALL is to use assembly + bytes memory data = abi.encodeWithSelector(ERC1271_IS_VALID_SIGNATURE_SELECTOR, msgHash, sig); + bytes32 retval; + /// @solidity memory-safe-assembly + assembly { + // allocate memory for storing the return value + let outDataOffset := mload(0x40) + mstore(0x40, add(outDataOffset, 32)) + // issue a static call and load the result if the call succeeded + let success := staticcall(gas(), signer, add(data, 32), mload(data), outDataOffset, 32) + if and(eq(success, 1), eq(returndatasize(), 32)) { + retval := mload(outDataOffset) + } + } + return retval == bytes32(ERC1271_IS_VALID_SIGNATURE_SELECTOR); + } else { + return ECDSA.recover(msgHash, v, r, s) == signer; + } + } + + function _hasCode(address addr) internal view returns (bool) { + uint256 size; + /// @solidity memory-safe-assembly + assembly { size := extcodesize(addr) } + return size > 0; + } +} diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol new file mode 100644 index 00000000..24ca4c31 --- /dev/null +++ b/contracts/lido/TokenRateNotifier.sol @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRatePusher} from "./interfaces/ITokenRatePusher.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; + +/// @notice An interface to subscribe on the `stETH` token rebases (defined in the `Lido` core contract) +interface IPostTokenRebaseReceiver { + + /// @notice Is called in the context of `Lido.handleOracleReport` to notify the subscribers about each token rebase + function handlePostTokenRebase( + uint256 _reportTimestamp, + uint256 _timeElapsed, + uint256 _preTotalShares, + uint256 _preTotalEther, + uint256 _postTotalShares, + uint256 _postTotalEther, + uint256 _sharesMintedAsFees + ) external; +} + +/// @author kovalgek +/// @notice Notifies all observers when rebase event occures. +contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { + using ERC165Checker for address; + + /// @notice Maximum amount of observers to be supported. + uint256 public constant MAX_OBSERVERS_COUNT = 32; + + /// @notice A value that indicates that value was not found. + uint256 public constant INDEX_NOT_FOUND = type(uint256).max; + + /// @notice An interface that each observer should support. + bytes4 public constant REQUIRED_INTERFACE = type(ITokenRatePusher).interfaceId; + + /// @notice All observers. + address[] public observers; + + /// @param initialOwner_ initial owner + constructor(address initialOwner_) { + if (initialOwner_ == address(0)) { + revert ErrorZeroAddressOwner(); + } + _transferOwnership(initialOwner_); + } + + /// @notice Add a `observer_` to the back of array + /// @param observer_ observer address + function addObserver(address observer_) external onlyOwner { + if (observer_ == address(0)) { + revert ErrorZeroAddressObserver(); + } + if (!observer_.supportsInterface(REQUIRED_INTERFACE)) { + revert ErrorBadObserverInterface(); + } + if (observers.length >= MAX_OBSERVERS_COUNT) { + revert ErrorMaxObserversCountExceeded(); + } + if (_observerIndex(observer_) != INDEX_NOT_FOUND) { + revert ErrorAddExistedObserver(); + } + + observers.push(observer_); + emit ObserverAdded(observer_); + } + + /// @notice Remove a observer at the given `observer_` position + /// @param observer_ observer remove position + function removeObserver(address observer_) external onlyOwner { + + uint256 observerIndexToRemove = _observerIndex(observer_); + + if (observerIndexToRemove == INDEX_NOT_FOUND) { + revert ErrorNoObserverToRemove(); + } + if (observers.length > 1) { + observers[observerIndexToRemove] = observers[observers.length - 1]; + } + observers.pop(); + + emit ObserverRemoved(observer_); + } + + /// @inheritdoc IPostTokenRebaseReceiver + /// @dev Parameters aren't used because all required data further components fetch by themselves. + function handlePostTokenRebase( + uint256, /* reportTimestamp */ + uint256, /* timeElapsed */ + uint256, /* preTotalShares */ + uint256, /* preTotalEther */ + uint256, /* postTotalShares */ + uint256, /* postTotalEther */ + uint256 /* sharesMintedAsFees */ + ) external { + for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { + // solhint-disable-next-line no-empty-blocks + try ITokenRatePusher(observers[obIndex]).pushTokenRate() {} + catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the pushTokenRate() reverts because of the + /// "out of gas" error. Here we assume that the pushTokenRate() method doesn't + /// have reverts with empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert ErrorTokenRateNotifierRevertedWithNoData(); + emit PushTokenRateFailed( + observers[obIndex], + lowLevelRevertData + ); + } + } + } + + /// @notice Observer length + /// @return Added observers count + function observersLength() external view returns (uint256) { + return observers.length; + } + + /// @notice `observer_` index in `observers` array. + /// @return An index of `observer_` or `INDEX_NOT_FOUND` if it wasn't found. + function _observerIndex(address observer_) internal view returns (uint256) { + for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { + if (observers[obIndex] == observer_) { + return obIndex; + } + } + return INDEX_NOT_FOUND; + } + + event PushTokenRateFailed(address indexed observer, bytes lowLevelRevertData); + event ObserverAdded(address indexed observer); + event ObserverRemoved(address indexed observer); + + error ErrorTokenRateNotifierRevertedWithNoData(); + error ErrorZeroAddressObserver(); + error ErrorBadObserverInterface(); + error ErrorMaxObserversCountExceeded(); + error ErrorNoObserverToRemove(); + error ErrorZeroAddressOwner(); + error ErrorAddExistedObserver(); +} diff --git a/contracts/lido/interfaces/ITokenRatePusher.sol b/contracts/lido/interfaces/ITokenRatePusher.sol new file mode 100644 index 00000000..9d157c3c --- /dev/null +++ b/contracts/lido/interfaces/ITokenRatePusher.sol @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice An interface for entity that pushes token rate. +interface ITokenRatePusher { + /// @notice Pushes token rate to L2 by depositing zero token amount. + function pushTokenRate() external; +} diff --git a/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol new file mode 100644 index 00000000..0ce7974c --- /dev/null +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRatePusher} from "../interfaces/ITokenRatePusher.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/// @dev For testing purposes. +contract OpStackTokenRatePusherWithOutOfGasErrorStub is ERC165, ITokenRatePusher { + + uint256 public constant OUT_OF_GAS_INCURRING_MAX = 1000000000000; + + mapping (uint256 => uint256) public data; + + function pushTokenRate() external { + for (uint256 i = 0; i < OUT_OF_GAS_INCURRING_MAX; ++i) { + data[i] = i; + } + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(ITokenRatePusher).interfaceId + || super.supportsInterface(_interfaceId) + ); + } +} diff --git a/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol b/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol new file mode 100644 index 00000000..6df0b7fa --- /dev/null +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRatePusher} from "../interfaces/ITokenRatePusher.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/// @dev For testing purposes. +contract OpStackTokenRatePusherWithSomeErrorStub is ERC165, ITokenRatePusher { + + error SomeError(); + + function pushTokenRate() pure external { + revert SomeError(); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(ITokenRatePusher).interfaceId + || super.supportsInterface(_interfaceId) + ); + } +} diff --git a/contracts/optimism/CrossDomainEnabled.sol b/contracts/optimism/CrossDomainEnabled.sol index 0fe0e5bb..23935681 100644 --- a/contracts/optimism/CrossDomainEnabled.sol +++ b/contracts/optimism/CrossDomainEnabled.sol @@ -8,11 +8,11 @@ import {ICrossDomainMessenger} from "./interfaces/ICrossDomainMessenger.sol"; /// @dev Helper contract for contracts performing cross-domain communications contract CrossDomainEnabled { /// @notice Messenger contract used to send and receive messages from the other domain - ICrossDomainMessenger public immutable messenger; + ICrossDomainMessenger public immutable MESSENGER; /// @param messenger_ Address of the CrossDomainMessenger on the current layer constructor(address messenger_) { - messenger = ICrossDomainMessenger(messenger_); + MESSENGER = ICrossDomainMessenger(messenger_); } /// @dev Sends a message to an account on another domain @@ -25,17 +25,17 @@ contract CrossDomainEnabled { uint32 gasLimit_, bytes memory message_ ) internal { - messenger.sendMessage(crossDomainTarget_, message_, gasLimit_); + MESSENGER.sendMessage(crossDomainTarget_, message_, gasLimit_); } /// @dev Enforces that the modified function is only callable by a specific cross-domain account /// @param sourceDomainAccount_ The only account on the originating domain which is /// authenticated to call this function modifier onlyFromCrossDomainAccount(address sourceDomainAccount_) { - if (msg.sender != address(messenger)) { + if (msg.sender != address(MESSENGER)) { revert ErrorUnauthorizedMessenger(); } - if (messenger.xDomainMessageSender() != sourceDomainAccount_) { + if (MESSENGER.xDomainMessageSender() != sourceDomainAccount_) { revert ErrorWrongCrossDomainSender(); } _; diff --git a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol new file mode 100644 index 00000000..8bdaf2d5 --- /dev/null +++ b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; +import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; +import {BridgingManager} from "../BridgingManager.sol"; +import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {DepositDataCodec} from "../lib//DepositDataCodec.sol"; + +/// @author psirex, kovalgek +/// @notice The L1 ERC20 token bridge locks bridged tokens on the L1 side, sends deposit messages +/// on the L2 side, and finalizes token withdrawals from L2. Additionally, adds the methods for +/// bridging management: enabling and disabling withdrawals/deposits +abstract contract L1ERC20ExtendedTokensBridge is + IL1ERC20Bridge, + BridgingManager, + RebasableAndNonRebasableTokens, + CrossDomainEnabled +{ + using SafeERC20 for IERC20; + + address private immutable L2_TOKEN_BRIDGE; + + /// @param messenger_ L1 messenger address being used for cross-chain communications + /// @param l2TokenBridge_ Address of the corresponding L2 bridge + /// @param l1TokenNonRebasable_ Address of the bridged token in the L1 chain + /// @param l1TokenRebasable_ Address of the bridged token in the L1 chain + /// @param l2TokenNonRebasable_ Address of the token minted on the L2 chain when token bridged + /// @param l2TokenRebasable_ Address of the token minted on the L2 chain when token bridged + constructor( + address messenger_, + address l2TokenBridge_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens( + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { + L2_TOKEN_BRIDGE = l2TokenBridge_; + } + + /// @notice required to abstact a way token rate is requested. + function tokenRate() virtual internal view returns (uint256); + + /// @inheritdoc IL1ERC20Bridge + function l2TokenBridge() external view returns (address) { + return L2_TOKEN_BRIDGE; + } + + /// @inheritdoc IL1ERC20Bridge + function depositERC20( + address l1Token_, + address l2Token_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) + external + whenDepositsEnabled + onlySupportedL1L2TokensPair(l1Token_, l2Token_) + { + if (Address.isContract(msg.sender)) { + revert ErrorSenderNotEOA(); + } + bytes memory encodedDepositData = DepositDataCodec.encodeDepositData(DepositDataCodec.DepositData({ + rate: uint96(tokenRate()), + timestamp: uint40(block.timestamp), + data: data_ + })); + _depositERC20To(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, encodedDepositData); + emit ERC20DepositInitiated(l1Token_, l2Token_, msg.sender, msg.sender, amount_, encodedDepositData); + } + + /// @inheritdoc IL1ERC20Bridge + function depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) + external + whenDepositsEnabled + onlyNonZeroAccount(to_) + onlySupportedL1L2TokensPair(l1Token_, l2Token_) + { + bytes memory encodedDepositData = DepositDataCodec.encodeDepositData(DepositDataCodec.DepositData({ + rate: uint96(tokenRate()), + timestamp: uint40(block.timestamp), + data: data_ + })); + _depositERC20To(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + emit ERC20DepositInitiated(l1Token_, l2Token_, msg.sender, to_, amount_, encodedDepositData); + } + + /// @inheritdoc IL1ERC20Bridge + function finalizeERC20Withdrawal( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) + external + whenWithdrawalsEnabled + onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) + onlySupportedL1L2TokensPair(l1Token_, l2Token_) + { + uint256 amountToWithdraw = (_isRebasable(l1Token_) && amount_ != 0) ? + IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_) : + amount_; + IERC20(l1Token_).safeTransfer(to_, amountToWithdraw); + emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, amountToWithdraw, data_); + } + + /// @dev Performs the logic for deposits by informing the L2 token bridge contract + /// of the deposit and calling safeTransferFrom to lock the L1 funds. + /// @param l1Token_ Address of the L1 ERC20 we are depositing + /// @param l2Token_ Address of the L1 respective L2 ERC20 + /// @param from_ Account to pull the deposit from on L1 + /// @param to_ Account to give the deposit to on L2 + /// @param amount_ Amount of the ERC20 to deposit. + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + /// @param encodedDepositData_ a concatenation of packed token rate with L1 time and + /// optional data passed by external contract + function _depositERC20To( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes memory encodedDepositData_ + ) internal { + uint256 amountToDeposit = _transferToBridge(l1Token_, from_, amount_); + + bytes memory message = abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + l1Token_, l2Token_, from_, to_, amountToDeposit, encodedDepositData_ + ); + + sendCrossDomainMessage(L2_TOKEN_BRIDGE, l2Gas_, message); + } + + /// @dev Transfers tokens to the bridge and wraps if needed. + /// @param l1Token_ Address of the L1 ERC20 we are depositing. + /// @param from_ Account to pull the deposit from on L1. + /// @param amount_ Amount of the ERC20 to deposit. + /// @return Amount of non-rebasable token. + function _transferToBridge( + address l1Token_, + address from_, + uint256 amount_ + ) internal returns (uint256) { + if (amount_ != 0) { + IERC20(l1Token_).safeTransferFrom(from_, address(this), amount_); + if(_isRebasable(l1Token_)) { + if(!IERC20(l1Token_).approve(L1_TOKEN_NON_REBASABLE, amount_)) revert ErrorRebasableTokenApprove(); + return IERC20Wrapper(L1_TOKEN_NON_REBASABLE).wrap(amount_); + } + } + return amount_; + } + + error ErrorSenderNotEOA(); + error ErrorRebasableTokenApprove(); +} diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol deleted file mode 100644 index a4438b88..00000000 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; -import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; - -import {BridgingManager} from "../BridgingManager.sol"; -import {BridgeableTokens} from "../BridgeableTokens.sol"; -import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; - -/// @author psirex -/// @notice The L1 ERC20 token bridge locks bridged tokens on the L1 side, sends deposit messages -/// on the L2 side, and finalizes token withdrawals from L2. Additionally, adds the methods for -/// bridging management: enabling and disabling withdrawals/deposits -contract L1ERC20TokenBridge is - IL1ERC20Bridge, - BridgingManager, - BridgeableTokens, - CrossDomainEnabled -{ - using SafeERC20 for IERC20; - - /// @inheritdoc IL1ERC20Bridge - address public immutable l2TokenBridge; - - /// @param messenger_ L1 messenger address being used for cross-chain communications - /// @param l2TokenBridge_ Address of the corresponding L2 bridge - /// @param l1Token_ Address of the bridged token in the L1 chain - /// @param l2Token_ Address of the token minted on the L2 chain when token bridged - constructor( - address messenger_, - address l2TokenBridge_, - address l1Token_, - address l2Token_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l2Token_) { - l2TokenBridge = l2TokenBridge_; - } - - /// @inheritdoc IL1ERC20Bridge - function depositERC20( - address l1Token_, - address l2Token_, - uint256 amount_, - uint32 l2Gas_, - bytes calldata data_ - ) - external - whenDepositsEnabled - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) - { - if (Address.isContract(msg.sender)) { - revert ErrorSenderNotEOA(); - } - _initiateERC20Deposit(msg.sender, msg.sender, amount_, l2Gas_, data_); - } - - /// @inheritdoc IL1ERC20Bridge - function depositERC20To( - address l1Token_, - address l2Token_, - address to_, - uint256 amount_, - uint32 l2Gas_, - bytes calldata data_ - ) - external - whenDepositsEnabled - onlyNonZeroAccount(to_) - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) - { - _initiateERC20Deposit(msg.sender, to_, amount_, l2Gas_, data_); - } - - /// @inheritdoc IL1ERC20Bridge - function finalizeERC20Withdrawal( - address l1Token_, - address l2Token_, - address from_, - address to_, - uint256 amount_, - bytes calldata data_ - ) - external - whenWithdrawalsEnabled - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) - onlyFromCrossDomainAccount(l2TokenBridge) - { - IERC20(l1Token_).safeTransfer(to_, amount_); - - emit ERC20WithdrawalFinalized( - l1Token_, - l2Token_, - from_, - to_, - amount_, - data_ - ); - } - - /// @dev Performs the logic for deposits by informing the L2 token bridge contract - /// of the deposit and calling safeTransferFrom to lock the L1 funds. - /// @param from_ Account to pull the deposit from on L1 - /// @param to_ Account to give the deposit to on L2 - /// @param amount_ Amount of the ERC20 to deposit. - /// @param l2Gas_ Gas limit required to complete the deposit on L2. - /// @param data_ Optional data to forward to L2. This data is provided - /// solely as a convenience for external contracts. Aside from enforcing a maximum - /// length, these contracts provide no guarantees about its content. - function _initiateERC20Deposit( - address from_, - address to_, - uint256 amount_, - uint32 l2Gas_, - bytes calldata data_ - ) internal { - IERC20(l1Token).safeTransferFrom(from_, address(this), amount_); - - bytes memory message = abi.encodeWithSelector( - IL2ERC20Bridge.finalizeDeposit.selector, - l1Token, - l2Token, - from_, - to_, - amount_, - data_ - ); - - sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); - - emit ERC20DepositInitiated( - l1Token, - l2Token, - from_, - to_, - amount_, - data_ - ); - } - - error ErrorSenderNotEOA(); -} diff --git a/contracts/optimism/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol new file mode 100644 index 00000000..d749e034 --- /dev/null +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {L1ERC20ExtendedTokensBridge} from "./L1ERC20ExtendedTokensBridge.sol"; + +/// @author kovalgek +/// @notice A subset of wstETH token interface of core LIDO protocol. +interface IERC20WstETH { + /// @notice Get amount of wstETH for a one stETH + /// @return Amount of wstETH for a 1 stETH + function stEthPerToken() external view returns (uint256); +} + +/// @author kovalgek +/// @notice Hides wstETH concept from other contracts to keep `L1ERC20ExtendedTokensBridge` reusable. +contract L1LidoTokensBridge is L1ERC20ExtendedTokensBridge { + + constructor( + address messenger_, + address l2TokenBridge_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) L1ERC20ExtendedTokensBridge( + messenger_, + l2TokenBridge_, + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { + } + + function tokenRate() override internal view returns (uint256) { + return IERC20WstETH(L1_TOKEN_NON_REBASABLE).stEthPerToken(); + } +} diff --git a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol new file mode 100644 index 00000000..ccba2bdd --- /dev/null +++ b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; +import {IERC20Bridged} from "../token/ERC20Bridged.sol"; +import {ITokenRateUpdatable} from "../optimism/interfaces/ITokenRateUpdatable.sol"; +import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; +import {ERC20RebasableBridged} from "../token/ERC20RebasableBridged.sol"; +import {BridgingManager} from "../BridgingManager.sol"; +import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {DepositDataCodec} from "../lib/DepositDataCodec.sol"; + +/// @author psirex, kovalgek +/// @notice The L2 token bridge works with the L1 token bridge to enable ERC20 token bridging +/// between L1 and L2. It acts as a minter for new tokens when it hears about +/// deposits into the L1 token bridge. It also acts as a burner of the tokens +/// intended for withdrawal, informing the L1 bridge to release L1 funds. Additionally, adds +/// the methods for bridging management: enabling and disabling withdrawals/deposits +contract L2ERC20ExtendedTokensBridge is + IL2ERC20Bridge, + BridgingManager, + RebasableAndNonRebasableTokens, + CrossDomainEnabled +{ + using SafeERC20 for IERC20; + + address private immutable L1_TOKEN_BRIDGE; + + /// @param messenger_ L2 messenger address being used for cross-chain communications + /// @param l1TokenBridge_ Address of the corresponding L1 bridge + /// @param l1TokenNonRebasable_ Address of the bridged token in the L1 chain + /// @param l1TokenRebasable_ Address of the bridged token in the L1 chain + /// @param l2TokenNonRebasable_ Address of the token minted on the L2 chain when token bridged + /// @param l2TokenRebasable_ Address of the token minted on the L2 chain when token bridged + constructor( + address messenger_, + address l1TokenBridge_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens( + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { + L1_TOKEN_BRIDGE = l1TokenBridge_; + } + + /// @inheritdoc IL2ERC20Bridge + function l1TokenBridge() external view returns (address) { + return L1_TOKEN_BRIDGE; + } + + /// @inheritdoc IL2ERC20Bridge + function withdraw( + address l2Token_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) external + whenWithdrawalsEnabled + onlySupportedL2Token(l2Token_) + { + if (Address.isContract(msg.sender)) { + revert ErrorSenderNotEOA(); + } + _withdrawTo(l2Token_, msg.sender, msg.sender, amount_, l1Gas_, data_); + emit WithdrawalInitiated(_l1Token(l2Token_), l2Token_, msg.sender, msg.sender, amount_, data_); + } + + /// @inheritdoc IL2ERC20Bridge + function withdrawTo( + address l2Token_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) external + whenWithdrawalsEnabled + onlySupportedL2Token(l2Token_) + { + _withdrawTo(l2Token_, msg.sender, to_, amount_, l1Gas_, data_); + emit WithdrawalInitiated(_l1Token(l2Token_), l2Token_, msg.sender, to_, amount_, data_); + } + + /// @inheritdoc IL2ERC20Bridge + function finalizeDeposit( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) + external + whenDepositsEnabled() + onlySupportedL1L2TokensPair(l1Token_, l2Token_) + onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) + { + DepositDataCodec.DepositData memory depositData = DepositDataCodec.decodeDepositData(data_); + ITokenRateUpdatable tokenRateOracle = ERC20RebasableBridged(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); + tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); + + uint256 depositedAmount = _mintTokens(l1Token_, l2Token_, to_, amount_); + emit DepositFinalized(l1Token_, l2Token_, from_, to_, depositedAmount, depositData.data); + } + + /// @notice Performs the logic for withdrawals by burning the token and informing + /// the L1 token Gateway of the withdrawal + /// @param l2Token_ Address of L2 token where withdrawal was initiated. + /// @param from_ Account to pull the withdrawal from on L2 + /// @param to_ Account to give the withdrawal to on L1 + /// @param amount_ Amount of the token to withdraw + /// @param l1Gas_ Unused, but included for potential forward compatibility considerations + /// @param data_ Optional data to forward to L1. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content + function _withdrawTo( + address l2Token_, + address from_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) internal { + uint256 amountToWithdraw = _burnTokens(l2Token_, from_, amount_); + + bytes memory message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + _l1Token(l2Token_), l2Token_, from_, to_, amountToWithdraw, data_ + ); + sendCrossDomainMessage(L1_TOKEN_BRIDGE, l1Gas_, message); + } + + /// @dev Mints tokens. + /// @param l1Token_ Address of L1 token for which deposit is finalizing. + /// @param l2Token_ Address of L2 token for which deposit is finalizing. + /// @param to_ Account that token mints for. + /// @param amount_ Amount of token or shares to mint. + /// @return returns amount of minted tokens. + function _mintTokens( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_ + ) internal returns (uint256) { + if(_isRebasable(l1Token_)) { + ERC20RebasableBridged(l2Token_).bridgeMintShares(to_, amount_); + return ERC20RebasableBridged(l2Token_).getTokensByShares(amount_); + } + + IERC20Bridged(l2Token_).bridgeMint(to_, amount_); + return amount_; + } + + /// @dev Burns tokens + /// @param l2Token_ Address of L2 token where withdrawal was initiated. + /// @param from_ Account which tokens are burns. + /// @param amount_ Amount of token to burn. + /// @return returns amount of non-rebasable token to withdraw. + function _burnTokens( + address l2Token_, + address from_, + uint256 amount_ + ) internal returns (uint256) { + if(_isRebasable(l2Token_)) { + uint256 shares = ERC20RebasableBridged(l2Token_).getSharesByTokens(amount_); + ERC20RebasableBridged(l2Token_).bridgeBurnShares(from_, shares); + return shares; + } + + IERC20Bridged(l2Token_).bridgeBurn(from_, amount_); + return amount_; + } + + error ErrorSenderNotEOA(); +} diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol deleted file mode 100644 index 1b1870e0..00000000 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; -import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; -import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; - -import {BridgingManager} from "../BridgingManager.sol"; -import {BridgeableTokens} from "../BridgeableTokens.sol"; -import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; - -/// @author psirex -/// @notice The L2 token bridge works with the L1 token bridge to enable ERC20 token bridging -/// between L1 and L2. It acts as a minter for new tokens when it hears about -/// deposits into the L1 token bridge. It also acts as a burner of the tokens -/// intended for withdrawal, informing the L1 bridge to release L1 funds. Additionally, adds -/// the methods for bridging management: enabling and disabling withdrawals/deposits -contract L2ERC20TokenBridge is - IL2ERC20Bridge, - BridgingManager, - BridgeableTokens, - CrossDomainEnabled -{ - /// @inheritdoc IL2ERC20Bridge - address public immutable l1TokenBridge; - - /// @param messenger_ L2 messenger address being used for cross-chain communications - /// @param l1TokenBridge_ Address of the corresponding L1 bridge - /// @param l1Token_ Address of the bridged token in the L1 chain - /// @param l2Token_ Address of the token minted on the L2 chain when token bridged - constructor( - address messenger_, - address l1TokenBridge_, - address l1Token_, - address l2Token_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l2Token_) { - l1TokenBridge = l1TokenBridge_; - } - - /// @inheritdoc IL2ERC20Bridge - function withdraw( - address l2Token_, - uint256 amount_, - uint32 l1Gas_, - bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _initiateWithdrawal(msg.sender, msg.sender, amount_, l1Gas_, data_); - } - - /// @inheritdoc IL2ERC20Bridge - function withdrawTo( - address l2Token_, - address to_, - uint256 amount_, - uint32 l1Gas_, - bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _initiateWithdrawal(msg.sender, to_, amount_, l1Gas_, data_); - } - - /// @inheritdoc IL2ERC20Bridge - function finalizeDeposit( - address l1Token_, - address l2Token_, - address from_, - address to_, - uint256 amount_, - bytes calldata data_ - ) - external - whenDepositsEnabled - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) - onlyFromCrossDomainAccount(l1TokenBridge) - { - IERC20Bridged(l2Token_).bridgeMint(to_, amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); - } - - /// @notice Performs the logic for withdrawals by burning the token and informing - /// the L1 token Gateway of the withdrawal - /// @param from_ Account to pull the withdrawal from on L2 - /// @param to_ Account to give the withdrawal to on L1 - /// @param amount_ Amount of the token to withdraw - /// @param l1Gas_ Unused, but included for potential forward compatibility considerations - /// @param data_ Optional data to forward to L1. This data is provided - /// solely as a convenience for external contracts. Aside from enforcing a maximum - /// length, these contracts provide no guarantees about its content - function _initiateWithdrawal( - address from_, - address to_, - uint256 amount_, - uint32 l1Gas_, - bytes calldata data_ - ) internal { - IERC20Bridged(l2Token).bridgeBurn(from_, amount_); - - bytes memory message = abi.encodeWithSelector( - IL1ERC20Bridge.finalizeERC20Withdrawal.selector, - l1Token, - l2Token, - from_, - to_, - amount_, - data_ - ); - - sendCrossDomainMessage(l1TokenBridge, l1Gas_, message); - - emit WithdrawalInitiated(l1Token, l2Token, from_, to_, amount_, data_); - } -} diff --git a/contracts/optimism/OpStackTokenRatePusher.sol b/contracts/optimism/OpStackTokenRatePusher.sol new file mode 100644 index 00000000..52479a22 --- /dev/null +++ b/contracts/optimism/OpStackTokenRatePusher.sol @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {ITokenRatePusher} from "../lido/interfaces/ITokenRatePusher.sol"; +import {IERC20WstETH} from "./L1LidoTokensBridge.sol"; +import {ITokenRateUpdatable} from "../optimism/interfaces/ITokenRateUpdatable.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/// @author kovalgek +/// @notice Pushes token rate to L2 Oracle. +contract OpStackTokenRatePusher is CrossDomainEnabled, ERC165, ITokenRatePusher { + + /// @notice Oracle address on L2 for receiving token rate. + address public immutable L2_TOKEN_RATE_ORACLE; + + /// @notice Non-rebasable token of Core Lido procotol. + address public immutable WSTETH; + + /// @notice Gas limit for L2 required to finish pushing token rate on L2 side. + /// Client pays for gas on L2 by burning it on L1. + /// Depends linearly on deposit data length and gas used for finalizing deposit on L2. + /// Formula to find value: + /// (gas cost of L2Bridge.finalizeDeposit() + OptimismPortal.minimumGasLimit(depositData.length)) * 1.5 + uint32 public immutable L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE; + + /// @param messenger_ L1 messenger address being used for cross-chain communications + /// @param wstEth_ Non-rebasable token of Core Lido procotol. + /// @param tokenRateOracle_ Oracle address on L2 for receiving token rate. + /// @param l2GasLimitForPushingTokenRate_ Gas limit required to complete pushing token rate on L2. + constructor( + address messenger_, + address wstEth_, + address tokenRateOracle_, + uint32 l2GasLimitForPushingTokenRate_ + ) CrossDomainEnabled(messenger_) { + WSTETH = wstEth_; + L2_TOKEN_RATE_ORACLE = tokenRateOracle_; + L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE = l2GasLimitForPushingTokenRate_; + } + + /// @inheritdoc ITokenRatePusher + function pushTokenRate() external { + uint256 tokenRate = IERC20WstETH(WSTETH).stEthPerToken(); + + bytes memory message = abi.encodeWithSelector( + ITokenRateUpdatable.updateRate.selector, + tokenRate, + block.timestamp + ); + + sendCrossDomainMessage(L2_TOKEN_RATE_ORACLE, L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE, message); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(ITokenRatePusher).interfaceId + || super.supportsInterface(_interfaceId) + ); + } +} diff --git a/contracts/optimism/README.md b/contracts/optimism/README.md index 598f7b99..6b853ba3 100644 --- a/contracts/optimism/README.md +++ b/contracts/optimism/README.md @@ -41,8 +41,8 @@ A high-level overview of the proposed solution might be found in the below diagr - [**`BridgingManager`**](#BridgingManager) - contains administrative methods to retrieve and control the state of the bridging process. - [**`BridgeableTokens`**](#BridgeableTokens) - contains the logic for validation of tokens used in the bridging process. - [**`CrossDomainEnabled`**](#CrossDomainEnabled) - helper contract for contracts performing cross-domain communications -- [**`L1ERC20TokenBridge`**](#L1ERC20TokenBridge) - Ethereum's counterpart of the bridge to bridge registered ERC20 compatible tokens between Ethereum and Optimism chains. -- [**`L2ERC20TokenBridge`**](#L2ERC20TokenBridge) - Optimism's counterpart of the bridge to bridge registered ERC20 compatible tokens between Ethereum and Optimism chains +- [**`L1ERC20ExtendedTokensBridge`**](#L1ERC20ExtendedTokensBridge) - Ethereum's counterpart of the bridge to bridge registered ERC20 compatible tokens between Ethereum and Optimism chains. +- [**`L2ERC20ExtendedTokensBridge`**](#L2ERC20ExtendedTokensBridge) - Optimism's counterpart of the bridge to bridge registered ERC20 compatible tokens between Ethereum and Optimism chains - [**`ERC20Bridged`**](#ERC20Bridged) - an implementation of the `ERC20` token with administrative methods to mint and burn tokens. - [**`OssifiableProxy`**](#OssifiableProxy) - the ERC1967 proxy with extra admin functionality. @@ -216,7 +216,7 @@ Sends a message to an account on another domain. Enforces that the modified function is only callable by a specific cross-domain account. -## `L1ERC20TokenBridge` +## `L1ERC20ExtendedTokensBridge` **Implements:** [`IL1ERC20Bridge`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/IL1ERC20Bridge.sol) **Inherits:** [`BridgingManager`](#BridgingManager) [`BridgeableTokens`](#BridgeableTokens) [`CrossDomainEnabled`](#CrossDomainEnabled) @@ -300,7 +300,7 @@ Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance Performs the logic for deposits by informing the L2 Deposited Token contract of the deposit and calling safeTransferFrom to lock the L1 funds. -## `L2ERC20TokenBridge` +## `L2ERC20ExtendedTokensBridge` **Implements:** [`IL2ERC20Bridge`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol) **Extends** [`BridgingManager`](#BridgingManager) [`BridgeableTokens`](#BridgeableTokens) [`CrossDomainEnabled`](#CrossDomainEnabled) @@ -512,39 +512,9 @@ Returns a `bool` value indicating whether the operation succeeded. Transfers `amount` of token from the `from_` account to `to_` using the allowance mechanism. `amount_` is then deducted from the caller's allowance. Returns a `bool` value indicating whether the operation succeed. -#### `increaseAllowance(address,uint256)` - -> **Visibility:**     `external` -> -> **Returns**        `(bool)` -> -> **Arguments:** -> -> - **`spender_`** - an address of the tokens spender -> - **`addedValue_`** - a number to increase allowance -> -> **Emits:** `Approval(address indexed owner, address indexed spender, uint256 value)` - -Atomically increases the allowance granted to `spender` by the caller. Returns a `bool` value indicating whether the operation succeed. - -#### `decreaseAllowance(address,uint256)` - -> **Visibility:**     `external` -> -> **Returns**        `(bool)` -> -> **Arguments:** -> -> - **`spender_`** - an address of the tokens spender -> - **`subtractedValue_`** - a number to decrease allowance -> -> **Emits:** `Approval(address indexed owner, address indexed spender, uint256 value)` - -Atomically decreases the allowance granted to `spender` by the caller. Returns a `bool` value indicating whether the operation succeed. - ## `ERC20Bridged` -**Implements:** [`IERC20Bridged`](https://github.com/lidofinance/lido-l2/blob/main/contracts/token/interfaces/IERC20Bridged.sol) +**Implements:** [`IERC20Bridged`](https://github.com/lidofinance/lido-l2/blob/main/contracts/token/ERC20Bridged.sol) **Inherits:** [`ERC20Metadata`](#ERC20Metadata) [`ERC20Core`](#ERC20CoreLogic) Inherits the `ERC20` default functionality that allows the bridge to mint and burn tokens. @@ -691,7 +661,7 @@ Validates that that proxy is not ossified and that method is called by the admin ## Deployment Process -To reduce the gas costs for users, contracts `L1ERC20TokenBridge`, `L2ERC20TokenBridge`, and `ERC20Bridged` contracts use immutable variables as much as possible. But some of those variables are cross-referred. For example, `L1ERC20TokenBridge` has reference to `L2ERC20TokenBridge` and vice versa. As we use proxies, we can deploy proxies at first and stub the implementation with an empty contract. Then deploy actual implementations with addresses of deployed proxies and then upgrade proxies with new implementations. For stub, the following contract might be used: +To reduce the gas costs for users, contracts `L1ERC20ExtendedTokensBridge`, `L2ERC20ExtendedTokensBridge`, and `ERC20Bridged` contracts use immutable variables as much as possible. But some of those variables are cross-referred. For example, `L1ERC20ExtendedTokensBridge` has reference to `L2ERC20ExtendedTokensBridge` and vice versa. As we use proxies, we can deploy proxies at first and stub the implementation with an empty contract. Then deploy actual implementations with addresses of deployed proxies and then upgrade proxies with new implementations. For stub, the following contract might be used: ``` pragma solidity ^0.8.0; @@ -706,7 +676,7 @@ As an additional link in the tokens flow chain, the Optimism protocol and bridge ## Minting of uncollateralized L2 token -Such an attack might happen if an attacker obtains the right to call `L2ERC20TokenBridge.finalizeDeposit()` directly. In such a scenario, an attacker can mint uncollaterized tokens on L2 and initiate withdrawal later. +Such an attack might happen if an attacker obtains the right to call `L2ERC20ExtendedTokensBridge.finalizeDeposit()` directly. In such a scenario, an attacker can mint uncollaterized tokens on L2 and initiate withdrawal later. The best way to detect such an attack is an offchain monitoring of the minting and depositing/withdrawal events. Based on such events might be tracked following stats: diff --git a/contracts/optimism/RebasableAndNonRebasableTokens.sol b/contracts/optimism/RebasableAndNonRebasableTokens.sol new file mode 100644 index 00000000..21491d19 --- /dev/null +++ b/contracts/optimism/RebasableAndNonRebasableTokens.sol @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {UnstructuredRefStorage} from "../token/UnstructuredRefStorage.sol"; + +/// @author psirex, kovalgek +/// @notice Contains the logic for validation of tokens used in the bridging process +contract RebasableAndNonRebasableTokens { + + /// @notice Address of the bridged non rebasable token in the L1 chain + address public immutable L1_TOKEN_NON_REBASABLE; + + /// @notice Address of the bridged rebasable token in the L1 chain + address public immutable L1_TOKEN_REBASABLE; + + /// @notice Address of the non rebasable token minted on the L2 chain when token bridged + address public immutable L2_TOKEN_NON_REBASABLE; + + /// @notice Address of the rebasable token minted on the L2 chain when token bridged + address public immutable L2_TOKEN_REBASABLE; + + /// @param l1TokenNonRebasable_ Address of the bridged non rebasable token in the L1 chain + /// @param l1TokenRebasable_ Address of the bridged rebasable token in the L1 chain + /// @param l2TokenNonRebasable_ Address of the non rebasable token minted on the L2 chain when token bridged + /// @param l2TokenRebasable_ Address of the rebasable token minted on the L2 chain when token bridged + constructor( + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) { + L1_TOKEN_NON_REBASABLE = l1TokenNonRebasable_; + L1_TOKEN_REBASABLE = l1TokenRebasable_; + L2_TOKEN_NON_REBASABLE = l2TokenNonRebasable_; + L2_TOKEN_REBASABLE = l2TokenRebasable_; + } + + /// @dev Validates that passed l1Token_ and l2Token_ tokens pair is supported by the bridge. + modifier onlySupportedL1L2TokensPair(address l1Token_, address l2Token_) { + if (l1Token_ != L1_TOKEN_NON_REBASABLE && l1Token_ != L1_TOKEN_REBASABLE) { + revert ErrorUnsupportedL1Token(); + } + if (l2Token_ != L2_TOKEN_NON_REBASABLE && l2Token_ != L2_TOKEN_REBASABLE) { + revert ErrorUnsupportedL2Token(); + } + if (!_isSupportedL1L2TokensPair(l1Token_, l2Token_)) { + revert ErrorUnsupportedL1L2TokensPair(); + } + _; + } + + function _isSupportedL1L2TokensPair(address l1Token_, address l2Token_) internal view returns (bool) { + bool isNonRebasablePair = l1Token_ == L1_TOKEN_NON_REBASABLE && l2Token_ == L2_TOKEN_NON_REBASABLE; + bool isRebasablePair = l1Token_ == L1_TOKEN_REBASABLE && l2Token_ == L2_TOKEN_REBASABLE; + return isNonRebasablePair || isRebasablePair; + } + + /// @dev Validates that passed l1Token_ is supported by the bridge + modifier onlySupportedL1Token(address l1Token_) { + if (l1Token_ != L1_TOKEN_NON_REBASABLE && l1Token_ != L1_TOKEN_REBASABLE) { + revert ErrorUnsupportedL1Token(); + } + _; + } + + /// @dev Validates that passed l2Token_ is supported by the bridge + modifier onlySupportedL2Token(address l2Token_) { + if (l2Token_ != L2_TOKEN_NON_REBASABLE && l2Token_ != L2_TOKEN_REBASABLE) { + revert ErrorUnsupportedL2Token(); + } + _; + } + + /// @dev validates that account_ is not zero address + modifier onlyNonZeroAccount(address account_) { + if (account_ == address(0)) { + revert ErrorAccountIsZeroAddress(); + } + _; + } + + function _isRebasable(address token_) internal view returns (bool) { + return token_ == L1_TOKEN_REBASABLE || token_ == L2_TOKEN_REBASABLE; + } + + function _l1Token(address l2Token_) internal view returns (address) { + return _isRebasable(l2Token_) ? L1_TOKEN_REBASABLE : L1_TOKEN_NON_REBASABLE; + } + + error ErrorUnsupportedL1Token(); + error ErrorUnsupportedL2Token(); + error ErrorUnsupportedL1L2TokensPair(); + error ErrorAccountIsZeroAddress(); +} diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol new file mode 100644 index 00000000..164807b4 --- /dev/null +++ b/contracts/optimism/TokenRateOracle.sol @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRateUpdatable} from "./interfaces/ITokenRateUpdatable.sol"; +import {IChainlinkAggregatorInterface} from "./interfaces/IChainlinkAggregatorInterface.sol"; +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; + +interface ITokenRateOracle is ITokenRateUpdatable, IChainlinkAggregatorInterface {} + +/// @author kovalgek +/// @notice Oracle for storing token rate. +contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { + + /// @notice A bridge which can update oracle. + address public immutable L2_ERC20_TOKEN_BRIDGE; + + /// @notice An address of account on L1 that can update token rate. + address public immutable L1_TOKEN_RATE_PUSHER; + + /// @notice A time period when token rate can be considered outdated. + uint256 public immutable TOKEN_RATE_OUTDATED_DELAY; + + /// @notice Decimals of the oracle response. + uint8 public constant DECIMALS = 18; + + uint256 public constant MIN_TOKEN_RATE = 1_000_000_000_000_000; // 0.001 + + uint256 public constant MAX_TOKEN_RATE = 1_000_000_000_000_000_000_000; // 1000 + + /// @notice wstETH/stETH token rate. + uint256 public tokenRate; + + /// @notice L1 time when token rate was pushed. + uint256 public rateL1Timestamp; + + /// @param messenger_ L2 messenger address being used for cross-chain communications + /// @param l2ERC20TokenBridge_ the bridge address that has a right to updates oracle. + /// @param l1TokenRatePusher_ An address of account on L1 that can update token rate. + /// @param tokenRateOutdatedDelay_ time period when token rate can be considered outdated. + constructor( + address messenger_, + address l2ERC20TokenBridge_, + address l1TokenRatePusher_, + uint256 tokenRateOutdatedDelay_ + ) CrossDomainEnabled(messenger_) { + L2_ERC20_TOKEN_BRIDGE = l2ERC20TokenBridge_; + L1_TOKEN_RATE_PUSHER = l1TokenRatePusher_; + TOKEN_RATE_OUTDATED_DELAY = tokenRateOutdatedDelay_; + } + + /// @inheritdoc IChainlinkAggregatorInterface + function latestRoundData() external view returns ( + uint80 roundId_, + int256 answer_, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ) { + uint80 roundId = uint80(rateL1Timestamp); + + return ( + roundId, + int256(tokenRate), + rateL1Timestamp, + rateL1Timestamp, + roundId + ); + } + + /// @inheritdoc IChainlinkAggregatorInterface + function latestAnswer() external view returns (int256) { + return int256(tokenRate); + } + + /// @inheritdoc IChainlinkAggregatorInterface + function decimals() external pure returns (uint8) { + return DECIMALS; + } + + /// @inheritdoc ITokenRateUpdatable + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { + + if (_isNoRightsToCall(msg.sender)) { + revert ErrorNoRights(msg.sender); + } + + if (rateL1Timestamp_ < rateL1Timestamp) { + emit NewTokenRateOutdated(tokenRate_, rateL1Timestamp, rateL1Timestamp_); + return; + } + + if (rateL1Timestamp_ > block.timestamp) { + revert ErrorL1TimestampInFuture(tokenRate_, rateL1Timestamp_); + } + + if (tokenRate_ < MIN_TOKEN_RATE || tokenRate_ > MAX_TOKEN_RATE) { + revert ErrorTokenRateIsOutOfRange(tokenRate_, rateL1Timestamp_); + } + + if (tokenRate_ == tokenRate && rateL1Timestamp_ == rateL1Timestamp) { + return; + } + + tokenRate = tokenRate_; + rateL1Timestamp = rateL1Timestamp_; + + emit RateUpdated(tokenRate, rateL1Timestamp); + } + + /// @notice Returns flag that shows that token rate can be considered outdated. + function isLikelyOutdated() external view returns (bool) { + return block.timestamp - rateL1Timestamp > TOKEN_RATE_OUTDATED_DELAY; + } + + function _isNoRightsToCall(address caller_) internal view returns (bool) { + bool isCalledFromL1TokenRatePusher = caller_ == address(MESSENGER) && + MESSENGER.xDomainMessageSender() == L1_TOKEN_RATE_PUSHER; + bool isCalledFromERC20TokenRateBridge = caller_ == L2_ERC20_TOKEN_BRIDGE; + return !isCalledFromL1TokenRatePusher && !isCalledFromERC20TokenRateBridge; + } + + event RateUpdated(uint256 tokenRate_, uint256 rateL1Timestamp_); + event NewTokenRateOutdated(uint256 tokenRate_, uint256 rateL1Timestamp_, uint256 newTateL1Timestamp_); + + error ErrorNoRights(address caller); + error ErrorL1TimestampInFuture(uint256 tokenRate_, uint256 rateL1Timestamp_); + error ErrorTokenRateIsOutOfRange(uint256 tokenRate_, uint256 rateL1Timestamp_); +} diff --git a/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol b/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol new file mode 100644 index 00000000..03332adf --- /dev/null +++ b/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice A subset of chainlink data feed interface for token rate oracle. +interface IChainlinkAggregatorInterface { + /// @notice get the latest token rate data. + /// @return roundId_ is a unique id for each answer. The value is based on timestamp. + /// @return answer_ is wstETH/stETH token rate. It is a chainlink convention to return int256. + /// @return startedAt_ is time when rate was pushed on L1 side. + /// @return updatedAt_ is the same as startedAt_. + /// @return answeredInRound_ is the same as roundId_. + function latestRoundData() + external + view + returns ( + uint80 roundId_, + int256 answer_, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ); + + /// @notice get the lastest token rate. + /// @return wstETH/stETH token rate. It is a chainlink convention to return int256. + function latestAnswer() external view returns (int256); + + /// @notice represents the number of decimals the oracle responses represent. + /// @return decimals of the oracle response. + function decimals() external view returns (uint8); +} diff --git a/contracts/optimism/interfaces/ITokenRateUpdatable.sol b/contracts/optimism/interfaces/ITokenRateUpdatable.sol new file mode 100644 index 00000000..c14461ac --- /dev/null +++ b/contracts/optimism/interfaces/ITokenRateUpdatable.sol @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice An interface for updating token rate of token rate oracle. +interface ITokenRateUpdatable { + /// @notice Updates token rate. + /// @param tokenRate_ wstETH/stETH token rate. + /// @param rateL1Timestamp_ L1 time when rate was pushed on L1 side. + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external; +} diff --git a/contracts/optimism/stubs/CrossDomainMessengerStub.sol b/contracts/optimism/stubs/CrossDomainMessengerStub.sol index f2d30805..d552ab3f 100644 --- a/contracts/optimism/stubs/CrossDomainMessengerStub.sol +++ b/contracts/optimism/stubs/CrossDomainMessengerStub.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.10; import {ICrossDomainMessenger} from "../interfaces/ICrossDomainMessenger.sol"; +/// @dev For testing purposes. contract CrossDomainMessengerStub is ICrossDomainMessenger { address public xDomainMessageSender; uint256 public messageNonce; diff --git a/contracts/stubs/ERC1271PermitSignerMock.sol b/contracts/stubs/ERC1271PermitSignerMock.sol new file mode 100644 index 00000000..56e94718 --- /dev/null +++ b/contracts/stubs/ERC1271PermitSignerMock.sol @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @dev For testing purposes. +contract ERC1271PermitSignerMock { + bytes4 public constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + function sign(bytes32 hash) public view returns (bytes1 v, bytes32 r, bytes32 s) { + v = 0x42; + r = hash; + s = bytes32(bytes20(address(this))); + } + + function isValidSignature(bytes32 hash, bytes memory sig) external view returns (bytes4) { + (bytes1 v, bytes32 r, bytes32 s) = sign(hash); + bytes memory validSig = abi.encodePacked(r, s, v); + return keccak256(sig) == keccak256(validSig) ? ERC1271_MAGIC_VALUE : bytes4(0); + } +} diff --git a/contracts/stubs/ERC20BridgedStub.sol b/contracts/stubs/ERC20BridgedStub.sol index 85e457a9..fd0b33f8 100644 --- a/contracts/stubs/ERC20BridgedStub.sol +++ b/contracts/stubs/ERC20BridgedStub.sol @@ -1,11 +1,12 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; -import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; +import {IERC20Bridged} from "../token/ERC20Bridged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +/// @dev For testing purposes. contract ERC20BridgedStub is IERC20Bridged, ERC20 { address public bridge; diff --git a/contracts/stubs/ERC20WrapperStub.sol b/contracts/stubs/ERC20WrapperStub.sol new file mode 100644 index 00000000..fc9ab194 --- /dev/null +++ b/contracts/stubs/ERC20WrapperStub.sol @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20Bridged} from "../token/ERC20Bridged.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20WstETH} from "../optimism/L1LidoTokensBridge.sol"; +import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; + +/// @dev represents wstETH on L1. For testing purposes. +contract ERC20WrapperStub is IERC20Wrapper, IERC20WstETH, ERC20 { + + IERC20 public stETH; + address public bridge; + uint256 public tokensRate; + + constructor(IERC20 stETH_, string memory name_, string memory symbol_) + ERC20(name_, symbol_) + { + stETH = stETH_; + + tokensRate = 2 * 10 **18; + _mint(msg.sender, 1000000 * 10**18); + } + + function wrap(uint256 _stETHAmount) external returns (uint256) { + require(_stETHAmount > 0, "wstETH: can't wrap zero stETH"); + + uint256 wstETHAmount = (_stETHAmount * (10 ** uint256(decimals()))) / tokensRate; + + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), _stETHAmount); + + return wstETHAmount; + } + + function unwrap(uint256 _wstETHAmount) external returns (uint256) { + require(_wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + + uint256 stETHAmount = (_wstETHAmount * tokensRate) / (10 ** uint256(decimals())); + + _burn(msg.sender, _wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + + return stETHAmount; + } + + function stEthPerToken() external view returns (uint256) { + return tokensRate; + } +} diff --git a/contracts/stubs/EmptyContractStub.sol b/contracts/stubs/EmptyContractStub.sol index 9a334ca1..96e3995c 100644 --- a/contracts/stubs/EmptyContractStub.sol +++ b/contracts/stubs/EmptyContractStub.sol @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; +/// @dev For testing purposes. contract EmptyContractStub { constructor() payable {} } diff --git a/contracts/token/ERC20Bridged.sol b/contracts/token/ERC20Bridged.sol index 574ddd8b..dee94ec0 100644 --- a/contracts/token/ERC20Bridged.sol +++ b/contracts/token/ERC20Bridged.sol @@ -1,14 +1,30 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; -import {IERC20Bridged} from "./interfaces/IERC20Bridged.sol"; - +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC20Core} from "./ERC20Core.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; -/// @author psirex +/// @author psirex, kovalgek +/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens +interface IERC20Bridged is IERC20 { + /// @notice Returns bridge which can mint and burn tokens on L2 + function bridge() external view returns (address); + + /// @notice Creates amount_ tokens and assigns them to account_, increasing the total supply + /// @param account_ An address of the account to mint tokens + /// @param amount_ An amount of tokens to mint + function bridgeMint(address account_, uint256 amount_) external; + + /// @notice Destroys amount_ tokens from account_, reducing the total supply + /// @param account_ An address of the account to burn tokens + /// @param amount_ An amount of tokens to burn + function bridgeBurn(address account_, uint256 amount_) external; +} + +/// @author psirex, kovalgek /// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens contract ERC20Bridged is IERC20Bridged, ERC20Core, ERC20Metadata { /// @inheritdoc IERC20Bridged diff --git a/contracts/token/ERC20BridgedPermit.sol b/contracts/token/ERC20BridgedPermit.sol new file mode 100644 index 00000000..d7eca099 --- /dev/null +++ b/contracts/token/ERC20BridgedPermit.sol @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ERC20Bridged} from "./ERC20Bridged.sol"; +import {PermitExtension} from "./PermitExtension.sol"; + +/// @author kovalgek +contract ERC20BridgedPermit is ERC20Bridged, PermitExtension { + + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param version_ The current major version of the signing domain (aka token version) + /// @param decimals_ The decimals places of the token + /// @param bridge_ The bridge address which allowd to mint/burn tokens + constructor( + string memory name_, + string memory symbol_, + string memory version_, + uint8 decimals_, + address bridge_ + ) + ERC20Bridged(name_, symbol_, decimals_, bridge_) + PermitExtension(name_, version_) + { + } + + /// @inheritdoc PermitExtension + function _permitAccepted(address owner_, address spender_, uint256 amount_) internal override { + _approve(owner_, spender_, amount_); + } +} diff --git a/contracts/token/ERC20Core.sol b/contracts/token/ERC20Core.sol index bf4e67db..354c28bd 100644 --- a/contracts/token/ERC20Core.sol +++ b/contracts/token/ERC20Core.sol @@ -44,38 +44,6 @@ contract ERC20Core is IERC20 { return true; } - /// @notice Atomically increases the allowance granted to spender by the caller. - /// @param spender_ An address of the tokens spender - /// @param addedValue_ An amount to increase the allowance - function increaseAllowance(address spender_, uint256 addedValue_) - external - returns (bool) - { - _approve( - msg.sender, - spender_, - allowance[msg.sender][spender_] + addedValue_ - ); - return true; - } - - /// @notice Atomically decreases the allowance granted to spender by the caller. - /// @param spender_ An address of the tokens spender - /// @param subtractedValue_ An amount to decrease the allowance - function decreaseAllowance(address spender_, uint256 subtractedValue_) - external - returns (bool) - { - uint256 currentAllowance = allowance[msg.sender][spender_]; - if (currentAllowance < subtractedValue_) { - revert ErrorDecreasedAllowanceBelowZero(); - } - unchecked { - _approve(msg.sender, spender_, currentAllowance - subtractedValue_); - } - return true; - } - /// @dev Moves amount_ of tokens from sender_ to recipient_ /// @param from_ An address of the sender of the tokens /// @param to_ An address of the recipient of the tokens diff --git a/contracts/token/ERC20Metadata.sol b/contracts/token/ERC20Metadata.sol index 397b4d0d..e3781f26 100644 --- a/contracts/token/ERC20Metadata.sol +++ b/contracts/token/ERC20Metadata.sol @@ -3,7 +3,18 @@ pragma solidity 0.8.10; -import {IERC20Metadata} from "./interfaces/IERC20Metadata.sol"; +/// @author psirex +/// @notice Interface for the optional metadata functions from the ERC20 standard. +interface IERC20Metadata { + /// @dev Returns the name of the token. + function name() external view returns (string memory); + + /// @dev Returns the symbol of the token. + function symbol() external view returns (string memory); + + /// @dev Returns the decimals places of the token. + function decimals() external view returns (uint8); +} /// @author psirex /// @notice Contains the optional metadata functions from the ERC20 standard diff --git a/contracts/token/ERC20RebasableBridged.sol b/contracts/token/ERC20RebasableBridged.sol new file mode 100644 index 00000000..4de21b66 --- /dev/null +++ b/contracts/token/ERC20RebasableBridged.sol @@ -0,0 +1,373 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Wrapper} from "./interfaces/IERC20Wrapper.sol"; +import {ITokenRateOracle} from "../optimism/TokenRateOracle.sol"; +import {ERC20Metadata} from "./ERC20Metadata.sol"; +import {UnstructuredRefStorage} from "./UnstructuredRefStorage.sol"; +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; + +/// @author kovalgek +/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn shares +interface IERC20BridgedShares is IERC20 { + /// @notice Returns bridge which can mint and burn shares on L2 + function L2_ERC20_TOKEN_BRIDGE() external view returns (address); + + /// @notice Creates amount_ shares and assigns them to account_, increasing the total shares supply + /// @param account_ An address of the account to mint shares + /// @param amount_ An amount of shares to mint + function bridgeMintShares(address account_, uint256 amount_) external; + + /// @notice Destroys amount_ shares from account_, reducing the total shares supply + /// @param account_ An address of the account to burn shares + /// @param amount_ An amount of shares to burn + function bridgeBurnShares(address account_, uint256 amount_) external; +} + +/// @author kovalgek +/// @notice Rebasable token that wraps/unwraps non-rebasable token and allow to mint/burn tokens by bridge. +contract ERC20RebasableBridged is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Metadata { + using SafeERC20 for IERC20; + using UnstructuredRefStorage for bytes32; + using UnstructuredStorage for bytes32; + + /// @inheritdoc IERC20BridgedShares + address public immutable L2_ERC20_TOKEN_BRIDGE; + + /// @notice Contract of non-rebasable token to wrap from. + IERC20 public immutable TOKEN_TO_WRAP_FROM; + + /// @notice Oracle contract used to get token rate for wrapping/unwrapping tokens. + ITokenRateOracle public immutable TOKEN_RATE_ORACLE; + + /// @dev token allowance slot position. + bytes32 internal constant TOKEN_ALLOWANCE_POSITION = keccak256("ERC20RebasableBridged.TOKEN_ALLOWANCE_POSITION"); + + /// @dev user shares slot position. + bytes32 internal constant SHARES_POSITION = keccak256("ERC20RebasableBridged.SHARES_POSITION"); + + /// @dev token shares slot position. + bytes32 internal constant TOTAL_SHARES_POSITION = keccak256("ERC20RebasableBridged.TOTAL_SHARES_POSITION"); + + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param decimals_ The decimals places of the token + /// @param tokenToWrapFrom_ address of the ERC20 token to wrap + /// @param tokenRateOracle_ address of oracle that returns tokens rate + /// @param l2ERC20TokenBridge_ The bridge address which allows to mint/burn tokens + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address tokenToWrapFrom_, + address tokenRateOracle_, + address l2ERC20TokenBridge_ + ) ERC20Metadata(name_, symbol_, decimals_) { + TOKEN_TO_WRAP_FROM = IERC20(tokenToWrapFrom_); + TOKEN_RATE_ORACLE = ITokenRateOracle(tokenRateOracle_); + L2_ERC20_TOKEN_BRIDGE = l2ERC20TokenBridge_; + } + + /// @notice Sets the name and the symbol of the tokens if they both are empty + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + function initialize(string memory name_, string memory symbol_) external { + _setERC20MetadataName(name_); + _setERC20MetadataSymbol(symbol_); + } + + /// @inheritdoc IERC20Wrapper + function wrap(uint256 sharesAmount_) external returns (uint256) { + if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); + + _mintShares(msg.sender, sharesAmount_); + TOKEN_TO_WRAP_FROM.safeTransferFrom(msg.sender, address(this), sharesAmount_); + + return _getTokensByShares(sharesAmount_); + } + + /// @inheritdoc IERC20Wrapper + function unwrap(uint256 tokenAmount_) external returns (uint256) { + if (tokenAmount_ == 0) revert ErrorZeroTokensUnwrap(); + + uint256 sharesAmount = _getSharesByTokens(tokenAmount_); + _burnShares(msg.sender, sharesAmount); + TOKEN_TO_WRAP_FROM.safeTransfer(msg.sender, sharesAmount); + + return sharesAmount; + } + + /// @inheritdoc IERC20BridgedShares + function bridgeMintShares(address account_, uint256 amount_) external onlyBridge { + _mintShares(account_, amount_); + } + + /// @inheritdoc IERC20BridgedShares + function bridgeBurnShares(address account_, uint256 amount_) external onlyBridge { + _burnShares(account_, amount_); + } + + /// @inheritdoc IERC20 + function allowance(address owner, address spender) external view returns (uint256) { + return _getTokenAllowance()[owner][spender]; + } + + /// @inheritdoc IERC20 + function totalSupply() external view returns (uint256) { + return _getTokensByShares(_getTotalShares()); + } + + /// @inheritdoc IERC20 + function balanceOf(address account_) external view returns (uint256) { + return _getTokensByShares(_sharesOf(account_)); + } + + /// @notice Get shares amount of the provided account. + /// @param account_ provided account address. + /// @return amount of shares owned by `_account`. + function sharesOf(address account_) external view returns (uint256) { + return _sharesOf(account_); + } + + /// @return total amount of shares. + function getTotalShares() external view returns (uint256) { + return _getTotalShares(); + } + + /// @notice Get amount of tokens for a given amount of shares. + /// @param sharesAmount_ amount of shares. + /// @return amount of tokens for a given shares amount. + function getTokensByShares(uint256 sharesAmount_) external view returns (uint256) { + return _getTokensByShares(sharesAmount_); + } + + /// @notice Get amount of shares for a given amount of tokens. + /// @param tokenAmount_ provided tokens amount. + /// @return amount of shares for a given tokens amount. + function getSharesByTokens(uint256 tokenAmount_) external view returns (uint256) { + return _getSharesByTokens(tokenAmount_); + } + + /// @inheritdoc IERC20 + function approve(address spender_, uint256 amount_) + external + returns (bool) + { + _approve(msg.sender, spender_, amount_); + return true; + } + + /// @inheritdoc IERC20 + function transfer(address to_, uint256 amount_) external returns (bool) { + _transfer(msg.sender, to_, amount_); + return true; + } + + /// @inheritdoc IERC20 + function transferFrom( + address from_, + address to_, + uint256 amount_ + ) external returns (bool) { + _spendAllowance(from_, msg.sender, amount_); + _transfer(from_, to_, amount_); + return true; + } + + function _getTokenAllowance() internal pure returns (mapping(address => mapping(address => uint256)) storage) { + return TOKEN_ALLOWANCE_POSITION.storageMapAddressMapAddressUint256(); + } + + /// @notice Amount of shares (locked wstETH amount) owned by the holder. + function _getShares() internal pure returns (mapping(address => uint256) storage) { + return SHARES_POSITION.storageMapAddressAddressUint256(); + } + + /// @notice The total amount of shares in existence. + function _getTotalShares() internal view returns (uint256) { + return TOTAL_SHARES_POSITION.getStorageUint256(); + } + + /// @notice Set total amount of shares. + function _setTotalShares(uint256 _newTotalShares) internal { + TOTAL_SHARES_POSITION.setStorageUint256(_newTotalShares); + } + + /// @dev Moves amount_ of tokens from sender_ to recipient_ + /// @param from_ An address of the sender of the tokens + /// @param to_ An address of the recipient of the tokens + /// @param amount_ An amount of tokens to transfer + function _transfer( + address from_, + address to_, + uint256 amount_ + ) internal onlyNonZeroAccount(from_) onlyNonZeroAccount(to_) { + uint256 sharesToTransfer = _getSharesByTokens(amount_); + _transferShares(from_, to_, sharesToTransfer); + _emitTransferEvents(from_, to_, amount_ ,sharesToTransfer); + } + + /// @dev Updates owner_'s allowance for spender_ based on spent amount_. Does not update + /// the allowance amount in case of infinite allowance + /// @param owner_ An address of the account to spend allowance + /// @param spender_ An address of the spender of the tokens + /// @param amount_ An amount of allowance spend + function _spendAllowance( + address owner_, + address spender_, + uint256 amount_ + ) internal { + uint256 currentAllowance = _getTokenAllowance()[owner_][spender_]; + if (currentAllowance == type(uint256).max) { + return; + } + if (amount_ > currentAllowance) { + revert ErrorNotEnoughAllowance(); + } + unchecked { + _approve(owner_, spender_, currentAllowance - amount_); + } + } + + /// @dev Sets amount_ as the allowance of spender_ over the owner_'s tokens + /// @param owner_ An address of the account to set allowance + /// @param spender_ An address of the tokens spender + /// @param amount_ An amount of tokens to allow to spend + function _approve( + address owner_, + address spender_, + uint256 amount_ + ) internal virtual onlyNonZeroAccount(owner_) onlyNonZeroAccount(spender_) { + _getTokenAllowance()[owner_][spender_] = amount_; + emit Approval(owner_, spender_, amount_); + } + + function _sharesOf(address account_) internal view returns (uint256) { + return _getShares()[account_]; + } + + function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { + (uint256 tokensRate, uint256 decimals) = _getTokenRateAndDecimal(); + return (sharesAmount_ * tokensRate) / (10 ** decimals); + } + + function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) { + (uint256 tokensRate, uint256 decimals) = _getTokenRateAndDecimal(); + return (tokenAmount_ * (10 ** decimals)) / tokensRate; + } + + function _getTokenRateAndDecimal() internal view returns (uint256, uint256) { + uint8 rateDecimals = TOKEN_RATE_ORACLE.decimals(); + + if (rateDecimals == uint8(0)) revert ErrorTokenRateDecimalsIsZero(); + + //slither-disable-next-line unused-return + ( + /* roundId_ */, + int256 answer, + /* startedAt_ */, + uint256 updatedAt, + /* answeredInRound_ */ + ) = TOKEN_RATE_ORACLE.latestRoundData(); + + if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); + + return (uint256(answer), uint256(rateDecimals)); + } + + /// @dev Creates amount_ shares and assigns them to account_, increasing the total shares supply + /// @param recipient_ An address of the account to mint shares + /// @param amount_ An amount of shares to mint + function _mintShares( + address recipient_, + uint256 amount_ + ) internal onlyNonZeroAccount(recipient_) { + _setTotalShares(_getTotalShares() + amount_); + _getShares()[recipient_] = _getShares()[recipient_] + amount_; + uint256 tokensAmount = _getTokensByShares(amount_); + _emitTransferEvents(address(0), recipient_, tokensAmount ,amount_); + } + + /// @dev Destroys amount_ shares from account_, reducing the total shares supply. + /// @param account_ An address of the account to mint shares + /// @param amount_ An amount of shares to mint + function _burnShares( + address account_, + uint256 amount_ + ) internal onlyNonZeroAccount(account_) { + uint256 accountShares = _getShares()[account_]; + if (accountShares < amount_) revert ErrorNotEnoughBalance(); + _setTotalShares(_getTotalShares() - amount_); + _getShares()[account_] = accountShares - amount_; + emit Transfer(account_, address(0), amount_); + } + + /// @dev Moves `sharesAmount_` shares from `sender_` to `recipient_`. + /// @param sender_ An address of the account to take shares + /// @param recipient_ An address of the account to transfer shares + /// @param sharesAmount_ An amount of shares to transfer + function _transferShares( + address sender_, + address recipient_, + uint256 sharesAmount_ + ) internal onlyNonZeroAccount(sender_) onlyNonZeroAccount(recipient_) { + + if (recipient_ == address(this)) revert ErrorTrasferToRebasableContract(); + + uint256 currentSenderShares = _getShares()[sender_]; + if (sharesAmount_ > currentSenderShares) revert ErrorNotEnoughBalance(); + + _getShares()[sender_] = currentSenderShares - sharesAmount_; + _getShares()[recipient_] = _getShares()[recipient_] + sharesAmount_; + } + + /// @dev Emits `Transfer` and `TransferShares` events + function _emitTransferEvents( + address _from, + address _to, + uint256 _tokenAmount, + uint256 _sharesAmount + ) internal { + emit Transfer(_from, _to, _tokenAmount); + emit TransferShares(_from, _to, _sharesAmount); + } + + /// @dev validates that account_ is not zero address + modifier onlyNonZeroAccount(address account_) { + if (account_ == address(0)) { + revert ErrorAccountIsZeroAddress(); + } + _; + } + + /// @dev Validates that sender of the transaction is the bridge + modifier onlyBridge() { + if (msg.sender != L2_ERC20_TOKEN_BRIDGE) { + revert ErrorNotBridge(); + } + _; + } + + /// @notice An executed shares transfer from `sender` to `recipient`. + /// @dev emitted in pair with an ERC20-defined `Transfer` event. + event TransferShares( + address indexed from, + address indexed to, + uint256 sharesValue + ); + + error ErrorZeroSharesWrap(); + error ErrorZeroTokensUnwrap(); + error ErrorTokenRateDecimalsIsZero(); + error ErrorWrongOracleUpdateTime(); + error ErrorTrasferToRebasableContract(); + error ErrorNotEnoughBalance(); + error ErrorNotEnoughAllowance(); + error ErrorAccountIsZeroAddress(); + error ErrorDecreasedAllowanceBelowZero(); + error ErrorNotBridge(); +} diff --git a/contracts/token/ERC20RebasableBridgedPermit.sol b/contracts/token/ERC20RebasableBridgedPermit.sol new file mode 100644 index 00000000..6b9be86d --- /dev/null +++ b/contracts/token/ERC20RebasableBridgedPermit.sol @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ERC20RebasableBridged} from "./ERC20RebasableBridged.sol"; +import {PermitExtension} from "./PermitExtension.sol"; + +/// @author kovalgek +contract ERC20RebasableBridgedPermit is ERC20RebasableBridged, PermitExtension { + + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param version_ The current major version of the signing domain (aka token version) + /// @param decimals_ The decimals places of the token + /// @param tokenToWrapFrom_ address of the ERC20 token to wrap + /// @param tokenRateOracle_ address of oracle that returns tokens rate + /// @param bridge_ The bridge address which allowd to mint/burn tokens + constructor( + string memory name_, + string memory symbol_, + string memory version_, + uint8 decimals_, + address tokenToWrapFrom_, + address tokenRateOracle_, + address bridge_ + ) + ERC20RebasableBridged(name_, symbol_, decimals_, tokenToWrapFrom_, tokenRateOracle_, bridge_) + PermitExtension(name_, version_) + { + } + + /// @inheritdoc PermitExtension + function _permitAccepted(address owner_, address spender_, uint256 amount_) internal override { + _approve(owner_, spender_, amount_); + } +} diff --git a/contracts/token/PermitExtension.sol b/contracts/token/PermitExtension.sol new file mode 100644 index 00000000..40cd3617 --- /dev/null +++ b/contracts/token/PermitExtension.sol @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; +import {EIP712} from "@openzeppelin/contracts-v4.9/utils/cryptography/EIP712.sol"; +import {IERC2612} from "@openzeppelin/contracts-v4.9/interfaces/IERC2612.sol"; +import {SignatureChecker} from "../lib/SignatureChecker.sol"; + +abstract contract PermitExtension is IERC2612, EIP712 { + using UnstructuredStorage for bytes32; + + /// @dev Nonces for ERC-2612 (Permit) + mapping(address => uint256) internal noncesByAddress; + + // TODO: outline structured storage used because at least EIP712 uses it + + + /// @dev Typehash constant for ERC-2612 (Permit) + /// + /// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + /// + bytes32 internal constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + /// @param name_ The name of the token + /// @param version_ The current major version of the signing domain (aka token version) + constructor( + string memory name_, + string memory version_ + ) EIP712(name_, version_) + { + } + + /// @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + /// given ``owner``'s signed approval. + /// Emits an {Approval} event. + /// + /// Requirements: + /// + /// - `spender` cannot be the zero address. + /// - `deadline` must be a timestamp in the future. + /// - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + /// over the EIP712-formatted function arguments. + /// - the signature must use ``owner``'s current nonce (see {nonces}). + /// + function permit( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s + ) external { + if (block.timestamp > _deadline) { + revert ErrorDeadlineExpired(); + } + + bytes32 structHash = keccak256( + abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, _useNonce(_owner), _deadline) + ); + + bytes32 hash = _hashTypedDataV4(structHash); + + if (!SignatureChecker.isValidSignature(_owner, hash, _v, _r, _s)) { + revert ErrorInvalidSignature(); + } + + _permitAccepted(_owner, _spender, _value); + } + + + /// @dev Returns the current nonce for `owner`. This value must be + /// included whenever a signature is generated for {permit}. + /// + /// Every successful call to {permit} increases ``owner``'s nonce by one. This + /// prevents a signature from being used multiple times. + /// + function nonces(address owner) external view returns (uint256) { + return noncesByAddress[owner]; + } + + /// @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + + /// @dev "Consume a nonce": return the current value and increment. + function _useNonce(address _owner) internal returns (uint256 current) { + current = noncesByAddress[_owner]; + noncesByAddress[_owner] = current + 1; + } + + /// @dev Override this function in the inherited contract to invoke the approve() function of ERC20. + function _permitAccepted(address owner_, address spender_, uint256 amount_) internal virtual; + + error ErrorInvalidSignature(); + error ErrorDeadlineExpired(); +} diff --git a/contracts/token/UnstructuredRefStorage.sol b/contracts/token/UnstructuredRefStorage.sol new file mode 100644 index 00000000..f4657639 --- /dev/null +++ b/contracts/token/UnstructuredRefStorage.sol @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +library UnstructuredRefStorage { + function storageMapAddressMapAddressUint256(bytes32 _position) internal pure returns ( + mapping(address => mapping(address => uint256)) storage result + ) { + assembly { result.slot := _position } + } + + function storageMapAddressAddressUint256(bytes32 _position) internal pure returns ( + mapping(address => uint256) storage result + ) { + assembly { result.slot := _position } + } +} \ No newline at end of file diff --git a/contracts/token/UnstructuredStorage.sol b/contracts/token/UnstructuredStorage.sol new file mode 100644 index 00000000..058d1ed3 --- /dev/null +++ b/contracts/token/UnstructuredStorage.sol @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +library UnstructuredStorage { + function getStorageBool(bytes32 position) internal view returns (bool data) { + assembly { data := sload(position) } + } + + function getStorageAddress(bytes32 position) internal view returns (address data) { + assembly { data := sload(position) } + } + + function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) { + assembly { data := sload(position) } + } + + function getStorageUint256(bytes32 position) internal view returns (uint256 data) { + assembly { data := sload(position) } + } + + function setStorageBool(bytes32 position, bool data) internal { + assembly { sstore(position, data) } + } + + function setStorageAddress(bytes32 position, address data) internal { + assembly { sstore(position, data) } + } + + function setStorageBytes32(bytes32 position, bytes32 data) internal { + assembly { sstore(position, data) } + } + + function setStorageUint256(bytes32 position, uint256 data) internal { + assembly { sstore(position, data) } + } +} \ No newline at end of file diff --git a/contracts/token/interfaces/IERC20Bridged.sol b/contracts/token/interfaces/IERC20Bridged.sol deleted file mode 100644 index f29633d9..00000000 --- a/contracts/token/interfaces/IERC20Bridged.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -/// @author psirex -/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens -interface IERC20Bridged is IERC20 { - /// @notice Returns bridge which can mint and burn tokens on L2 - function bridge() external view returns (address); - - /// @notice Creates amount_ tokens and assigns them to account_, increasing the total supply - /// @param account_ An address of the account to mint tokens - /// @param amount_ An amount of tokens to mint - function bridgeMint(address account_, uint256 amount_) external; - - /// @notice Destroys amount_ tokens from account_, reducing the total supply - /// @param account_ An address of the account to burn tokens - /// @param amount_ An amount of tokens to burn - function bridgeBurn(address account_, uint256 amount_) external; -} diff --git a/contracts/token/interfaces/IERC20Metadata.sol b/contracts/token/interfaces/IERC20Metadata.sol deleted file mode 100644 index a7c82d00..00000000 --- a/contracts/token/interfaces/IERC20Metadata.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author psirex -/// @notice Interface for the optional metadata functions from the ERC20 standard. -interface IERC20Metadata { - /// @dev Returns the name of the token. - function name() external view returns (string memory); - - /// @dev Returns the symbol of the token. - function symbol() external view returns (string memory); - - /// @dev Returns the decimals places of the token. - function decimals() external view returns (uint8); -} diff --git a/contracts/token/interfaces/IERC20Wrapper.sol b/contracts/token/interfaces/IERC20Wrapper.sol new file mode 100644 index 00000000..c443f3cd --- /dev/null +++ b/contracts/token/interfaces/IERC20Wrapper.sol @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice Extends the ERC20 functionality that allows to wrap/unwrap token. +interface IERC20Wrapper { + + /// @notice Exchanges wrappable token to wrapper one. + /// @param wrappableTokenAmount_ amount of wrappable token to wrap. + /// @return Amount of wrapper token user receives after wrap. + function wrap(uint256 wrappableTokenAmount_) external returns (uint256); + + /// @notice Exchanges wrapper token to wrappable one. + /// @param wrapperTokenAmount_ amount of wrapper token to uwrap in exchange for wrappable. + /// @return Amount of wrappable token user receives after unwrap. + function unwrap(uint256 wrapperTokenAmount_) external returns (uint256); +} diff --git a/package-lock.json b/package-lock.json index cfddd6b4..bf98439e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@arbitrum/sdk": "3.1.6", - "@eth-optimism/sdk": "3.2.0", + "@eth-optimism/sdk": "3.2.3", "@ethersproject/providers": "^5.6.8", "@lidofinance/evm-script-decoder": "^0.2.2", "@openzeppelin/contracts": "4.6.0", @@ -531,10 +531,9 @@ } }, "node_modules/@eth-optimism/sdk": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eth-optimism/sdk/-/sdk-3.2.0.tgz", - "integrity": "sha512-+ZEO/mDWz3WLzaPVHvgOAK4iN723HmI6sLLr2tmO1/RUoCHVfWMUDwuiikrA49cAsdsjMxCV9+0XNZ8btD2JUg==", - "hasInstallScript": true, + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@eth-optimism/sdk/-/sdk-3.2.3.tgz", + "integrity": "sha512-e3XQTbbU+HTzsEv/VIsJpZifK6YZVlzEtF6tj/Vz/VIEDCjZk5JPcnCQOMVcs9ICI4EJyyur+y/+RU7fPa6qtg==", "dependencies": { "@eth-optimism/contracts": "0.6.0", "@eth-optimism/contracts-bedrock": "0.17.1", @@ -542,7 +541,7 @@ "lodash": "^4.17.21", "merkletreejs": "^0.3.11", "rlp": "^2.2.7", - "semver": "^7.5.4" + "semver": "^7.6.0" }, "peerDependencies": { "ethers": "^5" @@ -25229,9 +25228,9 @@ } }, "@eth-optimism/sdk": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eth-optimism/sdk/-/sdk-3.2.0.tgz", - "integrity": "sha512-+ZEO/mDWz3WLzaPVHvgOAK4iN723HmI6sLLr2tmO1/RUoCHVfWMUDwuiikrA49cAsdsjMxCV9+0XNZ8btD2JUg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@eth-optimism/sdk/-/sdk-3.2.3.tgz", + "integrity": "sha512-e3XQTbbU+HTzsEv/VIsJpZifK6YZVlzEtF6tj/Vz/VIEDCjZk5JPcnCQOMVcs9ICI4EJyyur+y/+RU7fPa6qtg==", "requires": { "@eth-optimism/contracts": "0.6.0", "@eth-optimism/contracts-bedrock": "0.17.1", @@ -25239,7 +25238,7 @@ "lodash": "^4.17.21", "merkletreejs": "^0.3.11", "rlp": "^2.2.7", - "semver": "^7.5.4" + "semver": "^7.6.0" }, "dependencies": { "lru-cache": { diff --git a/package.json b/package.json index 820a38ee..166b2002 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "eslint-plugin-prettier": "^3.4.1", "eslint-plugin-promise": "^5.2.0", "ethereum-waffle": "^3.4.4", + "ethereumjs-util": "^7.0.8", "ethers": "^5.6.2", "hardhat": "^2.12.2", "hardhat-gas-reporter": "^1.0.8", @@ -70,10 +71,11 @@ }, "dependencies": { "@arbitrum/sdk": "3.1.6", - "@eth-optimism/sdk": "3.2.0", + "@eth-optimism/sdk": "3.2.3", "@ethersproject/providers": "^5.6.8", "@lidofinance/evm-script-decoder": "^0.2.2", "@openzeppelin/contracts": "4.6.0", + "@openzeppelin/contracts-v4.9": "npm:@openzeppelin/contracts@4.9.6", "chalk": "4.1.2" } } diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 77d3ecb7..57538f49 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -24,13 +24,16 @@ async function main() { const [l1DeployScript, l2DeployScript] = await optimism .deployment(networkName, { logger: console }) .erc20TokenBridgeDeployScript( - deploymentConfig.token, + deploymentConfig.l1Token, + deploymentConfig.l1RebasableToken, + deploymentConfig.l2TokenRateOracle, { deployer: ethDeployer, admins: { proxy: deploymentConfig.l1.proxyAdmin, - bridge: ethDeployer.address, + bridge: ethDeployer.address }, + contractsShift: 0 }, { deployer: optDeployer, @@ -38,6 +41,7 @@ async function main() { proxy: deploymentConfig.l2.proxyAdmin, bridge: optDeployer.address, }, + contractsShift: 0 } ); @@ -55,22 +59,22 @@ async function main() { await l1DeployScript.run(); await l2DeployScript.run(); - const l1ERC20TokenBridgeProxyDeployStepIndex = 1; + const l1ERC20ExtendedTokensBridgeProxyDeployStepIndex = 1; const l1BridgingManagement = new BridgingManagement( - l1DeployScript.getContractAddress(l1ERC20TokenBridgeProxyDeployStepIndex), + l1DeployScript.getContractAddress(l1ERC20ExtendedTokensBridgeProxyDeployStepIndex), ethDeployer, { logger: console } ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 3; + const l2ERC20ExtendedTokensBridgeProxyDeployStepIndex = 5; const l2BridgingManagement = new BridgingManagement( - l2DeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + l2DeployScript.getContractAddress(l2ERC20ExtendedTokensBridgeProxyDeployStepIndex), optDeployer, { logger: console } ); - await l1BridgingManagement.setup(deploymentConfig.l1); - await l2BridgingManagement.setup(deploymentConfig.l2); + await l1BridgingManagement.setup(deploymentConfig.l1); + await l2BridgingManagement.setup(deploymentConfig.l2); } main().catch((error) => { diff --git a/scripts/optimism/deploy-new-impls.ts b/scripts/optimism/deploy-new-impls.ts new file mode 100644 index 00000000..b28ea202 --- /dev/null +++ b/scripts/optimism/deploy-new-impls.ts @@ -0,0 +1,73 @@ +import env from "../../utils/env"; +import prompt from "../../utils/prompt"; +import network from "../../utils/network"; +import deployment from "../../utils/deployment"; + +import deploymentNewImplementations from "../../utils/optimism/deploymentNewImplementations"; + +async function main() { + const networkName = env.network(); + const ethOptNetwork = network.multichain(["eth", "opt"], networkName); + + const [ethDeployer] = ethOptNetwork.getSigners(env.privateKey(), { + forking: env.forking(), + }); + const [, optDeployer] = ethOptNetwork.getSigners( + env.string("OPT_DEPLOYER_PRIVATE_KEY"), + { + forking: env.forking(), + } + ); + + const deploymentConfig = deployment.loadMultiChainDeploymentConfig(); + + const [l1DeployScript, l2DeployScript] = await deploymentNewImplementations( + networkName, + { logger: console } + ) + .deployScript( + { + deployer: ethDeployer, + admins: { + proxy: deploymentConfig.l1.proxyAdmin, + bridge: ethDeployer.address + }, + contractsShift: 0, + tokenProxyAddress: deploymentConfig.l1Token, + tokenRebasableProxyAddress: deploymentConfig.l1RebasableToken, + opStackTokenRatePusherImplAddress: deploymentConfig.l1OpStackTokenRatePusher, + tokenBridgeProxyAddress: deploymentConfig.l1TokenBridge, + }, + { + deployer: optDeployer, + admins: { + proxy: deploymentConfig.l2.proxyAdmin, + bridge: optDeployer.address, + }, + contractsShift: 0, + tokenBridgeProxyAddress: deploymentConfig.l2TokenBridge, + tokenProxyAddress: deploymentConfig.l2Token, + tokenRateOracleProxyAddress: deploymentConfig.l2TokenRateOracle, + tokenRateOracleRateOutdatedDelay: deploymentConfig.tokenRateOutdatedDelay, + } + ); + + await deployment.printMultiChainDeploymentConfig( + "Deploy new implementations: bridges, wstETH, stETH", + ethDeployer, + optDeployer, + deploymentConfig, + l1DeployScript, + l2DeployScript + ); + + await prompt.proceed(); + + await l1DeployScript.run(); + await l2DeployScript.run(); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/optimism/deploy-oracle.ts b/scripts/optimism/deploy-oracle.ts new file mode 100644 index 00000000..08edc448 --- /dev/null +++ b/scripts/optimism/deploy-oracle.ts @@ -0,0 +1,75 @@ +import env from "../../utils/env"; +import prompt from "../../utils/prompt"; +import network from "../../utils/network"; +import optimism from "../../utils/optimism"; +import deployment from "../../utils/deployment"; +import { TokenRateNotifier__factory } from "../../typechain"; + +async function main() { + const networkName = env.network(); + const ethOptNetwork = network.multichain(["eth", "opt"], networkName); + + const [ethDeployer] = ethOptNetwork.getSigners(env.privateKey(), { + forking: env.forking(), + }); + const [, optDeployer] = ethOptNetwork.getSigners( + env.string("OPT_DEPLOYER_PRIVATE_KEY"), + { + forking: env.forking(), + } + ); + + const deploymentConfig = deployment.loadMultiChainDeploymentConfig(); + + const [l1DeployScript, l2DeployScript] = await optimism + .deploymentOracle(networkName, { logger: console }) + .oracleDeployScript( + deploymentConfig.l1Token, + deploymentConfig.l2GasLimitForPushingTokenRate, + deploymentConfig.tokenRateOutdatedDelay, + { + deployer: ethDeployer, + admins: { + proxy: deploymentConfig.l1.proxyAdmin, + bridge: ethDeployer.address, + }, + contractsShift: 0 + }, + { + deployer: optDeployer, + admins: { + proxy: deploymentConfig.l2.proxyAdmin, + bridge: optDeployer.address, + }, + contractsShift: 0 + } + ); + + await deployment.printMultiChainDeploymentConfig( + "Deploy Token Rate Oracle", + ethDeployer, + optDeployer, + deploymentConfig, + l1DeployScript, + l2DeployScript + ); + + await prompt.proceed(); + + await l1DeployScript.run(); + await l2DeployScript.run(); + + /// setup by adding observer + const tokenRateNotifier = TokenRateNotifier__factory.connect( + l1DeployScript.tokenRateNotifierImplAddress, + ethDeployer + ); + await tokenRateNotifier + .connect(ethDeployer) + .addObserver(l1DeployScript.opStackTokenRatePusherImplAddress); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/arbitrum/_launch.test.ts b/test/arbitrum/_launch.test.ts index e5711360..504fdde9 100644 --- a/test/arbitrum/_launch.test.ts +++ b/test/arbitrum/_launch.test.ts @@ -2,7 +2,7 @@ import { assert } from "chai"; import env from "../../utils/env"; import arbitrum from "../../utils/arbitrum"; -import { L1ERC20TokenBridge__factory } from "../../typechain"; +import { L1ERC20ExtendedTokensBridge__factory } from "../../typechain"; import { wei } from "../../utils/wei"; import testing, { scenario } from "../../utils/testing"; import { BridgingManagerRole } from "../../utils/bridging-management"; @@ -71,7 +71,7 @@ async function ctx() { wei.toBigNumber(wei`1 ether`) ); - const l1ERC20TokenGatewayImpl = L1ERC20TokenBridge__factory.connect( + const l1ERC20TokenGatewayImpl = L1ERC20ExtendedTokensBridge__factory.connect( l1ERC20TokenGateway.address, l1DevMultisig ); diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 7c33be93..ee290cb6 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -1,10 +1,11 @@ import { assert } from "chai"; import { ERC20BridgedStub__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, OptimismBridgeExecutor__factory, ERC20Bridged__factory, + ERC20WrapperStub__factory, } from "../../typechain"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; @@ -14,26 +15,27 @@ import { BridgingManagerRole } from "../../utils/bridging-management"; import env from "../../utils/env"; import network from "../../utils/network"; import { getBridgeExecutorParams } from "../../utils/bridge-executor"; +import deploymentAll from "../../utils/optimism/deploymentAllFromScratch"; scenario("Optimism :: Bridge Executor integration test", ctxFactory) .step("Activate L2 bridge", async (ctx) => { - const { l2ERC20TokenBridge, bridgeExecutor, l2CrossDomainMessenger } = + const { l2ERC20ExtendedTokensBridge, bridgeExecutor, l2CrossDomainMessenger } = ctx.l2; assert.isFalse( - await l2ERC20TokenBridge.hasRole( + await l2ERC20ExtendedTokensBridge.hasRole( BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, bridgeExecutor.address ) ); assert.isFalse( - await l2ERC20TokenBridge.hasRole( + await l2ERC20ExtendedTokensBridge.hasRole( BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash, bridgeExecutor.address ) ); - assert.isFalse(await l2ERC20TokenBridge.isDepositsEnabled()); - assert.isFalse(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + assert.isFalse(await l2ERC20ExtendedTokensBridge.isDepositsEnabled()); + assert.isFalse(await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled()); const actionsSetCountBefore = await bridgeExecutor.getActionsSetCount(); @@ -44,7 +46,7 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) 0, 300_000, bridgeExecutor.interface.encodeFunctionData("queue", [ - new Array(4).fill(l2ERC20TokenBridge.address), + new Array(4).fill(l2ERC20ExtendedTokensBridge.address), new Array(4).fill(0), [ "grantRole(bytes32,address)", @@ -54,25 +56,25 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) ], [ "0x" + - l2ERC20TokenBridge.interface + l2ERC20ExtendedTokensBridge.interface .encodeFunctionData("grantRole", [ BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, bridgeExecutor.address, ]) .substring(10), "0x" + - l2ERC20TokenBridge.interface + l2ERC20ExtendedTokensBridge.interface .encodeFunctionData("grantRole", [ BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash, bridgeExecutor.address, ]) .substring(10), "0x" + - l2ERC20TokenBridge.interface + l2ERC20ExtendedTokensBridge.interface .encodeFunctionData("enableDeposits") .substring(10), "0x" + - l2ERC20TokenBridge.interface + l2ERC20ExtendedTokensBridge.interface .encodeFunctionData("enableWithdrawals") .substring(10), ], @@ -89,33 +91,33 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) await bridgeExecutor.execute(actionsSetCountAfter.sub(1), { value: 0 }); assert.isTrue( - await l2ERC20TokenBridge.hasRole( + await l2ERC20ExtendedTokensBridge.hasRole( BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, bridgeExecutor.address ) ); assert.isTrue( - await l2ERC20TokenBridge.hasRole( + await l2ERC20ExtendedTokensBridge.hasRole( BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash, bridgeExecutor.address ) ); - assert.isTrue(await l2ERC20TokenBridge.isDepositsEnabled()); - assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + assert.isTrue(await l2ERC20ExtendedTokensBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled()); }) .step("Change Proxy implementation", async (ctx) => { const { l2Token, l2CrossDomainMessenger, - l2ERC20TokenBridgeProxy, + l2ERC20ExtendedTokensBridgeProxy, bridgeExecutor, } = ctx.l2; const actionsSetCountBefore = await bridgeExecutor.getActionsSetCount(); const proxyImplBefore = - await l2ERC20TokenBridgeProxy.proxy__getImplementation(); + await l2ERC20ExtendedTokensBridgeProxy.proxy__getImplementation(); await l2CrossDomainMessenger.relayMessage( 0, @@ -124,12 +126,12 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) 0, 300_000, bridgeExecutor.interface.encodeFunctionData("queue", [ - [l2ERC20TokenBridgeProxy.address], + [l2ERC20ExtendedTokensBridgeProxy.address], [0], ["proxy__upgradeTo(address)"], [ "0x" + - l2ERC20TokenBridgeProxy.interface + l2ERC20ExtendedTokensBridgeProxy.interface .encodeFunctionData("proxy__upgradeTo", [l2Token.address]) .substring(10), ], @@ -143,7 +145,7 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) await bridgeExecutor.execute(actionsSetCountBefore, { value: 0 }); const proxyImplAfter = - await l2ERC20TokenBridgeProxy.proxy__getImplementation(); + await l2ERC20ExtendedTokensBridgeProxy.proxy__getImplementation(); assert.notEqual(proxyImplBefore, proxyImplAfter); assert.equal(proxyImplAfter, l2Token.address); @@ -152,14 +154,14 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) .step("Change proxy Admin", async (ctx) => { const { l2CrossDomainMessenger, - l2ERC20TokenBridgeProxy, + l2ERC20ExtendedTokensBridgeProxy, bridgeExecutor, accounts: { sender }, } = ctx.l2; const actionsSetCountBefore = await bridgeExecutor.getActionsSetCount(); - const proxyAdminBefore = await l2ERC20TokenBridgeProxy.proxy__getAdmin(); + const proxyAdminBefore = await l2ERC20ExtendedTokensBridgeProxy.proxy__getAdmin(); await l2CrossDomainMessenger.relayMessage( 0, @@ -168,12 +170,12 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) 0, 300_000, bridgeExecutor.interface.encodeFunctionData("queue", [ - [l2ERC20TokenBridgeProxy.address], + [l2ERC20ExtendedTokensBridgeProxy.address], [0], ["proxy__changeAdmin(address)"], [ "0x" + - l2ERC20TokenBridgeProxy.interface + l2ERC20ExtendedTokensBridgeProxy.interface .encodeFunctionData("proxy__changeAdmin", [sender.address]) .substring(10), ], @@ -186,7 +188,7 @@ scenario("Optimism :: Bridge Executor integration test", ctxFactory) assert.equalBN(actionsSetCountBefore.add(1), actionSetCount); await bridgeExecutor.execute(actionsSetCountBefore, { value: 0 }); - const proxyAdminAfter = await l2ERC20TokenBridgeProxy.proxy__getAdmin(); + const proxyAdminAfter = await l2ERC20ExtendedTokensBridgeProxy.proxy__getAdmin(); assert.notEqual(proxyAdminBefore, proxyAdminAfter); assert.equal(proxyAdminAfter, sender.address); @@ -200,7 +202,6 @@ async function ctxFactory() { .multichain(["eth", "opt"], networkName) .getProviders({ forking: true }); - const testingOnDeployedContracts = testing.env.USE_DEPLOYED_CONTRACTS(false); const l1Deployer = testing.accounts.deployer(l1Provider); const l2Deployer = testing.accounts.deployer(l2Provider); @@ -212,7 +213,14 @@ async function ctxFactory() { "TT" ); + const l1TokenRebasable = await new ERC20WrapperStub__factory(l1Deployer).deploy( + l1Token.address, + "Test Token", + "TT" + ); + const optAddresses = optimism.addresses(networkName); + const testingOnDeployedContracts = testing.env.USE_DEPLOYED_CONTRACTS(false); const govBridgeExecutor = testingOnDeployedContracts ? OptimismBridgeExecutor__factory.connect( @@ -229,35 +237,41 @@ async function ctxFactory() { const l1EthGovExecutorAddress = await govBridgeExecutor.getEthereumGovernanceExecutor(); - const [, l2DeployScript] = await optimism - .deployment(networkName) - .erc20TokenBridgeDeployScript( - l1Token.address, - { - deployer: l1Deployer, - admins: { proxy: l1Deployer.address, bridge: l1Deployer.address }, + const [, optDeployScript] = await deploymentAll( + networkName + ).deployAllScript( + l1Token.address, + l1TokenRebasable.address, + { + deployer: l1Deployer, + admins: { + proxy: l1Deployer.address, + bridge: l1Deployer.address }, - { - deployer: l2Deployer, - admins: { - proxy: govBridgeExecutor.address, - bridge: govBridgeExecutor.address, - }, - } - ); + contractsShift: 0 + }, + { + deployer: l2Deployer, + admins: { + proxy: govBridgeExecutor.address, + bridge: govBridgeExecutor.address, + }, + contractsShift: 0 + } + ); - await l2DeployScript.run(); + await optDeployScript.run(); const l2Token = ERC20Bridged__factory.connect( - l2DeployScript.getContractAddress(1), + optDeployScript.tokenProxyAddress, l2Deployer ); - const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( - l2DeployScript.getContractAddress(3), + const l2ERC20ExtendedTokensBridge = L2ERC20ExtendedTokensBridge__factory.connect( + optDeployScript.tokenBridgeProxyAddress, l2Deployer ); - const l2ERC20TokenBridgeProxy = OssifiableProxy__factory.connect( - l2DeployScript.getContractAddress(3), + const l2ERC20ExtendedTokensBridgeProxy = OssifiableProxy__factory.connect( + optDeployScript.tokenBridgeProxyAddress, l2Deployer ); @@ -291,9 +305,9 @@ async function ctxFactory() { l2: { l2Token, bridgeExecutor: govBridgeExecutor.connect(l2Deployer), - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l2CrossDomainMessenger, - l2ERC20TokenBridgeProxy, + l2ERC20ExtendedTokensBridgeProxy, accounts: { sender: testing.accounts.sender(l2Provider), admin: l2Deployer, diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts deleted file mode 100644 index 09aeefa2..00000000 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { assert } from "chai"; -import hre, { ethers } from "hardhat"; -import { - ERC20BridgedStub__factory, - L1ERC20TokenBridge__factory, - L2ERC20TokenBridge__factory, - OssifiableProxy__factory, - EmptyContractStub__factory, -} from "../../typechain"; -import { CrossDomainMessengerStub__factory } from "../../typechain/factories/CrossDomainMessengerStub__factory"; -import testing, { unit } from "../../utils/testing"; -import { wei } from "../../utils/wei"; - -unit("Optimism :: L1ERC20TokenBridge", ctxFactory) - .test("l2TokenBridge()", async (ctx) => { - assert.equal( - await ctx.l1TokenBridge.l2TokenBridge(), - ctx.accounts.l2TokenBridgeEOA.address - ); - }) - - .test("depositERC20() :: deposits disabled", async (ctx) => { - await ctx.l1TokenBridge.disableDeposits(); - - assert.isFalse(await ctx.l1TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - ctx.l1TokenBridge.depositERC20( - ctx.stubs.l1Token.address, - ctx.stubs.l2Token.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("depositsERC20() :: wrong l1Token address", async (ctx) => { - await assert.revertsWith( - ctx.l1TokenBridge.depositERC20( - ctx.accounts.stranger.address, - ctx.stubs.l2Token.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorUnsupportedL1Token()" - ); - }) - - .test("depositsERC20() :: wrong l2Token address", async (ctx) => { - await assert.revertsWith( - ctx.l1TokenBridge.depositERC20( - ctx.stubs.l1Token.address, - ctx.accounts.stranger.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorUnsupportedL2Token()" - ); - }) - - .test("depositERC20() :: not from EOA", async (ctx) => { - await assert.revertsWith( - ctx.l1TokenBridge - .connect(ctx.accounts.emptyContractAsEOA) - .depositERC20( - ctx.stubs.l1Token.address, - ctx.stubs.l2Token.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorSenderNotEOA()" - ); - }) - - .test("depositERC20()", async (ctx) => { - const { - l1TokenBridge, - accounts: { deployer, l2TokenBridgeEOA }, - stubs: { l1Token, l2Token, l1Messenger }, - } = ctx; - - const l2Gas = wei`0.99 wei`; - const amount = wei`1 ether`; - const data = "0xdeadbeaf"; - - await l1Token.approve(l1TokenBridge.address, amount); - - const deployerBalanceBefore = await l1Token.balanceOf(deployer.address); - const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); - - const tx = await l1TokenBridge.depositERC20( - l1Token.address, - l2Token.address, - amount, - l2Gas, - data - ); - - await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ - l1Token.address, - l2Token.address, - deployer.address, - deployer.address, - amount, - data, - ]); - - await assert.emits(l1Messenger, tx, "SentMessage", [ - l2TokenBridgeEOA.address, - l1TokenBridge.address, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( - "finalizeDeposit", - [ - l1Token.address, - l2Token.address, - deployer.address, - deployer.address, - amount, - data, - ] - ), - 1, // message nonce - l2Gas, - ]); - - assert.equalBN( - await l1Token.balanceOf(deployer.address), - deployerBalanceBefore.sub(amount) - ); - - assert.equalBN( - await l1Token.balanceOf(l1TokenBridge.address), - bridgeBalanceBefore.add(amount) - ); - }) - - .test("depositERC20To() :: deposits disabled", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token, l2Token }, - accounts: { recipient }, - } = ctx; - await l1TokenBridge.disableDeposits(); - - assert.isFalse(await l1TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - l1TokenBridge.depositERC20To( - l1Token.address, - l2Token.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("depositsERC20To() :: wrong l1Token address", async (ctx) => { - const { - l1TokenBridge, - stubs: { l2Token }, - accounts: { recipient, stranger }, - } = ctx; - await l1TokenBridge.disableDeposits(); - - assert.isFalse(await l1TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - l1TokenBridge.depositERC20To( - stranger.address, - l2Token.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("depositsERC20To() :: wrong l2Token address", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token }, - accounts: { recipient, stranger }, - } = ctx; - await l1TokenBridge.disableDeposits(); - - assert.isFalse(await l1TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - l1TokenBridge.depositERC20To( - l1Token.address, - stranger.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("depositsERC20To() :: recipient is zero address", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token }, - accounts: { stranger }, - } = ctx; - - await assert.revertsWith( - l1TokenBridge.depositERC20To( - l1Token.address, - stranger.address, - ethers.constants.AddressZero, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorAccountIsZeroAddress()" - ); - }) - - .test("depositERC20To()", async (ctx) => { - const { - l1TokenBridge, - accounts: { deployer, l2TokenBridgeEOA, recipient }, - stubs: { l1Token, l2Token, l1Messenger }, - } = ctx; - - const l2Gas = wei`0.99 wei`; - const amount = wei`1 ether`; - const data = "0x"; - - await l1Token.approve(l1TokenBridge.address, amount); - - const deployerBalanceBefore = await l1Token.balanceOf(deployer.address); - const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); - - const tx = await l1TokenBridge.depositERC20To( - l1Token.address, - l2Token.address, - recipient.address, - amount, - l2Gas, - data - ); - - await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - amount, - data, - ]); - - await assert.emits(l1Messenger, tx, "SentMessage", [ - l2TokenBridgeEOA.address, - l1TokenBridge.address, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( - "finalizeDeposit", - [ - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - amount, - data, - ] - ), - 1, // message nonce - l2Gas, - ]); - - assert.equalBN( - await l1Token.balanceOf(deployer.address), - deployerBalanceBefore.sub(amount) - ); - - assert.equalBN( - await l1Token.balanceOf(l1TokenBridge.address), - bridgeBalanceBefore.add(amount) - ); - }) - - .test( - "finalizeERC20Withdrawal() :: withdrawals are disabled", - async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token, l2Token }, - accounts: { deployer, recipient, l2TokenBridgeEOA }, - } = ctx; - await l1TokenBridge.disableWithdrawals(); - - assert.isFalse(await l1TokenBridge.isWithdrawalsEnabled()); - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorWithdrawalsDisabled()" - ); - } - ) - - .test("finalizeERC20Withdrawal() :: wrong l1Token", async (ctx) => { - const { - l1TokenBridge, - stubs: { l2Token }, - accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, - } = ctx; - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .finalizeERC20Withdrawal( - stranger.address, - l2Token.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL1Token()" - ); - }) - - .test("finalizeERC20Withdrawal() :: wrong l2Token", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token }, - accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, - } = ctx; - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .finalizeERC20Withdrawal( - l1Token.address, - stranger.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL2Token()" - ); - }) - - .test("finalizeERC20Withdrawal() :: unauthorized messenger", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token, l2Token }, - accounts: { deployer, recipient, stranger }, - } = ctx; - - await assert.revertsWith( - l1TokenBridge - .connect(stranger) - .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnauthorizedMessenger()" - ); - }) - - .test( - "finalizeERC20Withdrawal() :: wrong cross domain sender", - async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token, l2Token, l1Messenger }, - accounts: { deployer, recipient, stranger, l1MessengerStubAsEOA }, - } = ctx; - - await l1Messenger.setXDomainMessageSender(stranger.address); - - await assert.revertsWith( - l1TokenBridge - .connect(l1MessengerStubAsEOA) - .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorWrongCrossDomainSender()" - ); - } - ) - - .test("finalizeERC20Withdrawal()", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1Token, l2Token, l1Messenger }, - accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, - } = ctx; - - await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); - - const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); - - const amount = wei`1 ether`; - const data = "0xdeadbeaf"; - - const tx = await l1TokenBridge - .connect(l1MessengerStubAsEOA) - .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - amount, - data - ); - - await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - amount, - data, - ]); - - assert.equalBN(await l1Token.balanceOf(recipient.address), amount); - assert.equalBN( - await l1Token.balanceOf(l1TokenBridge.address), - bridgeBalanceBefore.sub(amount) - ); - }) - - .run(); - -async function ctxFactory() { - const [deployer, l2TokenBridgeEOA, stranger, recipient] = - await hre.ethers.getSigners(); - - const l1MessengerStub = await new CrossDomainMessengerStub__factory( - deployer - ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); - - const l1TokenStub = await new ERC20BridgedStub__factory(deployer).deploy( - "L1 Token", - "L1" - ); - - const l2TokenStub = await new ERC20BridgedStub__factory(deployer).deploy( - "L2 Token", - "L2" - ); - - const emptyContract = await new EmptyContractStub__factory(deployer).deploy({ - value: wei.toBigNumber(wei`1 ether`), - }); - const emptyContractAsEOA = await testing.impersonate(emptyContract.address); - - const l1MessengerStubAsEOA = await testing.impersonate( - l1MessengerStub.address - ); - - const l1TokenBridgeImpl = await new L1ERC20TokenBridge__factory( - deployer - ).deploy( - l1MessengerStub.address, - l2TokenBridgeEOA.address, - l1TokenStub.address, - l2TokenStub.address - ); - - const l1TokenBridgeProxy = await new OssifiableProxy__factory( - deployer - ).deploy( - l1TokenBridgeImpl.address, - deployer.address, - l1TokenBridgeImpl.interface.encodeFunctionData("initialize", [ - deployer.address, - ]) - ); - - const l1TokenBridge = L1ERC20TokenBridge__factory.connect( - l1TokenBridgeProxy.address, - deployer - ); - - await l1TokenStub.transfer(l1TokenBridge.address, wei`100 ether`); - - const roles = await Promise.all([ - l1TokenBridge.DEPOSITS_ENABLER_ROLE(), - l1TokenBridge.DEPOSITS_DISABLER_ROLE(), - l1TokenBridge.WITHDRAWALS_ENABLER_ROLE(), - l1TokenBridge.WITHDRAWALS_DISABLER_ROLE(), - ]); - - for (const role of roles) { - await l1TokenBridge.grantRole(role, deployer.address); - } - - await l1TokenBridge.enableDeposits(); - await l1TokenBridge.enableWithdrawals(); - - return { - accounts: { - deployer, - stranger, - l2TokenBridgeEOA, - emptyContractAsEOA, - recipient, - l1MessengerStubAsEOA, - }, - stubs: { - l1Token: l1TokenStub, - l2Token: l2TokenStub, - l1Messenger: l1MessengerStub, - }, - l1TokenBridge, - }; -} diff --git a/test/optimism/L1LidoTokensBridge.unit.test.ts b/test/optimism/L1LidoTokensBridge.unit.test.ts new file mode 100644 index 00000000..df1aaae0 --- /dev/null +++ b/test/optimism/L1LidoTokensBridge.unit.test.ts @@ -0,0 +1,1082 @@ +import { assert } from "chai"; +import hre, { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + L1LidoTokensBridge__factory, + L2ERC20ExtendedTokensBridge__factory, + OssifiableProxy__factory, + EmptyContractStub__factory, + ERC20WrapperStub +} from "../../typechain"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { CrossDomainMessengerStub__factory } from "../../typechain/factories/CrossDomainMessengerStub__factory"; +import testing, { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; + +unit("Optimism :: L1LidoTokensBridge", ctxFactory) + + .test("initial state", async (ctx) => { + assert.equal(await ctx.l1TokenBridge.l2TokenBridge(), ctx.accounts.l2TokenBridgeEOA.address); + assert.equal(await ctx.l1TokenBridge.MESSENGER(), ctx.accounts.l1MessengerStubAsEOA._address); + assert.equal(await ctx.l1TokenBridge.L1_TOKEN_NON_REBASABLE(), ctx.stubs.l1TokenNonRebasable.address); + assert.equal(await ctx.l1TokenBridge.L1_TOKEN_REBASABLE(), ctx.stubs.l1TokenRebasable.address); + assert.equal(await ctx.l1TokenBridge.L2_TOKEN_NON_REBASABLE(), ctx.stubs.l2TokenNonRebasable.address); + assert.equal(await ctx.l1TokenBridge.L2_TOKEN_REBASABLE(), ctx.stubs.l2TokenRebasable.address); + }) + + .test("depositERC20() :: deposits disabled", async (ctx) => { + await ctx.l1TokenBridge.disableDeposits(); + + assert.isFalse(await ctx.l1TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1TokenNonRebasable.address, + ctx.stubs.l2TokenNonRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1TokenRebasable.address, + ctx.stubs.l2TokenRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("depositERC20() :: wrong l1Token address", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.accounts.stranger.address, + ctx.stubs.l2TokenNonRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.accounts.stranger.address, + ctx.stubs.l2TokenRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("depositERC20() :: wrong l2Token address", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1TokenNonRebasable.address, + ctx.accounts.stranger.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1TokenRebasable.address, + ctx.accounts.stranger.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("depositERC20() :: wrong tokens combination", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1TokenRebasable.address, + ctx.stubs.l2TokenNonRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + await assert.revertsWith( + ctx.l1TokenBridge.depositERC20( + ctx.stubs.l1TokenNonRebasable.address, + ctx.stubs.l2TokenRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + }) + + .test("depositERC20() :: not from EOA", async (ctx) => { + await assert.revertsWith( + ctx.l1TokenBridge + .connect(ctx.accounts.emptyContractAsEOA) + .depositERC20( + ctx.stubs.l1TokenNonRebasable.address, + ctx.stubs.l2TokenNonRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorSenderNotEOA()" + ); + await assert.revertsWith( + ctx.l1TokenBridge + .connect(ctx.accounts.emptyContractAsEOA) + .depositERC20( + ctx.stubs.l1TokenRebasable.address, + ctx.stubs.l2TokenRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorSenderNotEOA()" + ); + }) + + .test("depositERC20() :: non-rebasable token flow", async (ctx) => { + const { + l1TokenBridge, + accounts: { deployer, l2TokenBridgeEOA }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, + } = ctx; + + const l2Gas = wei`0.99 wei`; + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + + await l1TokenNonRebasable.approve(l1TokenBridge.address, amount); + + const deployerBalanceBefore = await l1TokenNonRebasable.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge.depositERC20( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + amount, + l2Gas, + data + ); + + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(ctx.provider, l1TokenNonRebasable); + const dataToReceive = ethers.utils.hexConcat([packedTokenRateAndTimestampData, data]); + + await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + deployer.address, + amount, + dataToReceive, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeDeposit", + [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + deployer.address, + amount, + dataToReceive, + ] + ), + 1, // message nonce + l2Gas, + ]); + + assert.equalBN( + await l1TokenNonRebasable.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.add(amount) + ); + }) + + .test("depositERC20() :: rebasable token flow", async (ctx) => { + const { + l1TokenBridge, + accounts: { deployer, l2TokenBridgeEOA }, + stubs: { l1TokenRebasable, l2TokenRebasable, l1TokenNonRebasable, l1Messenger }, + } = ctx; + + const l2Gas = wei`0.99 wei`; + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + const rate = await l1TokenNonRebasable.stEthPerToken(); + const decimalsStr = await l1TokenNonRebasable.decimals(); + const decimals = BigNumber.from(10).pow(decimalsStr); + + const amountWrapped = (wei.toBigNumber(amount)).mul(BigNumber.from(decimals)).div(rate); + const deployerBalanceBefore = await l1TokenRebasable.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + await l1TokenRebasable.approve(l1TokenBridge.address, amount); + + const tx = await l1TokenBridge.depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + amount, + l2Gas, + data + ); + + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(ctx.provider, l1TokenNonRebasable); + const dataToReceive = ethers.utils.hexConcat([packedTokenRateAndTimestampData, data]); + + await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + deployer.address, + amount, + dataToReceive, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + deployer.address, + amountWrapped, + dataToReceive, + ] + ), + 1, // message nonce + l2Gas, + ]); + + assert.equalBN( + await l1TokenRebasable.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.add(amountWrapped) + ); + }) + + .test("depositERC20To() :: deposits disabled", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, + accounts: { recipient }, + } = ctx; + await l1TokenBridge.disableDeposits(); + + assert.isFalse(await l1TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("depositERC20To() :: wrong l1Token address", async (ctx) => { + const { + l1TokenBridge, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, + accounts: { recipient, stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + stranger.address, + l2TokenNonRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + stranger.address, + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("depositERC20To() :: wrong l2Token address", async (ctx) => { + const { + l1TokenBridge, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, + accounts: { recipient, stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + stranger.address, + l2TokenNonRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + stranger.address, + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("depositERC20To() :: wrong tokens combination", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l1TokenRebasable, l2TokenNonRebasable, l2TokenRebasable }, + accounts: { recipient }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenNonRebasable.address, + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + }) + + .test("depositERC20To() :: recipient is zero address", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable } + } = ctx; + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + ethers.constants.AddressZero, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorAccountIsZeroAddress()" + ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + ethers.constants.AddressZero, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorAccountIsZeroAddress()" + ); + }) + + .test("depositERC20To() :: non-rebasable token flow", async (ctx) => { + const { + l1TokenBridge, + accounts: { deployer, l2TokenBridgeEOA, recipient }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, + } = ctx; + + const l2Gas = wei`0.99 wei`; + const amount = wei`1 ether`; + const data = "0x"; + + await l1TokenNonRebasable.approve(l1TokenBridge.address, amount); + + const deployerBalanceBefore = await l1TokenNonRebasable.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge.depositERC20To( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + amount, + l2Gas, + data + ); + + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(ctx.provider, l1TokenNonRebasable); + const dataToReceive = ethers.utils.hexConcat([packedTokenRateAndTimestampData, data]); + + await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + dataToReceive, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeDeposit", + [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + dataToReceive, + ] + ), + 1, // message nonce + l2Gas, + ]); + + assert.equalBN( + await l1TokenNonRebasable.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.add(amount) + ); + }) + + .test("depositERC20To() :: rebasable token flow", async (ctx) => { + const { + l1TokenBridge, + accounts: { deployer, l2TokenBridgeEOA, recipient }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable, l1Messenger }, + } = ctx; + + const l2Gas = wei`0.99 wei`; + const amount = wei`1 ether`; + const data = "0x"; + + const rate = await l1TokenNonRebasable.stEthPerToken(); + const decimalsStr = await l1TokenNonRebasable.decimals(); + const decimals = BigNumber.from(10).pow(decimalsStr); + + const amountWrapped = (wei.toBigNumber(amount)).mul(BigNumber.from(decimals)).div(rate); + + await l1TokenRebasable.approve(l1TokenBridge.address, amount); + + const deployerBalanceBefore = await l1TokenRebasable.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge.depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + amount, + l2Gas, + data + ); + + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(ctx.provider, l1TokenNonRebasable); + const dataToReceive = ethers.utils.hexConcat([packedTokenRateAndTimestampData, data]); + + await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amount, + dataToReceive, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountWrapped, + dataToReceive, + ] + ), + 1, // message nonce + l2Gas, + ]); + + assert.equalBN( + await l1TokenRebasable.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.add(amountWrapped) + ); + }) + + .test( + "finalizeERC20Withdrawal() :: withdrawals are disabled", + async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, + accounts: { deployer, recipient, l2TokenBridgeEOA }, + } = ctx; + await l1TokenBridge.disableWithdrawals(); + + assert.isFalse(await l1TokenBridge.isWithdrawalsEnabled()); + + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + } + ) + + .test("finalizeERC20Withdrawal() :: wrong l1Token", async (ctx) => { + const { + l1TokenBridge, + stubs: { l2TokenNonRebasable, l2TokenRebasable, l1Messenger }, + accounts: { deployer, recipient, l1MessengerStubAsEOA, stranger, l2TokenBridgeEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + stranger.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + stranger.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("finalizeERC20Withdrawal() :: wrong l2Token", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l1TokenRebasable, l1Messenger }, + accounts: { deployer, recipient, l2TokenBridgeEOA, l1MessengerStubAsEOA, stranger }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("finalizeERC20Withdrawal() :: wrong token combination", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l1TokenRebasable, l2TokenNonRebasable, l2TokenRebasable, l1Messenger }, + accounts: { deployer, recipient, l2TokenBridgeEOA, l1MessengerStubAsEOA }, + } = ctx; + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + }) + + .test("finalizeERC20Withdrawal() :: unauthorized messenger", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, + accounts: { deployer, recipient, stranger }, + } = ctx; + + await assert.revertsWith( + l1TokenBridge + .connect(stranger) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); + await assert.revertsWith( + l1TokenBridge + .connect(stranger) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); + }) + + .test("finalizeERC20Withdrawal() :: wrong cross domain sender", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable, l1Messenger }, + accounts: { deployer, recipient, stranger, l1MessengerStubAsEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(stranger.address); + + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWrongCrossDomainSender()" + ); + await assert.revertsWith( + l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWrongCrossDomainSender()" + ); + }) + + .test("finalizeERC20Withdrawal() :: non-rebasable token flow", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, + accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + assert.equalBN(await l1TokenNonRebasable.balanceOf(recipient.address), amount); + assert.equalBN( + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.sub(amount) + ); + }) + + .test("finalizeERC20Withdrawal() :: rebasable token flow", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenRebasable, l2TokenRebasable, l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, + accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + await l1TokenRebasable.transfer(l1TokenNonRebasable.address, wei`100 ether`); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + const rate = await l1TokenNonRebasable.stEthPerToken(); + const decimalsStr = await l1TokenNonRebasable.decimals(); + const decimals = BigNumber.from(10).pow(decimalsStr); + const amountUnwrapped = (wei.toBigNumber(amount)).mul(rate).div(BigNumber.from(decimals)); + const bridgeBalanceBefore = await l1TokenRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amount, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountUnwrapped, + data, + ]); + + assert.equalBN(await l1TokenRebasable.balanceOf(recipient.address), amountUnwrapped); + assert.equalBN( + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), + bridgeBalanceBefore.sub(amount) + ); + }) + + .test("finalizeERC20Withdrawal() :: zero amount of rebasable token", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenRebasable, l2TokenRebasable, l1Messenger }, + accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + const data = "0xdeadbeaf"; + const recipientBalanceBefore = await l1TokenRebasable.balanceOf(recipient.address); + const bridgeBalanceBefore = await l1TokenRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + 0, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + 0, + data, + ]); + + assert.equalBN(await l1TokenRebasable.balanceOf(recipient.address), recipientBalanceBefore); + assert.equalBN(await l1TokenRebasable.balanceOf(l1TokenBridge.address), bridgeBalanceBefore); + }) + + .test("finalizeERC20Withdrawal() :: zero amount of non-rebasable token", async (ctx) => { + const { + l1TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, + accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, + } = ctx; + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + const data = "0xdeadbeaf"; + const recipientBalanceBefore = await l1TokenNonRebasable.balanceOf(recipient.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const tx = await l1TokenBridge + .connect(l1MessengerStubAsEOA) + .finalizeERC20Withdrawal( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + 0, + data + ); + + await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + 0, + data, + ]); + + assert.equalBN(await l1TokenNonRebasable.balanceOf(recipient.address), recipientBalanceBefore); + assert.equalBN(await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), bridgeBalanceBefore); + }) + + .run(); + +async function ctxFactory() { + const [deployer, l2TokenBridgeEOA, stranger, recipient] = + await hre.ethers.getSigners(); + + const provider = await hre.ethers.provider; + + const l1MessengerStub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + + const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token Rebasable", + "L1R" + ); + + const l1TokenNonRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( + l1TokenRebasableStub.address, + "L1 Token Non Rebasable", + "L1NR" + ); + + const l2TokenNonRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L2 Token Non Rebasable", + "L2NR" + ); + + const l2TokenRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( + l2TokenNonRebasableStub.address, + "L2 Token Rebasable", + "L2R" + ); + + const emptyContract = await new EmptyContractStub__factory(deployer).deploy({ + value: wei.toBigNumber(wei`1 ether`), + }); + const emptyContractAsEOA = await testing.impersonate(emptyContract.address); + + const l1MessengerStubAsEOA = await testing.impersonate( + l1MessengerStub.address + ); + + const l1TokenBridgeImpl = await new L1LidoTokensBridge__factory( + deployer + ).deploy( + l1MessengerStub.address, + l2TokenBridgeEOA.address, + l1TokenNonRebasableStub.address, + l1TokenRebasableStub.address, + l2TokenNonRebasableStub.address, + l2TokenRebasableStub.address + ); + + const l1TokenBridgeProxy = await new OssifiableProxy__factory( + deployer + ).deploy( + l1TokenBridgeImpl.address, + deployer.address, + l1TokenBridgeImpl.interface.encodeFunctionData("initialize", [ + deployer.address + ]) + ); + + const l1TokenBridge = L1LidoTokensBridge__factory.connect( + l1TokenBridgeProxy.address, + deployer + ); + + await l1TokenNonRebasableStub.transfer(l1TokenBridge.address, wei`100 ether`); + await l1TokenRebasableStub.transfer(l1TokenBridge.address, wei`100 ether`); + + const roles = await Promise.all([ + l1TokenBridge.DEPOSITS_ENABLER_ROLE(), + l1TokenBridge.DEPOSITS_DISABLER_ROLE(), + l1TokenBridge.WITHDRAWALS_ENABLER_ROLE(), + l1TokenBridge.WITHDRAWALS_DISABLER_ROLE(), + ]); + + for (const role of roles) { + await l1TokenBridge.grantRole(role, deployer.address); + } + + await l1TokenBridge.enableDeposits(); + await l1TokenBridge.enableWithdrawals(); + + return { + provider: provider, + accounts: { + deployer, + stranger, + l2TokenBridgeEOA, + emptyContractAsEOA, + recipient, + l1MessengerStubAsEOA, + }, + stubs: { + l1TokenNonRebasable: l1TokenNonRebasableStub, + l1TokenRebasable: l1TokenRebasableStub, + l2TokenNonRebasable: l2TokenNonRebasableStub, + l2TokenRebasable: l2TokenRebasableStub, + l1Messenger: l1MessengerStub, + }, + l1TokenBridge, + }; +} + +async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapperStub) { + const stEthPerToken = await l1Token.stEthPerToken(); + const blockNumber = await l1Provider.getBlockNumber(); + const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + const stEthPerTokenStr = ethers.utils.hexZeroPad(stEthPerToken.toHexString(), 12); + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); +} diff --git a/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts new file mode 100644 index 00000000..ed800439 --- /dev/null +++ b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts @@ -0,0 +1,1132 @@ +import hre, { ethers } from "hardhat"; +import { + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + TokenRateOracle__factory, + ERC20RebasableBridged__factory, + L1LidoTokensBridge__factory, + L2ERC20ExtendedTokensBridge__factory, + OssifiableProxy__factory, + EmptyContractStub__factory, + CrossDomainMessengerStub__factory, + L2ERC20ExtendedTokensBridge +} from "../../typechain"; +import testing, { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; +import { assert } from "chai"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { getContractAddress } from "ethers/lib/utils"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { BigNumber } from "ethers"; + +unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) + .test("initial state", async (ctx) => { + assert.equal(await ctx.l2TokenBridge.l1TokenBridge(), ctx.accounts.l1TokenBridgeEOA.address); + assert.equal(await ctx.l2TokenBridge.MESSENGER(), ctx.accounts.l2MessengerStubEOA._address); + assert.equal(await ctx.l2TokenBridge.L1_TOKEN_NON_REBASABLE(), ctx.stubs.l1TokenNonRebasable.address); + assert.equal(await ctx.l2TokenBridge.L1_TOKEN_REBASABLE(), ctx.stubs.l1TokenRebasable.address); + assert.equal(await ctx.l2TokenBridge.L2_TOKEN_NON_REBASABLE(), ctx.stubs.l2TokenNonRebasable.address); + assert.equal(await ctx.l2TokenBridge.L2_TOKEN_REBASABLE(), ctx.stubs.l2TokenRebasable.address); + }) + + .test("withdraw() :: withdrawals disabled", async (ctx) => { + const { + l2TokenBridge, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, + } = ctx; + + await ctx.l2TokenBridge.disableWithdrawals(); + + assert.isFalse(await ctx.l2TokenBridge.isWithdrawalsEnabled()); + + await assert.revertsWith( + l2TokenBridge.withdraw( + l2TokenNonRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + + await assert.revertsWith( + l2TokenBridge.withdraw( + l2TokenRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + }) + + .test("withdraw() :: unsupported l2Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { stranger }, + } = ctx; + await assert.revertsWith( + l2TokenBridge.withdraw(stranger.address, wei`1 ether`, wei`1 gwei`, "0x"), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("withdraw() :: not from EOA", async (ctx) => { + const { + l2TokenBridge, + accounts: { emptyContractEOA }, + stubs: { l2TokenRebasable, l2TokenNonRebasable }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(emptyContractEOA) + .withdraw( + l2TokenNonRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorSenderNotEOA()" + ); + await assert.revertsWith( + l2TokenBridge + .connect(emptyContractEOA) + .withdraw( + l2TokenRebasable.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorSenderNotEOA()" + ); + }) + + .test("withdraw() :: non-rebasable token flow", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA }, + stubs: { + l2Messenger, + l1TokenNonRebasable, + l2TokenNonRebasable, + }, + } = ctx; + + const deployerBalanceBefore = await l2TokenNonRebasable.balanceOf(deployer.address); + const totalSupplyBefore = await l2TokenNonRebasable.totalSupply(); + + const amount = wei`1 ether`; + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + + const tx = await l2TokenBridge.withdraw( + l2TokenNonRebasable.address, + amount, + l1Gas, + data + ); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + deployer.address, + amount, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + deployer.address, + amount, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN( + await l2TokenNonRebasable.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l2TokenNonRebasable.totalSupply(), + totalSupplyBefore.sub(amount) + ); + }) + + .test("withdraw() :: rebasable token flow", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA, l2MessengerStubEOA, recipient }, + stubs: { + l2Messenger, + l1TokenRebasable, + l2TokenRebasable + }, + } = ctx; + + const amountToDeposit = wei`1 ether`; + const amountToWithdraw = wei.toBigNumber(amountToDeposit).mul(ctx.exchangeRate).div(ctx.decimalsBN); + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + const provider = await hre.ethers.provider; + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(provider, ctx.exchangeRate); + + const tx1 = await l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountToDeposit, + packedTokenRateAndTimestampData + ); + + const recipientBalanceBefore = await l2TokenRebasable.balanceOf(recipient.address); + const totalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2TokenBridge.connect(recipient).withdraw( + l2TokenRebasable.address, + amountToWithdraw, + l1Gas, + data + ); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + recipient.address, + amountToWithdraw, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + recipient.address, + amountToDeposit, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(deployer.address), + recipientBalanceBefore.sub(amountToWithdraw) + ); + + assert.equalBN( + await l2TokenRebasable.totalSupply(), + totalSupplyBefore.sub(amountToWithdraw) + ); + }) + + .test("withdraw() :: zero rebasable tokens", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA, recipient }, + stubs: { + l2Messenger, + l1TokenRebasable, + l2TokenRebasable + }, + } = ctx; + + await pushTokenRate(ctx); + + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + const recipientBalanceBefore = await l2TokenRebasable.balanceOf(recipient.address); + const totalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2TokenBridge + .connect(recipient) + .withdraw( + l2TokenRebasable.address, + 0, + l1Gas, + data); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN(await l2TokenRebasable.balanceOf(deployer.address), recipientBalanceBefore); + assert.equalBN(await l2TokenRebasable.totalSupply(), totalSupplyBefore); + }) + + .test("withdraw() :: zero non-rebasable tokens", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA, recipient }, + stubs: { + l2Messenger, + l1TokenNonRebasable, + l2TokenNonRebasable + }, + } = ctx; + + await pushTokenRate(ctx); + + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + const recipientBalanceBefore = await l2TokenNonRebasable.balanceOf(recipient.address); + const totalSupplyBefore = await l2TokenNonRebasable.totalSupply(); + + const tx = await l2TokenBridge + .connect(recipient) + .withdraw( + l2TokenNonRebasable.address, + 0, + l1Gas, + data); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN(await l2TokenNonRebasable.balanceOf(recipient.address), recipientBalanceBefore); + assert.equalBN(await l2TokenNonRebasable.totalSupply(), totalSupplyBefore); + }) + + .test("withdrawTo() :: withdrawals disabled", async (ctx) => { + const { + l2TokenBridge, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, + accounts: { recipient }, + } = ctx; + + await ctx.l2TokenBridge.disableWithdrawals(); + + assert.isFalse(await ctx.l2TokenBridge.isWithdrawalsEnabled()); + + await assert.revertsWith( + l2TokenBridge.withdrawTo( + l2TokenNonRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + await assert.revertsWith( + l2TokenBridge.withdrawTo( + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorWithdrawalsDisabled()" + ); + }) + + .test("withdrawTo() :: unsupported l2Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { stranger, recipient }, + } = ctx; + await assert.revertsWith( + l2TokenBridge.withdrawTo( + stranger.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("withdrawTo() :: non rebasable token flow", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, recipient, l1TokenBridgeEOA }, + stubs: { + l2Messenger: l2MessengerStub, + l1TokenNonRebasable, + l2TokenNonRebasable + }, + } = ctx; + + const deployerBalanceBefore = await l2TokenNonRebasable.balanceOf(deployer.address); + const totalSupplyBefore = await l2TokenNonRebasable.totalSupply(); + + const amount = wei`1 ether`; + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + + const tx = await l2TokenBridge.withdrawTo( + l2TokenNonRebasable.address, + recipient.address, + amount, + l1Gas, + data + ); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + await assert.emits(l2MessengerStub, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN( + await l2TokenNonRebasable.balanceOf(deployer.address), + deployerBalanceBefore.sub(amount) + ); + + assert.equalBN( + await l2TokenNonRebasable.totalSupply(), + totalSupplyBefore.sub(amount) + ); + }) + + .test("withdrawTo() :: rebasable token flow", async (ctx) => { + + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA, l2MessengerStubEOA, recipient }, + stubs: { + l2Messenger, + l1TokenNonRebasable, + l2TokenNonRebasable, + l1TokenRebasable, + l2TokenRebasable + }, + } = ctx; + + const amountToDeposit = wei`1 ether`; + const amountToWithdraw = wei.toBigNumber(amountToDeposit).mul(ctx.exchangeRate).div(ctx.decimalsBN); + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + const provider = await hre.ethers.provider; + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(provider, ctx.exchangeRate); + + const tx1 = await l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + deployer.address, + amountToDeposit, + packedTokenRateAndTimestampData + ); + + const deployerBalanceBefore = await l2TokenRebasable.balanceOf(deployer.address); + const totalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2TokenBridge.connect(deployer).withdrawTo( + l2TokenRebasable.address, + recipient.address, + amountToWithdraw, + l1Gas, + data + ); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountToWithdraw, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountToDeposit, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(recipient.address), + deployerBalanceBefore.sub(amountToWithdraw) + ); + + assert.equalBN( + await l2TokenRebasable.totalSupply(), + totalSupplyBefore.sub(amountToWithdraw) + ); + }) + + .test("withdrawTo() :: zero rebasable tokens", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA, recipient }, + stubs: { + l2Messenger, + l1TokenRebasable, + l2TokenRebasable + }, + } = ctx; + + await pushTokenRate(ctx); + + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + const recipientBalanceBefore = await l2TokenRebasable.balanceOf(recipient.address); + const totalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2TokenBridge + .connect(recipient) + .withdrawTo( + l2TokenRebasable.address, + recipient.address, + 0, + l1Gas, + data); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN(await l2TokenRebasable.balanceOf(deployer.address), recipientBalanceBefore); + assert.equalBN(await l2TokenRebasable.totalSupply(), totalSupplyBefore); + }) + + .test("withdrawTo() :: zero non-rebasable tokens", async (ctx) => { + const { + l2TokenBridge, + accounts: { deployer, l1TokenBridgeEOA, recipient }, + stubs: { + l2Messenger, + l1TokenNonRebasable, + l2TokenNonRebasable + }, + } = ctx; + + await pushTokenRate(ctx); + + const l1Gas = wei`1 wei`; + const data = "0xdeadbeaf"; + const recipientBalanceBefore = await l2TokenNonRebasable.balanceOf(recipient.address); + const totalSupplyBefore = await l2TokenNonRebasable.totalSupply(); + + const tx = await l2TokenBridge + .connect(recipient) + .withdrawTo( + l2TokenNonRebasable.address, + recipient.address, + 0, + l1Gas, + data); + + await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ]); + + await assert.emits(l2Messenger, tx, "SentMessage", [ + l1TokenBridgeEOA.address, + l2TokenBridge.address, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + recipient.address, + recipient.address, + 0, + data, + ] + ), + 1, // message nonce + l1Gas, + ]); + + assert.equalBN(await l2TokenNonRebasable.balanceOf(recipient.address), recipientBalanceBefore); + assert.equalBN(await l2TokenNonRebasable.totalSupply(), totalSupplyBefore); + }) + + .test("finalizeDeposit() :: deposits disabled", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, + } = ctx; + + await l2TokenBridge.disableDeposits(); + + assert.isFalse(await l2TokenBridge.isDepositsEnabled()); + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorDepositsDisabled()" + ); + }) + + .test("finalizeDeposit() :: unsupported l1Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + stranger.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + stranger.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); + }) + + .test("finalizeDeposit() :: unsupported l2Token", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, + stubs: { l1TokenNonRebasable, l1TokenRebasable }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenNonRebasable.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); + }) + + .test("finalizeDeposit() :: unsupported tokens combination", async (ctx) => { + const { + l2TokenBridge, + accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, + stubs: { l1TokenNonRebasable, l1TokenRebasable, l2TokenNonRebasable, l2TokenRebasable }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenNonRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1L2TokensPair()" + ); + }) + + .test("finalizeDeposit() :: unauthorized messenger", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, + accounts: { deployer, recipient, stranger }, + } = ctx; + + await assert.revertsWith( + l2TokenBridge + .connect(stranger) + .finalizeDeposit( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); + await assert.revertsWith( + l2TokenBridge + .connect(stranger) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); + }) + + .test("finalizeDeposit() :: wrong cross domain sender", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable, l2Messenger }, + accounts: { deployer, recipient, stranger, l2MessengerStubEOA }, + } = ctx; + + await l2Messenger.setXDomainMessageSender(stranger.address); + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWrongCrossDomainSender()" + ); + + await assert.revertsWith( + l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorWrongCrossDomainSender()" + ); + }) + + .test("finalizeDeposit() :: non-rebasable token flow", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l2Messenger }, + accounts: { deployer, recipient, l2MessengerStubEOA, l1TokenBridgeEOA }, + } = ctx; + + await l2Messenger.setXDomainMessageSender(l1TokenBridgeEOA.address); + + const totalSupplyBefore = await l2TokenNonRebasable.totalSupply(); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + const provider = await hre.ethers.provider; + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(provider, ctx.exchangeRate); + const dataToReceive = ethers.utils.hexConcat([packedTokenRateAndTimestampData, data]); + + const tx = await l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + dataToReceive + ); + + await assert.emits(l2TokenBridge, tx, "DepositFinalized", [ + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + data, + ]); + + assert.equalBN(await l2TokenNonRebasable.balanceOf(recipient.address), amount); + assert.equalBN(await l2TokenNonRebasable.totalSupply(), totalSupplyBefore.add(amount)); + }) + + .test("finalizeDeposit() :: rebasable token flow", async (ctx) => { + const { + l2TokenBridge, + stubs: { l1TokenRebasable, l2TokenRebasable, l2Messenger }, + accounts: { deployer, recipient, l2MessengerStubEOA, l1TokenBridgeEOA }, + } = ctx; + + await l2Messenger.setXDomainMessageSender(l1TokenBridgeEOA.address); + + const amountToDeposit = wei`1 ether`; + const amountToEmit = wei.toBigNumber(amountToDeposit).mul(ctx.exchangeRate).div(ctx.decimalsBN); + const data = "0xdeadbeaf"; + const provider = await hre.ethers.provider; + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(provider, ctx.exchangeRate); + const dataToReceive = ethers.utils.hexConcat([packedTokenRateAndTimestampData, data]); + + const tx = await l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountToDeposit, + dataToReceive + ); + + await assert.emits(l2TokenBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + amountToEmit, + data, + ]); + + assert.equalBN(await l2TokenRebasable.balanceOf(recipient.address), amountToEmit); + }) + + .run(); + +async function ctxFactory() { + const [deployer, stranger, recipient, l1TokenBridgeEOA] = + await hre.ethers.getSigners(); + + const decimals = 18; + const decimalsBN = BigNumber.from(10).pow(decimals); + const exchangeRate = BigNumber.from('12').pow(decimals - 1); + + const l2MessengerStub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + const l2MessengerStubEOA = await testing.impersonate(l2MessengerStub.address); + await l2MessengerStub.setXDomainMessageSender(l1TokenBridgeEOA.address); + + const emptyContract = await new EmptyContractStub__factory(deployer).deploy({ + value: wei.toBigNumber(wei`1 ether`), + }); + const emptyContractEOA = await testing.impersonate(emptyContract.address); + + const [ + , + , + , + , + , + , + l2TokenBridgeProxyAddress + ] = await predictAddresses(deployer, 7); + + const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token Rebasable", + "L1R" + ); + + const l1TokenNonRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( + l1TokenRebasableStub.address, + "L1 Token Non Rebasable", + "L1NR" + ); + + const l2TokenNonRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L2 Token Non Rebasable", + "L2NR" + ); + + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + l2MessengerStub.address, + l2TokenBridgeProxyAddress, + l1TokenBridgeEOA.address, + 86400 + ); + + const l2TokenRebasableStub = await new ERC20RebasableBridged__factory(deployer).deploy( + "L2 Token Rebasable", + "L2R", + decimals, + l2TokenNonRebasableStub.address, + tokenRateOracle.address, + l2TokenBridgeProxyAddress + ); + + const l2TokenBridgeImpl = await new L2ERC20ExtendedTokensBridge__factory( + deployer + ).deploy( + l2MessengerStub.address, + l1TokenBridgeEOA.address, + l1TokenNonRebasableStub.address, + l1TokenRebasableStub.address, + l2TokenNonRebasableStub.address, + l2TokenRebasableStub.address + ); + + const l2TokenBridgeProxy = await new OssifiableProxy__factory( + deployer + ).deploy( + l2TokenBridgeImpl.address, + deployer.address, + l2TokenBridgeImpl.interface.encodeFunctionData("initialize", [ + deployer.address, + ]) + ); + + const l2TokenBridge = L2ERC20ExtendedTokensBridge__factory.connect( + l2TokenBridgeProxy.address, + deployer + ); + + const roles = await Promise.all([ + l2TokenBridge.DEPOSITS_ENABLER_ROLE(), + l2TokenBridge.DEPOSITS_DISABLER_ROLE(), + l2TokenBridge.WITHDRAWALS_ENABLER_ROLE(), + l2TokenBridge.WITHDRAWALS_DISABLER_ROLE(), + ]); + + for (const role of roles) { + await l2TokenBridge.grantRole(role, deployer.address); + } + + await l2TokenBridge.enableDeposits(); + await l2TokenBridge.enableWithdrawals(); + + return { + stubs: { + l1TokenNonRebasable: l1TokenNonRebasableStub, + l1TokenRebasable: l1TokenRebasableStub, + l2TokenNonRebasable: l2TokenNonRebasableStub, + l2TokenRebasable: l2TokenRebasableStub, + l2Messenger: l2MessengerStub, + }, + accounts: { + deployer, + stranger, + recipient, + l2MessengerStubEOA, + emptyContractEOA, + l1TokenBridgeEOA, + }, + l2TokenBridge, + exchangeRate, + decimalsBN + }; +} + +async function predictAddresses(account: SignerWithAddress, txsCount: number) { + const currentNonce = await account.getTransactionCount(); + + const res: string[] = []; + for (let i = 0; i < txsCount; ++i) { + res.push( + getContractAddress({ + from: account.address, + nonce: currentNonce + i, + }) + ); + } + return res; +} + +async function packedTokenRateAndTimestamp(provider: JsonRpcProvider, tokenRate: BigNumber) { + const blockNumber = await provider.getBlockNumber(); + const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; + const stEthPerTokenStr = ethers.utils.hexZeroPad(tokenRate.toHexString(), 12); + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); +} + +type ContextType = Awaited> + +async function pushTokenRate(ctx: ContextType) { + const provider = await hre.ethers.provider; + + const packedTokenRateAndTimestampData = await packedTokenRateAndTimestamp(provider, ctx.exchangeRate); + + await ctx.l2TokenBridge + .connect(ctx.accounts.l2MessengerStubEOA) + .finalizeDeposit( + ctx.stubs.l1TokenRebasable.address, + ctx.stubs.l2TokenRebasable.address, + ctx.accounts.deployer.address, + ctx.accounts.deployer.address, + 0, + packedTokenRateAndTimestampData + ); +} diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts deleted file mode 100644 index 97e40afa..00000000 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -import hre from "hardhat"; -import { - ERC20BridgedStub__factory, - L1ERC20TokenBridge__factory, - L2ERC20TokenBridge__factory, - OssifiableProxy__factory, - EmptyContractStub__factory, - CrossDomainMessengerStub__factory, -} from "../../typechain"; -import testing, { unit } from "../../utils/testing"; -import { wei } from "../../utils/wei"; -import { assert } from "chai"; - -unit("Optimism:: L2ERC20TokenBridge", ctxFactory) - .test("l1TokenBridge()", async (ctx) => { - assert.equal( - await ctx.l2TokenBridge.l1TokenBridge(), - ctx.accounts.l1TokenBridgeEOA.address - ); - }) - - .test("withdraw() :: withdrawals disabled", async (ctx) => { - const { - l2TokenBridge, - stubs: { l2Token: l2TokenStub }, - } = ctx; - - await ctx.l2TokenBridge.disableWithdrawals(); - - assert.isFalse(await ctx.l2TokenBridge.isWithdrawalsEnabled()); - - await assert.revertsWith( - l2TokenBridge.withdraw( - l2TokenStub.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorWithdrawalsDisabled()" - ); - }) - - .test("withdraw() :: unsupported l2Token", async (ctx) => { - const { - l2TokenBridge, - accounts: { stranger }, - } = ctx; - await assert.revertsWith( - l2TokenBridge.withdraw(stranger.address, wei`1 ether`, wei`1 gwei`, "0x"), - "ErrorUnsupportedL2Token()" - ); - }) - - .test("withdraw()", async (ctx) => { - const { - l2TokenBridge, - accounts: { deployer, l1TokenBridgeEOA }, - stubs: { - l2Messenger: l2MessengerStub, - l1Token: l1TokenStub, - l2Token: l2TokenStub, - }, - } = ctx; - - const deployerBalanceBefore = await l2TokenStub.balanceOf(deployer.address); - const totalSupplyBefore = await l2TokenStub.totalSupply(); - - const amount = wei`1 ether`; - const l1Gas = wei`1 wei`; - const data = "0xdeadbeaf"; - - const tx = await l2TokenBridge.withdraw( - l2TokenStub.address, - amount, - l1Gas, - data - ); - - await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ - l1TokenStub.address, - l2TokenStub.address, - deployer.address, - deployer.address, - amount, - data, - ]); - - await assert.emits(l2MessengerStub, tx, "SentMessage", [ - l1TokenBridgeEOA.address, - l2TokenBridge.address, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( - "finalizeERC20Withdrawal", - [ - l1TokenStub.address, - l2TokenStub.address, - deployer.address, - deployer.address, - amount, - data, - ] - ), - 1, // message nonce - l1Gas, - ]); - - assert.equalBN( - await l2TokenStub.balanceOf(deployer.address), - deployerBalanceBefore.sub(amount) - ); - - assert.equalBN( - await l2TokenStub.totalSupply(), - totalSupplyBefore.sub(amount) - ); - }) - - .test("withdrawTo() :: withdrawals disabled", async (ctx) => { - const { - l2TokenBridge, - stubs: { l2Token: l2TokenStub }, - accounts: { recipient }, - } = ctx; - - await ctx.l2TokenBridge.disableWithdrawals(); - - assert.isFalse(await ctx.l2TokenBridge.isWithdrawalsEnabled()); - - await assert.revertsWith( - l2TokenBridge.withdrawTo( - l2TokenStub.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorWithdrawalsDisabled()" - ); - }) - - .test("withdrawTo() :: unsupported l2Token", async (ctx) => { - const { - l2TokenBridge, - accounts: { stranger, recipient }, - } = ctx; - await assert.revertsWith( - l2TokenBridge.withdrawTo( - stranger.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorUnsupportedL2Token()" - ); - }) - - .test("withdrawTo()", async (ctx) => { - const { - l2TokenBridge, - accounts: { deployer, recipient, l1TokenBridgeEOA }, - stubs: { - l2Messenger: l2MessengerStub, - l1Token: l1TokenStub, - l2Token: l2TokenStub, - }, - } = ctx; - - const deployerBalanceBefore = await l2TokenStub.balanceOf(deployer.address); - const totalSupplyBefore = await l2TokenStub.totalSupply(); - - const amount = wei`1 ether`; - const l1Gas = wei`1 wei`; - const data = "0xdeadbeaf"; - - const tx = await l2TokenBridge.withdrawTo( - l2TokenStub.address, - recipient.address, - amount, - l1Gas, - data - ); - - await assert.emits(l2TokenBridge, tx, "WithdrawalInitiated", [ - l1TokenStub.address, - l2TokenStub.address, - deployer.address, - recipient.address, - amount, - data, - ]); - - await assert.emits(l2MessengerStub, tx, "SentMessage", [ - l1TokenBridgeEOA.address, - l2TokenBridge.address, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( - "finalizeERC20Withdrawal", - [ - l1TokenStub.address, - l2TokenStub.address, - deployer.address, - recipient.address, - amount, - data, - ] - ), - 1, // message nonce - l1Gas, - ]); - - assert.equalBN( - await l2TokenStub.balanceOf(deployer.address), - deployerBalanceBefore.sub(amount) - ); - - assert.equalBN( - await l2TokenStub.totalSupply(), - totalSupplyBefore.sub(amount) - ); - }) - - .test("finalizeDeposit() :: deposits disabled", async (ctx) => { - const { - l2TokenBridge, - accounts: { l2MessengerStubEOA, deployer, recipient }, - stubs: { l1Token: l1TokenStub, l2Token: l2TokenStub }, - } = ctx; - - await l2TokenBridge.disableDeposits(); - - assert.isFalse(await l2TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - l2TokenBridge - .connect(l2MessengerStubEOA) - .finalizeDeposit( - l1TokenStub.address, - l2TokenStub.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("finalizeDeposit() :: unsupported l1Token", async (ctx) => { - const { - l2TokenBridge, - accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, - stubs: { l2Token: l2TokenStub }, - } = ctx; - - await assert.revertsWith( - l2TokenBridge - .connect(l2MessengerStubEOA) - .finalizeDeposit( - stranger.address, - l2TokenStub.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL1Token()" - ); - }) - - .test("finalizeDeposit() :: unsupported l2Token", async (ctx) => { - const { - l2TokenBridge, - accounts: { l2MessengerStubEOA, deployer, recipient, stranger }, - stubs: { l1Token: l1TokenStub }, - } = ctx; - - await assert.revertsWith( - l2TokenBridge - .connect(l2MessengerStubEOA) - .finalizeDeposit( - l1TokenStub.address, - stranger.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL2Token()" - ); - }) - - .test("finalizeDeposit() :: unauthorized messenger", async (ctx) => { - const { - l2TokenBridge, - stubs: { l1Token, l2Token }, - accounts: { deployer, recipient, stranger }, - } = ctx; - - await assert.revertsWith( - l2TokenBridge - .connect(stranger) - .finalizeDeposit( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnauthorizedMessenger()" - ); - }) - - .test("finalizeDeposit() :: wrong cross domain sender", async (ctx) => { - const { - l2TokenBridge, - stubs: { l1Token, l2Token, l2Messenger }, - accounts: { deployer, recipient, stranger, l2MessengerStubEOA }, - } = ctx; - - await l2Messenger.setXDomainMessageSender(stranger.address); - - await assert.revertsWith( - l2TokenBridge - .connect(l2MessengerStubEOA) - .finalizeDeposit( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorWrongCrossDomainSender()" - ); - }) - - .test("finalizeDeposit()", async (ctx) => { - const { - l2TokenBridge, - stubs: { l1Token, l2Token, l2Messenger }, - accounts: { deployer, recipient, l2MessengerStubEOA, l1TokenBridgeEOA }, - } = ctx; - - await l2Messenger.setXDomainMessageSender(l1TokenBridgeEOA.address); - - const totalSupplyBefore = await l2Token.totalSupply(); - - const amount = wei`1 ether`; - const data = "0xdeadbeaf"; - - const tx = await l2TokenBridge - .connect(l2MessengerStubEOA) - .finalizeDeposit( - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - amount, - data - ); - - await assert.emits(l2TokenBridge, tx, "DepositFinalized", [ - l1Token.address, - l2Token.address, - deployer.address, - recipient.address, - amount, - data, - ]); - - assert.equalBN(await l2Token.balanceOf(recipient.address), amount); - assert.equalBN(await l2Token.totalSupply(), totalSupplyBefore.add(amount)); - }) - - .run(); - -async function ctxFactory() { - const [deployer, stranger, recipient, l1TokenBridgeEOA] = - await hre.ethers.getSigners(); - - const l2Messenger = await new CrossDomainMessengerStub__factory( - deployer - ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); - - const l2MessengerStubEOA = await testing.impersonate(l2Messenger.address); - - const l1Token = await new ERC20BridgedStub__factory(deployer).deploy( - "L1 Token", - "L1" - ); - - const l2Token = await new ERC20BridgedStub__factory(deployer).deploy( - "L2 Token", - "L2" - ); - - const emptyContract = await new EmptyContractStub__factory(deployer).deploy({ - value: wei.toBigNumber(wei`1 ether`), - }); - const emptyContractEOA = await testing.impersonate(emptyContract.address); - - const l2TokenBridgeImpl = await new L2ERC20TokenBridge__factory( - deployer - ).deploy( - l2Messenger.address, - l1TokenBridgeEOA.address, - l1Token.address, - l2Token.address - ); - - const l2TokenBridgeProxy = await new OssifiableProxy__factory( - deployer - ).deploy( - l2TokenBridgeImpl.address, - deployer.address, - l2TokenBridgeImpl.interface.encodeFunctionData("initialize", [ - deployer.address, - ]) - ); - - const l2TokenBridge = L2ERC20TokenBridge__factory.connect( - l2TokenBridgeProxy.address, - deployer - ); - - await l2Token.transfer(l2TokenBridge.address, wei`100 ether`); - - const roles = await Promise.all([ - l2TokenBridge.DEPOSITS_ENABLER_ROLE(), - l2TokenBridge.DEPOSITS_DISABLER_ROLE(), - l2TokenBridge.WITHDRAWALS_ENABLER_ROLE(), - l2TokenBridge.WITHDRAWALS_DISABLER_ROLE(), - ]); - - for (const role of roles) { - await l2TokenBridge.grantRole(role, deployer.address); - } - - await l2TokenBridge.enableDeposits(); - await l2TokenBridge.enableWithdrawals(); - - return { - stubs: { l1Token, l2Token, l2Messenger: l2Messenger }, - accounts: { - deployer, - stranger, - recipient, - l2MessengerStubEOA, - emptyContractEOA, - l1TokenBridgeEOA, - }, - l2TokenBridge, - }; -} diff --git a/test/optimism/OpStackTokenRatePusher.unit.test.ts b/test/optimism/OpStackTokenRatePusher.unit.test.ts new file mode 100644 index 00000000..cf7a99f6 --- /dev/null +++ b/test/optimism/OpStackTokenRatePusher.unit.test.ts @@ -0,0 +1,102 @@ +import { ethers } from "hardhat"; +import { assert } from "chai"; +import { utils } from 'ethers' +import { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; + +import { + OpStackTokenRatePusher__factory, + CrossDomainMessengerStub__factory, + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + ITokenRateOracle__factory, + ITokenRatePusher__factory +} from "../../typechain"; + +unit("OpStackTokenRatePusher", ctxFactory) + + .test("initial state", async (ctx) => { + const { tokenRateOracle } = ctx.accounts; + const { opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub } = ctx.contracts; + + assert.equal(await opStackTokenRatePusher.MESSENGER(), l1MessengerStub.address); + assert.equal(await opStackTokenRatePusher.WSTETH(), l1TokenNonRebasableStub.address); + assert.equal(await opStackTokenRatePusher.L2_TOKEN_RATE_ORACLE(), tokenRateOracle.address); + assert.equalBN(await opStackTokenRatePusher.L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE(), 123); + const iTokenRatePusher = getInterfaceID(ITokenRatePusher__factory.createInterface()); + assert.isTrue(await opStackTokenRatePusher.supportsInterface(iTokenRatePusher._hex)); + }) + + .test("pushTokenRate() :: success", async (ctx) => { + const { tokenRateOracle } = ctx.accounts; + const { l2GasLimitForPushingTokenRate } = ctx.constants; + const { opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub } = ctx.contracts; + + let tokenRate = await l1TokenNonRebasableStub.stEthPerToken(); + + let tx = await opStackTokenRatePusher.pushTokenRate(); + + const provider = await ethers.provider; + const blockNumber = await provider.getBlockNumber(); + const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; + + await assert.emits(l1MessengerStub , tx, "SentMessage", [ + tokenRateOracle.address, + opStackTokenRatePusher.address, + ITokenRateOracle__factory.createInterface().encodeFunctionData( + "updateRate", + [ + tokenRate, + blockTimestamp + ] + ), + 1, + l2GasLimitForPushingTokenRate, + ]); + }) + + .run(); + +async function ctxFactory() { + const [deployer, bridge, stranger, tokenRateOracle, l1TokenBridgeEOA] = await ethers.getSigners(); + + const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token Rebasable", + "L1R" + ); + + const l1TokenNonRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( + l1TokenRebasableStub.address, + "L1 Token Non Rebasable", + "L1NR" + ); + + const l1MessengerStub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + await l1MessengerStub.setXDomainMessageSender(l1TokenBridgeEOA.address); + + const l2GasLimitForPushingTokenRate = 123; + + const opStackTokenRatePusher = await new OpStackTokenRatePusher__factory(deployer).deploy( + l1MessengerStub.address, + l1TokenNonRebasableStub.address, + tokenRateOracle.address, + l2GasLimitForPushingTokenRate + ); + + return { + accounts: { deployer, bridge, stranger, tokenRateOracle }, + contracts: { opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub }, + constants: { l2GasLimitForPushingTokenRate } + }; +} + +export function getInterfaceID(contractInterface: utils.Interface) { + let interfaceID = ethers.constants.Zero; + const functions: string[] = Object.keys(contractInterface.functions); + for (let i = 0; i < functions.length; i++) { + interfaceID = interfaceID.xor(contractInterface.getSighash(functions[i])); + } + return interfaceID; +} diff --git a/test/optimism/TokenRateNotifier.unit.test.ts b/test/optimism/TokenRateNotifier.unit.test.ts new file mode 100644 index 00000000..c2f3864c --- /dev/null +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -0,0 +1,298 @@ +import { ethers } from "hardhat"; +import { assert } from "chai"; +import { utils } from 'ethers' +import { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { + TokenRateNotifier__factory, + ITokenRatePusher__factory, + OpStackTokenRatePusher__factory, + ITokenRateOracle__factory, + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + CrossDomainMessengerStub__factory, + OpStackTokenRatePusherWithSomeErrorStub__factory, + OpStackTokenRatePusherWithOutOfGasErrorStub__factory +} from "../../typechain"; + +unit("TokenRateNotifier", ctxFactory) + + .test("deploy with zero address owner", async (ctx) => { + const { deployer } = ctx.accounts; + + await assert.revertsWith( + new TokenRateNotifier__factory(deployer).deploy(ethers.constants.AddressZero), + "ErrorZeroAddressOwner()" + ); + }) + + .test("initial state", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + + assert.equalBN(await tokenRateNotifier.MAX_OBSERVERS_COUNT(), 32); + const iTokenRateObserver = getInterfaceID(ITokenRatePusher__factory.createInterface()); + assert.equal(await tokenRateNotifier.REQUIRED_INTERFACE(), iTokenRateObserver._hex); + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + }) + + .test("addObserver() :: not the owner", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { stranger } = ctx.accounts; + + await assert.revertsWith( + tokenRateNotifier + .connect(stranger) + .addObserver(ethers.constants.AddressZero), + "Ownable: caller is not the owner" + ); + }) + + .test("addObserver() :: revert on adding zero address observer", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + + await assert.revertsWith( + tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(ethers.constants.AddressZero), + "ErrorZeroAddressObserver()" + ); + }) + + .test("addObserver() :: revert on adding observer with bad interface", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const observer = await new TokenRateNotifier__factory(deployer).deploy(deployer.address); + await assert.revertsWith( + tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(observer.address), + "ErrorBadObserverInterface()" + ); + }) + + .test("addObserver() :: revert on adding too many observers", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; + const { deployer, owner, tokenRateOracle } = ctx.accounts; + const { l2GasLimitForPushingTokenRate } = ctx.constants; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + const maxObservers = await tokenRateNotifier.MAX_OBSERVERS_COUNT(); + for (let i = 0; i < maxObservers.toNumber(); i++) { + + const { + opStackTokenRatePusher + } = await getOpStackTokenRatePusher(deployer, owner, tokenRateOracle, l2GasLimitForPushingTokenRate); + + await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address); + } + assert.equalBN(await tokenRateNotifier.observersLength(), maxObservers); + + await assert.revertsWith( + tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address), + "ErrorMaxObserversCountExceeded()" + ); + }) + + .test("addObserver() :: revert on adding the same observer twice", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; + + await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address); + + await assert.revertsWith( + tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address), + "ErrorAddExistedObserver()" + ); + }) + + .test("addObserver() :: happy path of adding observer", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + const tx = await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address); + assert.equalBN(await tokenRateNotifier.observersLength(), 1); + + await assert.emits(tokenRateNotifier, tx, "ObserverAdded", [opStackTokenRatePusher.address]); + }) + + .test("removeObserver() :: revert on calling by not the owner", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { stranger } = ctx.accounts; + + await assert.revertsWith( + tokenRateNotifier + .connect(stranger) + .removeObserver(ethers.constants.AddressZero), + "Ownable: caller is not the owner" + ); + }) + + .test("removeObserver() :: revert on removing non-added observer", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + + await assert.revertsWith( + tokenRateNotifier + .connect(ctx.accounts.owner) + .removeObserver(opStackTokenRatePusher.address), + "ErrorNoObserverToRemove()" + ); + }) + + .test("removeObserver() :: happy path of removing observer", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + + await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address); + + assert.equalBN(await tokenRateNotifier.observersLength(), 1); + + const tx = await tokenRateNotifier + .connect(ctx.accounts.owner) + .removeObserver(opStackTokenRatePusher.address); + await assert.emits(tokenRateNotifier, tx, "ObserverRemoved", [opStackTokenRatePusher.address]); + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + }) + + .test("handlePostTokenRebase() :: failed with some error", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const observer = await new OpStackTokenRatePusherWithSomeErrorStub__factory(deployer).deploy(); + await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(observer.address); + + const tx = await tokenRateNotifier.handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7); + + await assert.emits(tokenRateNotifier, tx, "PushTokenRateFailed", [observer.address, "0x332e27d2"]); + }) + + .test("handlePostTokenRebase() :: revert when observer has out of gas error", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const observer = await new OpStackTokenRatePusherWithOutOfGasErrorStub__factory(deployer).deploy(); + await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(observer.address); + + await assert.revertsWith( + tokenRateNotifier.handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7), + "ErrorTokenRateNotifierRevertedWithNoData()" + ); + }) + + .test("handlePostTokenRebase() :: happy path of handling token rebase", async (ctx) => { + const { + tokenRateNotifier, + l1MessengerStub, + opStackTokenRatePusher, + l1TokenNonRebasableStub + } = ctx.contracts; + const { tokenRateOracle } = ctx.accounts; + const { l2GasLimitForPushingTokenRate } = ctx.constants; + + let tokenRate = await l1TokenNonRebasableStub.stEthPerToken(); + await tokenRateNotifier + .connect(ctx.accounts.owner) + .addObserver(opStackTokenRatePusher.address); + let tx = await tokenRateNotifier.handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7); + + const provider = await ethers.provider; + const blockNumber = await provider.getBlockNumber(); + const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; + + await assert.emits(l1MessengerStub, tx, "SentMessage", [ + tokenRateOracle.address, + opStackTokenRatePusher.address, + ITokenRateOracle__factory.createInterface().encodeFunctionData( + "updateRate", + [ + tokenRate, + blockTimestamp + ] + ), + 1, + l2GasLimitForPushingTokenRate, + ]); + }) + + .run(); + +async function getOpStackTokenRatePusher( + deployer: SignerWithAddress, + owner: SignerWithAddress, + tokenRateOracle: SignerWithAddress, + l2GasLimitForPushingTokenRate: number) { + + const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(owner.address); + + const l1MessengerStub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + + const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token Rebasable", + "L1R" + ); + + const l1TokenNonRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( + l1TokenRebasableStub.address, + "L1 Token Non Rebasable", + "L1NR" + ); + + const opStackTokenRatePusher = await new OpStackTokenRatePusher__factory(deployer).deploy( + l1MessengerStub.address, + l1TokenNonRebasableStub.address, + tokenRateOracle.address, + l2GasLimitForPushingTokenRate + ); + + return {tokenRateNotifier, opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub} +} + +async function ctxFactory() { + const [deployer, owner, stranger, tokenRateOracle] = await ethers.getSigners(); + + const l2GasLimitForPushingTokenRate = 123; + + const { + tokenRateNotifier, + opStackTokenRatePusher, + l1MessengerStub, + l1TokenNonRebasableStub + } = await getOpStackTokenRatePusher(deployer, owner, tokenRateOracle, l2GasLimitForPushingTokenRate); + + return { + accounts: { deployer, owner, stranger, tokenRateOracle }, + contracts: { tokenRateNotifier, opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub }, + constants: { l2GasLimitForPushingTokenRate } + }; +} + +export function getInterfaceID(contractInterface: utils.Interface) { + let interfaceID = ethers.constants.Zero; + const functions: string[] = Object.keys(contractInterface.functions); + for (let i = 0; i < functions.length; i++) { + interfaceID = interfaceID.xor(contractInterface.getSighash(functions[i])); + } + return interfaceID; +} diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts new file mode 100644 index 00000000..b309ef77 --- /dev/null +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -0,0 +1,188 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { BigNumber } from "ethers"; +import testing, { unit } from "../../utils/testing"; +import { + TokenRateOracle__factory, + CrossDomainMessengerStub__factory +} from "../../typechain"; +import { wei } from "../../utils/wei"; + +unit("TokenRateOracle", ctxFactory) + + .test("state after init", async (ctx) => { + const { tokenRateOracle, l2MessengerStub } = ctx.contracts; + const { bridge, l1TokenBridgeEOA } = ctx.accounts; + + assert.equal(await tokenRateOracle.MESSENGER(), l2MessengerStub.address); + assert.equal(await tokenRateOracle.L2_ERC20_TOKEN_BRIDGE(), bridge.address); + assert.equal(await tokenRateOracle.L1_TOKEN_RATE_PUSHER(), l1TokenBridgeEOA.address); + assert.equalBN(await tokenRateOracle.TOKEN_RATE_OUTDATED_DELAY(), 86400); + + assert.equalBN(await tokenRateOracle.latestAnswer(), 0); + + const { + roundId_, + answer_, + startedAt_, + updatedAt_, + answeredInRound_ + } = await tokenRateOracle.latestRoundData(); + + assert.equalBN(roundId_, 0); + assert.equalBN(answer_, 0); + assert.equalBN(startedAt_, 0); + assert.equalBN(updatedAt_, 0); + assert.equalBN(answeredInRound_, 0); + assert.equalBN(await tokenRateOracle.decimals(), 18); + }) + + .test("updateRate() :: called by non-bridge account", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { stranger } = ctx.accounts; + await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "ErrorNoRights(\""+stranger.address+"\")"); + }) + + .test("updateRate() :: called by messenger with incorrect cross-domain sender", async (ctx) => { + const { tokenRateOracle, l2MessengerStub } = ctx.contracts; + const { stranger, l2MessengerStubEOA } = ctx.accounts; + await l2MessengerStub.setXDomainMessageSender(stranger.address); + await assert.revertsWith(tokenRateOracle.connect(l2MessengerStubEOA).updateRate(10, 40), "ErrorNoRights(\""+l2MessengerStubEOA._address+"\")"); + }) + + .test("updateRate() :: incorrect time", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + const { tokenRateCorrect } = ctx.constants; + + const tx0 = await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, 1000); + const tx1 = await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, 20); + + await assert.emits(tokenRateOracle, tx1, "NewTokenRateOutdated", [tokenRateCorrect, 1000, 20]); + }) + + .test("updateRate() :: time in future", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + const { tokenRateCorrect, blockTimestamp } = ctx.constants; + + const timeInFuture = blockTimestamp + 100000; + await assert.revertsWith( + tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, timeInFuture), + "ErrorL1TimestampInFuture("+tokenRateCorrect+", "+timeInFuture+")" + ); + }) + + .test("updateRate() :: rate is out of range", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + const { tokenRateTooBig, tokenRateTooSmall, blockTimestamp } = ctx.constants; + + await assert.revertsWith( + tokenRateOracle.connect(bridge).updateRate(tokenRateTooBig, blockTimestamp), + "ErrorTokenRateIsOutOfRange("+tokenRateTooBig+", "+blockTimestamp+")" + ); + await assert.revertsWith( + tokenRateOracle.connect(bridge).updateRate(tokenRateTooSmall, blockTimestamp), + "ErrorTokenRateIsOutOfRange("+tokenRateTooSmall+", "+blockTimestamp+")" + ); + }) + + .test("updateRate() :: don't update state if values are the same", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + const { tokenRateCorrect } = ctx.constants; + + const tx1 = await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, 1000); + await assert.emits(tokenRateOracle, tx1, "RateUpdated", [tokenRateCorrect, 1000]); + + const tx2 = await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, 1000); + await assert.notEmits(tokenRateOracle, tx2, "RateUpdated"); + }) + + .test("updateRate() :: happy path called by bridge", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + const { tokenRateCorrect, blockTimestamp } = ctx.constants; + + await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, blockTimestamp); + + assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRateCorrect); + + const { + roundId_, + answer_, + startedAt_, + updatedAt_, + answeredInRound_ + } = await tokenRateOracle.latestRoundData(); + + assert.equalBN(roundId_, blockTimestamp); + assert.equalBN(answer_, tokenRateCorrect); + assert.equalBN(startedAt_, blockTimestamp); + assert.equalBN(updatedAt_, blockTimestamp); + assert.equalBN(answeredInRound_, blockTimestamp); + assert.equalBN(await tokenRateOracle.decimals(), 18); + }) + + .test("updateRate() :: happy path called by messenger with correct cross-domain sender", async (ctx) => { + const { tokenRateOracle, l2MessengerStub } = ctx.contracts; + const { l2MessengerStubEOA, l1TokenBridgeEOA } = ctx.accounts; + const { tokenRateCorrect, blockTimestamp } = ctx.constants; + + await l2MessengerStub.setXDomainMessageSender(l1TokenBridgeEOA.address); + + await tokenRateOracle.connect(l2MessengerStubEOA).updateRate(tokenRateCorrect, blockTimestamp); + + assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRateCorrect); + + const { + roundId_, + answer_, + startedAt_, + updatedAt_, + answeredInRound_ + } = await tokenRateOracle.latestRoundData(); + + assert.equalBN(roundId_, blockTimestamp); + assert.equalBN(answer_, tokenRateCorrect); + assert.equalBN(startedAt_, blockTimestamp); + assert.equalBN(updatedAt_, blockTimestamp); + assert.equalBN(answeredInRound_, blockTimestamp); + assert.equalBN(await tokenRateOracle.decimals(), 18); + }) + + .run(); + +async function ctxFactory() { + + const [deployer, bridge, stranger, l1TokenBridgeEOA] = await hre.ethers.getSigners(); + + const l2MessengerStub = await new CrossDomainMessengerStub__factory( + deployer + ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); + const l2MessengerStubEOA = await testing.impersonate(l2MessengerStub.address); + + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + l2MessengerStub.address, + bridge.address, + l1TokenBridgeEOA.address, + 86400 + ); + + const decimals = 18; + const decimalsBN = BigNumber.from(10).pow(decimals); + const tokenRateCorrect = BigNumber.from('12').pow(decimals - 1); + const tokenRateTooBig = BigNumber.from('2000').pow(decimals); + const tokenRateTooSmall = BigNumber.from('1').pow(decimals-3); + + const provider = await hre.ethers.provider; + const blockNumber = await provider.getBlockNumber(); + const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; + + return { + accounts: { deployer, bridge, stranger, l1TokenBridgeEOA, l2MessengerStubEOA }, + contracts: { tokenRateOracle, l2MessengerStub }, + constants: { tokenRateCorrect, tokenRateTooBig, tokenRateTooSmall, blockTimestamp } + }; +} diff --git a/test/optimism/_launch.test.ts b/test/optimism/_launch.test.ts index 4d192be3..41040dd2 100644 --- a/test/optimism/_launch.test.ts +++ b/test/optimism/_launch.test.ts @@ -5,7 +5,7 @@ import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; import testing, { scenario } from "../../utils/testing"; import { BridgingManagerRole } from "../../utils/bridging-management"; -import { L1ERC20TokenBridge__factory } from "../../typechain"; +import { L1LidoTokensBridge__factory } from "../../typechain"; const REVERT = env.bool("REVERT", true); @@ -22,28 +22,28 @@ scenario("Optimism :: Launch integration test", ctxFactory) }) .step("Enable deposits", async (ctx) => { - const { l1ERC20TokenBridge } = ctx; - assert.isFalse(await l1ERC20TokenBridge.isDepositsEnabled()); + const { l1LidoTokensBridge } = ctx; + assert.isFalse(await l1LidoTokensBridge.isDepositsEnabled()); - await l1ERC20TokenBridge.enableDeposits(); - assert.isTrue(await l1ERC20TokenBridge.isDepositsEnabled()); + await l1LidoTokensBridge.enableDeposits(); + assert.isTrue(await l1LidoTokensBridge.isDepositsEnabled()); }) .step("Renounce role", async (ctx) => { - const { l1ERC20TokenBridge, l1DevMultisig } = ctx; + const { l1LidoTokensBridge, l1DevMultisig } = ctx; assert.isTrue( - await l1ERC20TokenBridge.hasRole( + await l1LidoTokensBridge.hasRole( BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, await l1DevMultisig.getAddress() ) ); - await l1ERC20TokenBridge.renounceRole( + await l1LidoTokensBridge.renounceRole( BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, await l1DevMultisig.getAddress() ); assert.isFalse( - await l1ERC20TokenBridge.hasRole( + await l1LidoTokensBridge.hasRole( BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash, await l1DevMultisig.getAddress() ) @@ -55,7 +55,7 @@ scenario("Optimism :: Launch integration test", ctxFactory) async function ctxFactory() { const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); - const { l1Provider, l2Provider, l1ERC20TokenBridge } = await optimism + const { l1Provider, l2Provider, l1LidoTokensBridge } = await optimism .testing(networkName) .getIntegrationTestSetup(); @@ -73,8 +73,8 @@ async function ctxFactory() { l1Provider ); - const l1ERC20TokenBridgeImpl = L1ERC20TokenBridge__factory.connect( - l1ERC20TokenBridge.address, + const l1LidoTokensBridgeImpl = L1LidoTokensBridge__factory.connect( + l1LidoTokensBridge.address, l1DevMultisig ); @@ -82,7 +82,7 @@ async function ctxFactory() { l1Provider, l2Provider, l1DevMultisig, - l1ERC20TokenBridge: l1ERC20TokenBridgeImpl, + l1LidoTokensBridge: l1LidoTokensBridgeImpl, snapshot: { l1: l1Snapshot, l2: l2Snapshot, diff --git a/test/optimism/bridging-rebasable-to.e2e.test.ts b/test/optimism/bridging-rebasable-to.e2e.test.ts new file mode 100644 index 00000000..6dc3d5ac --- /dev/null +++ b/test/optimism/bridging-rebasable-to.e2e.test.ts @@ -0,0 +1,165 @@ +import { + CrossChainMessenger, + DAIBridgeAdapter, + MessageStatus, + } from "@eth-optimism/sdk"; + import { assert } from "chai"; + import { TransactionResponse } from "@ethersproject/providers"; + + import env from "../../utils/env"; + import { wei } from "../../utils/wei"; + import network from "../../utils/network"; + import optimism from "../../utils/optimism"; + import { ERC20Mintable } from "../../typechain"; + import { scenario } from "../../utils/testing"; + import { sleep } from "../../utils/testing/e2e"; + import { LidoBridgeAdapter } from "../../utils/optimism/LidoBridgeAdapter"; + + let depositTokensTxResponse: TransactionResponse; + let withdrawTokensTxResponse: TransactionResponse; + + scenario("Optimism :: Bridging via depositTo/withdrawTo E2E test", ctxFactory) + .step( + "Validate tester has required amount of L1 token", + async ({ l1TokenRebasable, l1Tester, depositAmount }) => { + const balanceBefore = await l1TokenRebasable.balanceOf(l1Tester.address); + if (balanceBefore.lt(depositAmount)) { + try { + await (l1TokenRebasable as ERC20Mintable).mint( + l1Tester.address, + depositAmount + ); + } catch {} + const balanceAfter = await l1TokenRebasable.balanceOf(l1Tester.address); + assert.isTrue( + balanceAfter.gte(depositAmount), + "Tester has not enough L1 token" + ); + } + } + ) + + .step("Set allowance for L1LidoTokensBridge to deposit", async (ctx) => { + const allowanceTxResponse = await ctx.crossChainMessenger.approveERC20( + ctx.l1TokenRebasable.address, + ctx.l2TokenRebasable.address, + ctx.depositAmount + ); + + await allowanceTxResponse.wait(); + + assert.equalBN( + await ctx.l1TokenRebasable.allowance( + ctx.l1Tester.address, + ctx.l1LidoTokensBridge.address + ), + ctx.depositAmount + ); + }) + + .step("Bridge tokens to L2 via depositERC20To()", async (ctx) => { + depositTokensTxResponse = await ctx.l1LidoTokensBridge + .connect(ctx.l1Tester) + .depositERC20To( + ctx.l1TokenRebasable.address, + ctx.l2TokenRebasable.address, + ctx.l1Tester.address, + ctx.depositAmount, + 2_000_000, + "0x" + ); + + await depositTokensTxResponse.wait(); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + depositTokensTxResponse.hash, + MessageStatus.RELAYED + ); + }) + + .step("Withdraw tokens from L2 via withdrawERC20To()", async (ctx) => { + withdrawTokensTxResponse = await ctx.l2ERC20ExtendedTokensBridge + .connect(ctx.l2Tester) + .withdrawTo( + ctx.l2TokenRebasable.address, + ctx.l1Tester.address, + ctx.withdrawalAmount, + 0, + "0x" + ); + await withdrawTokensTxResponse.wait(); + }) + + .step("Waiting for status to change to READY_TO_PROVE", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.READY_TO_PROVE + ); + }) + + .step("Proving the L2 -> L1 message", async (ctx) => { + const tx = await ctx.crossChainMessenger.proveMessage( + withdrawTokensTxResponse.hash + ); + await tx.wait(); + }) + + .step("Waiting for status to change to IN_CHALLENGE_PERIOD", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.IN_CHALLENGE_PERIOD + ); + }) + + .step("Waiting for status to change to READY_FOR_RELAY", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.READY_FOR_RELAY + ); + }) + + .step("Finalizing L2 -> L1 message", async (ctx) => { + const finalizationPeriod = await ctx.crossChainMessenger.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS(); + await sleep(finalizationPeriod * 1000); + await ctx.crossChainMessenger.finalizeMessage(withdrawTokensTxResponse); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse, + MessageStatus.RELAYED + ); + }) + + .run(); + + async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "sepolia"); + const testingSetup = await optimism.testing(networkName).getE2ETestSetup(); + + return { + depositAmount: wei`0.0025 ether`, + withdrawalAmount: wei`0.0025 ether`, + l1Tester: testingSetup.l1Tester, + l2Tester: testingSetup.l2Tester, + l1TokenRebasable: testingSetup.l1TokenRebasable, + l2TokenRebasable: testingSetup.l2TokenRebasable, + l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, + l2ERC20ExtendedTokensBridge: testingSetup.l2ERC20ExtendedTokensBridge, + crossChainMessenger: new CrossChainMessenger({ + l2ChainId: network.chainId("opt", networkName), + l1ChainId: network.chainId("eth", networkName), + l1SignerOrProvider: testingSetup.l1Tester, + l2SignerOrProvider: testingSetup.l2Tester, + bridges: { + LidoBridge: { + Adapter: LidoBridgeAdapter, + l1Bridge: testingSetup.l1LidoTokensBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, + }, + }, + }), + }; + } diff --git a/test/optimism/bridging-rebasable.e2e.test.ts b/test/optimism/bridging-rebasable.e2e.test.ts new file mode 100644 index 00000000..27aaa65d --- /dev/null +++ b/test/optimism/bridging-rebasable.e2e.test.ts @@ -0,0 +1,152 @@ +import { + CrossChainMessenger, + MessageStatus, + } from "@eth-optimism/sdk"; + import { assert } from "chai"; + import { TransactionResponse } from "@ethersproject/providers"; + + import env from "../../utils/env"; + import { wei } from "../../utils/wei"; + import network from "../../utils/network"; + import optimism from "../../utils/optimism"; + import { ERC20Mintable } from "../../typechain"; + import { scenario } from "../../utils/testing"; + import { sleep } from "../../utils/testing/e2e"; + import { LidoBridgeAdapter } from "../../utils/optimism/LidoBridgeAdapter"; + + let depositTokensTxResponse: TransactionResponse; + let withdrawTokensTxResponse: TransactionResponse; + + scenario("Optimism :: Bridging via deposit/withdraw E2E test", ctxFactory) + .step( + "Validate tester has required amount of L1 token", + async ({ l1TokenRebasable, l1Tester, depositAmount }) => { + const balanceBefore = await l1TokenRebasable.balanceOf(l1Tester.address); + if (balanceBefore.lt(depositAmount)) { + try { + await (l1TokenRebasable as ERC20Mintable).mint( + l1Tester.address, + depositAmount + ); + } catch {} + const balanceAfter = await l1TokenRebasable.balanceOf(l1Tester.address); + assert.isTrue( + balanceAfter.gte(depositAmount), + "Tester has not enough L1 token" + ); + } + } + ) + + .step("Set allowance for L1LidoTokensBridge to deposit", async (ctx) => { + const allowanceTxResponse = await ctx.crossChainMessenger.approveERC20( + ctx.l1TokenRebasable.address, + ctx.l2TokenRebasable.address, + ctx.depositAmount + ); + + await allowanceTxResponse.wait(); + + assert.equalBN( + await ctx.l1TokenRebasable.allowance( + ctx.l1Tester.address, + ctx.l1LidoTokensBridge.address + ), + ctx.depositAmount + ); + }) + + .step("Bridge tokens to L2 via depositERC20()", async (ctx) => { + depositTokensTxResponse = await ctx.crossChainMessenger.depositERC20( + ctx.l1TokenRebasable.address, + ctx.l2TokenRebasable.address, + ctx.depositAmount + ); + await depositTokensTxResponse.wait(); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + depositTokensTxResponse.hash, + MessageStatus.RELAYED + ); + }) + + .step("Withdraw tokens from L2 via withdrawERC20()", async (ctx) => { + withdrawTokensTxResponse = await ctx.crossChainMessenger.withdrawERC20( + ctx.l1TokenRebasable.address, + ctx.l2TokenRebasable.address, + ctx.withdrawalAmount + ); + await withdrawTokensTxResponse.wait(); + }) + + .step("Waiting for status to change to READY_TO_PROVE", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.READY_TO_PROVE + ); + }) + + .step("Proving the L2 -> L1 message", async (ctx) => { + const tx = await ctx.crossChainMessenger.proveMessage( + withdrawTokensTxResponse.hash + ); + await tx.wait(); + }) + + .step("Waiting for status to change to IN_CHALLENGE_PERIOD", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.IN_CHALLENGE_PERIOD + ); + }) + + .step("Waiting for status to change to READY_FOR_RELAY", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse.hash, + MessageStatus.READY_FOR_RELAY + ); + }) + + .step("Finalizing L2 -> L1 message", async (ctx) => { + const finalizationPeriod = await ctx.crossChainMessenger.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS(); + await sleep(finalizationPeriod * 1000); + await ctx.crossChainMessenger.finalizeMessage(withdrawTokensTxResponse); + }) + + .step("Waiting for status to change to RELAYED", async (ctx) => { + await ctx.crossChainMessenger.waitForMessageStatus( + withdrawTokensTxResponse, + MessageStatus.RELAYED + ); + }) + + .run(); + + async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "sepolia"); + const testingSetup = await optimism.testing(networkName).getE2ETestSetup(); + + return { + depositAmount: wei`0.0025 ether`, + withdrawalAmount: wei`0.0025 ether`, + l1Tester: testingSetup.l1Tester, + l1TokenRebasable: testingSetup.l1TokenRebasable, + l2TokenRebasable: testingSetup.l2TokenRebasable, + l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, + crossChainMessenger: new CrossChainMessenger({ + l2ChainId: network.chainId("opt", networkName), + l1ChainId: network.chainId("eth", networkName), + l1SignerOrProvider: testingSetup.l1Tester, + l2SignerOrProvider: testingSetup.l2Tester, + bridges: { + LidoBridge: { + Adapter: LidoBridgeAdapter, + l1Bridge: testingSetup.l1LidoTokensBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, + }, + }, + }), + }; + } diff --git a/test/optimism/bridging-rebasable.integration.test.ts b/test/optimism/bridging-rebasable.integration.test.ts new file mode 100644 index 00000000..905b6b36 --- /dev/null +++ b/test/optimism/bridging-rebasable.integration.test.ts @@ -0,0 +1,855 @@ +import { assert } from "chai"; + +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import optimism from "../../utils/optimism"; +import testing, { scenario } from "../../utils/testing"; +import { ethers } from "hardhat"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ERC20WrapperStub } from "../../typechain"; + +scenario("Optimism :: Bridging rebasable token integration test", ctxFactory) + .after(async (ctx) => { + await ctx.l1Provider.send("evm_revert", [ctx.snapshot.l1]); + await ctx.l2Provider.send("evm_revert", [ctx.snapshot.l2]); + }) + + .step("Activate bridging on L1", async (ctx) => { + const { l1LidoTokensBridge } = ctx; + const { l1ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l1LidoTokensBridge + .connect(l1ERC20ExtendedTokensBridgeAdmin) + .enableDeposits(); + } else { + console.log("L1 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l1LidoTokensBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l1LidoTokensBridge + .connect(l1ERC20ExtendedTokensBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L1 withdrawals already enabled"); + } + + assert.isTrue(await l1LidoTokensBridge.isDepositsEnabled()); + assert.isTrue(await l1LidoTokensBridge.isWithdrawalsEnabled()); + }) + + .step("Activate bridging on L2", async (ctx) => { + const { l2ERC20ExtendedTokensBridge } = ctx; + const { l2ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l2ERC20ExtendedTokensBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l2ERC20ExtendedTokensBridge + .connect(l2ERC20ExtendedTokensBridgeAdmin) + .enableDeposits(); + } else { + console.log("L2 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l2ERC20ExtendedTokensBridge + .connect(l2ERC20ExtendedTokensBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L2 withdrawals already enabled"); + } + + assert.isTrue(await l2ERC20ExtendedTokensBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled()); + }) + + .step("Set up Token Rate Oracle by pushing first rate", async (ctx) => { + + const { + l1Token, + l1TokenRebasable, + l2TokenRebasable, + l1LidoTokensBridge, + l2CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l2Provider + } = ctx; + + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1LidoTokensBridge.address, + l2ERC20ExtendedTokensBridge.address, + 0, + 300_000, + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + }) + + .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1LidoTokensBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l1Provider + } = ctx; + + const { accountA: tokenHolderA } = ctx.accounts; + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1LidoTokensBridge.address, 0); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1LidoTokensBridge.address + ); + + const tx = await l1LidoTokensBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + 0, + 200_000, + "0x" + ); + + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20ExtendedTokensBridge.address, + l1LidoTokensBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore + ); + }) + + .step("Finalize deposit zero tokens on L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l2TokenRebasable, + l1LidoTokensBridge, + l2CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l2Provider + } = ctx; + + const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); + + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + + const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1LidoTokensBridge.address, + l2ERC20ExtendedTokensBridge.address, + 0, + 300_000, + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + "0x", + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore + ); + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TokenRebasableTotalSupplyBefore + ); + }) + + .step("L1 -> L2 deposit via depositERC20() method", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1LidoTokensBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l1Provider + } = ctx; + const { accountA: tokenHolderA } = ctx.accounts; + const { depositAmountNonRebasable, depositAmountRebasable } = ctx.common; + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1LidoTokensBridge.address, depositAmountRebasable); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1LidoTokensBridge.address + ); + + const tx = await l1LidoTokensBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + depositAmountRebasable, + 200_000, + "0x" + ); + + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmountRebasable, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmountNonRebasable, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20ExtendedTokensBridge.address, + l1LidoTokensBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmountNonRebasable) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH + tokenHolderABalanceBefore.sub(depositAmountRebasable) + ); + }) + + .step("Finalize deposit on L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l2TokenRebasable, + l1LidoTokensBridge, + l2CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l2Provider + } = ctx; + const { depositAmountNonRebasable, depositAmountRebasable } = ctx.common; + + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + + const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); + const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1LidoTokensBridge.address, + l2ERC20ExtendedTokensBridge.address, + 0, + 300_000, + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmountNonRebasable, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmountRebasable, + "0x", + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(depositAmountRebasable) + ); + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TokenRebasableTotalSupplyBefore.add(depositAmountRebasable) + ); + }) + + .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { + const { accountA: tokenHolderA } = ctx.accounts; + const { withdrawalAmountRebasable } = ctx.common; + const { + l1TokenRebasable, + l2TokenRebasable, + l2ERC20ExtendedTokensBridge + } = ctx; + + const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l2TotalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2ERC20ExtendedTokensBridge + .connect(tokenHolderA.l2Signer) + .withdraw( + l2TokenRebasable.address, + withdrawalAmountRebasable, + 0, + "0x" + ); + + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmountRebasable, + "0x", + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.sub(withdrawalAmountRebasable) + ); + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TotalSupplyBefore.sub(withdrawalAmountRebasable) + ); + }) + + .step("Finalize withdrawal on L1", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1CrossDomainMessenger, + l1LidoTokensBridge, + l2CrossDomainMessenger, + l2TokenRebasable, + l2ERC20ExtendedTokensBridge, + } = ctx; + const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; + const { withdrawalAmountNonRebasable, withdrawalAmountRebasable } = ctx.common; + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address + ); + + await l1CrossDomainMessenger + .connect(l1Stranger) + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); + + const tx = await l1CrossDomainMessenger + .connect(l1Stranger) + .relayMessage( + l1LidoTokensBridge.address, + l2CrossDomainMessenger.address, + l1LidoTokensBridge.interface.encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmountNonRebasable, + "0x", + ] + ), + 0 + ); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmountRebasable, + "0x", + ]); + + assert.equalBN( + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(withdrawalAmountRebasable) + ); + }) + + + .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { + + const { + l1Token, + l1TokenRebasable, + l1LidoTokensBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l1Provider + } = ctx; + const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; + assert.notEqual(tokenHolderA.address, tokenHolderB.address); + + const { exchangeRate } = ctx.common; + const depositAmountNonRebasable = wei`0.03 ether`; + const depositAmountRebasable = wei.toBigNumber(depositAmountNonRebasable).mul(exchangeRate); + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1LidoTokensBridge.address, depositAmountRebasable); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address + ); + + const tx = await l1LidoTokensBridge + .connect(tokenHolderA.l1Signer) + .depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + depositAmountRebasable, + 200_000, + "0x" + ); + + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountRebasable, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountNonRebasable, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20ExtendedTokensBridge.address, + l1LidoTokensBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmountNonRebasable) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH + tokenHolderABalanceBefore.sub(depositAmountRebasable) + ); + }) + + .step("Finalize deposit on L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1LidoTokensBridge, + l2TokenRebasable, + l2CrossDomainMessenger, + l2ERC20ExtendedTokensBridge, + l2Provider + } = ctx; + + const { + accountA: tokenHolderA, + accountB: tokenHolderB, + l1CrossDomainMessengerAliased, + } = ctx.accounts; + + const { exchangeRate } = ctx.common; + + const depositAmountNonRebasable = wei`0.03 ether`; + const depositAmountRebasable = wei.toBigNumber(depositAmountNonRebasable).mul(exchangeRate); + + const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); + + const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tokenHolderBBalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderB.address + ); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1LidoTokensBridge.address, + l2ERC20ExtendedTokensBridge.address, + 0, + 300_000, + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountNonRebasable, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountRebasable, + "0x", + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderB.address), + tokenHolderBBalanceBefore.add(depositAmountRebasable) + ); + + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TokenRebasableTotalSupplyBefore.add(depositAmountRebasable) + ); + }) + + .step("L2 -> L1 withdrawal via withdrawTo()", async (ctx) => { + const { l1TokenRebasable, l2TokenRebasable, l2ERC20ExtendedTokensBridge } = ctx; + const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; + + const { exchangeRate } = ctx.common; + const withdrawalAmountNonRebasable = wei`0.03 ether`; + const withdrawalAmountRebasable = wei.toBigNumber(withdrawalAmountNonRebasable).mul(exchangeRate); + + const tokenHolderBBalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderB.address + ); + const l2TotalSupplyBefore = await l2TokenRebasable.totalSupply(); + + const tx = await l2ERC20ExtendedTokensBridge + .connect(tokenHolderB.l2Signer) + .withdrawTo( + l2TokenRebasable.address, + tokenHolderA.address, + withdrawalAmountRebasable, + 0, + "0x" + ); + + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmountRebasable, + "0x", + ]); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderB.address), + tokenHolderBBalanceBefore.sub(withdrawalAmountRebasable) + ); + + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TotalSupplyBefore.sub(withdrawalAmountRebasable) + ); + }) + + .step("Finalize withdrawal on L1", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1CrossDomainMessenger, + l1LidoTokensBridge, + l2CrossDomainMessenger, + l2TokenRebasable, + l2ERC20ExtendedTokensBridge, + } = ctx; + const { + accountA: tokenHolderA, + accountB: tokenHolderB, + l1Stranger, + } = ctx.accounts; + + const { exchangeRate } = ctx.common; + const withdrawalAmountNonRebasable = wei`0.03 ether`; + const withdrawalAmountRebasable = wei.toBigNumber(withdrawalAmountNonRebasable).mul(exchangeRate); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address + ); + + await l1CrossDomainMessenger + .connect(l1Stranger) + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); + + const tx = await l1CrossDomainMessenger + .connect(l1Stranger) + .relayMessage( + l1LidoTokensBridge.address, + l2CrossDomainMessenger.address, + l1LidoTokensBridge.interface.encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmountNonRebasable, + "0x", + ] + ), + 0 + ); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmountRebasable, + "0x", + ]); + + assert.equalBN( + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(withdrawalAmountRebasable) + ); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); + + const { + l1Provider, + l2Provider, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, + ...contracts + } = await optimism.testing(networkName).getIntegrationTestSetup(); + + const l1Snapshot = await l1Provider.send("evm_snapshot", []); + const l2Snapshot = await l2Provider.send("evm_snapshot", []); + + await optimism.testing(networkName).stubL1CrossChainMessengerContract(); + + const accountA = testing.accounts.accountA(l1Provider, l2Provider); + const accountB = testing.accounts.accountB(l1Provider, l2Provider); + + const exchangeRate = 2; + const depositAmountNonRebasable = wei`0.15 ether`; + const depositAmountRebasable = wei.toBigNumber(depositAmountNonRebasable).mul(exchangeRate); + + const withdrawalAmountNonRebasable = wei`0.05 ether`; + const withdrawalAmountRebasable = wei.toBigNumber(withdrawalAmountNonRebasable).mul(exchangeRate); + + await testing.setBalance( + await contracts.l1TokensHolder.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l1ERC20ExtendedTokensBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l2ERC20ExtendedTokensBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + await contracts.l1TokenRebasable + .connect(contracts.l1TokensHolder) + .transfer(accountA.l1Signer.address, depositAmountRebasable); + + const l1CrossDomainMessengerAliased = await testing.impersonate( + testing.accounts.applyL1ToL2Alias(contracts.l1CrossDomainMessenger.address), + l2Provider + ); + + await testing.setBalance( + await l1CrossDomainMessengerAliased.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + return { + l1Provider, + l2Provider, + ...contracts, + accounts: { + accountA, + accountB, + l1Stranger: testing.accounts.stranger(l1Provider), + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, + l1CrossDomainMessengerAliased, + }, + common: { + depositAmountNonRebasable, + depositAmountRebasable, + withdrawalAmountNonRebasable, + withdrawalAmountRebasable, + exchangeRate, + }, + snapshot: { + l1: l1Snapshot, + l2: l2Snapshot, + }, + }; +} + +async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapperStub) { + const stEthPerToken = await l1Token.stEthPerToken(); + const blockNumber = await l1Provider.getBlockNumber(); + const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + const stEthPerTokenStr = ethers.utils.hexZeroPad(stEthPerToken.toHexString(), 12); + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); +} diff --git a/test/optimism/bridging-to.e2e.test.ts b/test/optimism/bridging-to.e2e.test.ts index a7a0dbc6..13d9833f 100644 --- a/test/optimism/bridging-to.e2e.test.ts +++ b/test/optimism/bridging-to.e2e.test.ts @@ -38,7 +38,7 @@ scenario("Optimism :: Bridging via depositTo/withdrawTo E2E test", ctxFactory) } ) - .step("Set allowance for L1ERC20TokenBridge to deposit", async (ctx) => { + .step("Set allowance for L1LidoTokensBridge to deposit", async (ctx) => { const allowanceTxResponse = await ctx.crossChainMessenger.approveERC20( ctx.l1Token.address, ctx.l2Token.address, @@ -50,14 +50,14 @@ scenario("Optimism :: Bridging via depositTo/withdrawTo E2E test", ctxFactory) assert.equalBN( await ctx.l1Token.allowance( ctx.l1Tester.address, - ctx.l1ERC20TokenBridge.address + ctx.l1LidoTokensBridge.address ), ctx.depositAmount ); }) .step("Bridge tokens to L2 via depositERC20To()", async (ctx) => { - depositTokensTxResponse = await ctx.l1ERC20TokenBridge + depositTokensTxResponse = await ctx.l1LidoTokensBridge .connect(ctx.l1Tester) .depositERC20To( ctx.l1Token.address, @@ -79,7 +79,7 @@ scenario("Optimism :: Bridging via depositTo/withdrawTo E2E test", ctxFactory) }) .step("Withdraw tokens from L2 via withdrawERC20To()", async (ctx) => { - withdrawTokensTxResponse = await ctx.l2ERC20TokenBridge + withdrawTokensTxResponse = await ctx.l2ERC20ExtendedTokensBridge .connect(ctx.l2Tester) .withdrawTo( ctx.l2Token.address, @@ -145,8 +145,8 @@ async function ctxFactory() { l2Tester: testingSetup.l2Tester, l1Token: testingSetup.l1Token, l2Token: testingSetup.l2Token, - l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, - l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, + l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, + l2ERC20ExtendedTokensBridge: testingSetup.l2ERC20ExtendedTokensBridge, crossChainMessenger: new CrossChainMessenger({ l2ChainId: network.chainId("opt", networkName), l1ChainId: network.chainId("eth", networkName), @@ -155,8 +155,8 @@ async function ctxFactory() { bridges: { LidoBridge: { Adapter: DAIBridgeAdapter, - l1Bridge: testingSetup.l1ERC20TokenBridge.address, - l2Bridge: testingSetup.l2ERC20TokenBridge.address, + l1Bridge: testingSetup.l1LidoTokensBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, }, }, }), diff --git a/test/optimism/bridging.e2e.test.ts b/test/optimism/bridging.e2e.test.ts index d578e860..c045267e 100644 --- a/test/optimism/bridging.e2e.test.ts +++ b/test/optimism/bridging.e2e.test.ts @@ -1,6 +1,5 @@ import { CrossChainMessenger, - DAIBridgeAdapter, MessageStatus, } from "@eth-optimism/sdk"; import { assert } from "chai"; @@ -13,6 +12,7 @@ import optimism from "../../utils/optimism"; import { ERC20Mintable } from "../../typechain"; import { scenario } from "../../utils/testing"; import { sleep } from "../../utils/testing/e2e"; +import { LidoBridgeAdapter } from "../../utils/optimism/LidoBridgeAdapter"; let depositTokensTxResponse: TransactionResponse; let withdrawTokensTxResponse: TransactionResponse; @@ -38,7 +38,7 @@ scenario("Optimism :: Bridging via deposit/withdraw E2E test", ctxFactory) } ) - .step("Set allowance for L1ERC20TokenBridge to deposit", async (ctx) => { + .step("Set allowance for L1LidoTokensBridge to deposit", async (ctx) => { const allowanceTxResponse = await ctx.crossChainMessenger.approveERC20( ctx.l1Token.address, ctx.l2Token.address, @@ -50,7 +50,7 @@ scenario("Optimism :: Bridging via deposit/withdraw E2E test", ctxFactory) assert.equalBN( await ctx.l1Token.allowance( ctx.l1Tester.address, - ctx.l1ERC20TokenBridge.address + ctx.l1LidoTokensBridge.address ), ctx.depositAmount ); @@ -134,7 +134,7 @@ async function ctxFactory() { l1Tester: testingSetup.l1Tester, l1Token: testingSetup.l1Token, l2Token: testingSetup.l2Token, - l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, + l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, crossChainMessenger: new CrossChainMessenger({ l2ChainId: network.chainId("opt", networkName), l1ChainId: network.chainId("eth", networkName), @@ -142,9 +142,9 @@ async function ctxFactory() { l2SignerOrProvider: testingSetup.l2Tester, bridges: { LidoBridge: { - Adapter: DAIBridgeAdapter, - l1Bridge: testingSetup.l1ERC20TokenBridge.address, - l2Bridge: testingSetup.l2ERC20TokenBridge.address, + Adapter: LidoBridgeAdapter, + l1Bridge: testingSetup.l1LidoTokensBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, }, }, }), diff --git a/test/optimism/bridging.integration.test.ts b/test/optimism/bridging.integration.test.ts index 23eb66f6..9ec87c7d 100644 --- a/test/optimism/bridging.integration.test.ts +++ b/test/optimism/bridging.integration.test.ts @@ -4,94 +4,97 @@ import env from "../../utils/env"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; import testing, { scenario } from "../../utils/testing"; +import { ethers } from "hardhat"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ERC20WrapperStub } from "../../typechain"; -scenario("Optimism :: Bridging integration test", ctxFactory) +scenario("Optimism :: Bridging non-rebasable token integration test", ctxFactory) .after(async (ctx) => { await ctx.l1Provider.send("evm_revert", [ctx.snapshot.l1]); await ctx.l2Provider.send("evm_revert", [ctx.snapshot.l2]); }) .step("Activate bridging on L1", async (ctx) => { - const { l1ERC20TokenBridge } = ctx; - const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + const { l1LidoTokensBridge } = ctx; + const { l1ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; - const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { - await l1ERC20TokenBridge - .connect(l1ERC20TokenBridgeAdmin) + await l1LidoTokensBridge + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableDeposits(); } else { console.log("L1 deposits already enabled"); } const isWithdrawalsEnabled = - await l1ERC20TokenBridge.isWithdrawalsEnabled(); + await l1LidoTokensBridge.isWithdrawalsEnabled(); if (!isWithdrawalsEnabled) { - await l1ERC20TokenBridge - .connect(l1ERC20TokenBridgeAdmin) + await l1LidoTokensBridge + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableWithdrawals(); } else { console.log("L1 withdrawals already enabled"); } - assert.isTrue(await l1ERC20TokenBridge.isDepositsEnabled()); - assert.isTrue(await l1ERC20TokenBridge.isWithdrawalsEnabled()); + assert.isTrue(await l1LidoTokensBridge.isDepositsEnabled()); + assert.isTrue(await l1LidoTokensBridge.isWithdrawalsEnabled()); }) .step("Activate bridging on L2", async (ctx) => { - const { l2ERC20TokenBridge } = ctx; - const { l2ERC20TokenBridgeAdmin } = ctx.accounts; + const { l2ERC20ExtendedTokensBridge } = ctx; + const { l2ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; - const isDepositsEnabled = await l2ERC20TokenBridge.isDepositsEnabled(); + const isDepositsEnabled = await l2ERC20ExtendedTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { - await l2ERC20TokenBridge - .connect(l2ERC20TokenBridgeAdmin) + await l2ERC20ExtendedTokensBridge + .connect(l2ERC20ExtendedTokensBridgeAdmin) .enableDeposits(); } else { console.log("L2 deposits already enabled"); } const isWithdrawalsEnabled = - await l2ERC20TokenBridge.isWithdrawalsEnabled(); + await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled(); if (!isWithdrawalsEnabled) { - await l2ERC20TokenBridge - .connect(l2ERC20TokenBridgeAdmin) + await l2ERC20ExtendedTokensBridge + .connect(l2ERC20ExtendedTokensBridgeAdmin) .enableWithdrawals(); } else { console.log("L2 withdrawals already enabled"); } - assert.isTrue(await l2ERC20TokenBridge.isDepositsEnabled()); - assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + assert.isTrue(await l2ERC20ExtendedTokensBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled()); }) .step("L1 -> L2 deposit via depositERC20() method", async (ctx) => { const { l1Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2Token, l1CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmount } = ctx.common; await l1Token .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, depositAmount); + .approve(l1LidoTokensBridge.address, depositAmount); const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1Token.address, @@ -101,16 +104,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + const dataToSend = await packedTokenRateAndTimestamp(ctx.l1Provider, l1Token); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1Token.address, l2Token.address, tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x", + dataToSend, ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1Token.address, @@ -118,23 +123,23 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x", + dataToSend, ] ); const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmount) ); assert.equalBN( @@ -147,9 +152,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l2Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { depositAmount } = ctx.common; const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = @@ -159,27 +164,28 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); const l2TokenTotalSupplyBefore = await l2Token.totalSupply(); + const dataToReceive = await packedTokenRateAndTimestamp(ctx.l2Provider, l1Token); const tx = await l2CrossDomainMessenger .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, - l2ERC20TokenBridge.address, + l1LidoTokensBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1Token.address, l2Token.address, tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x", + dataToReceive, ]), { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -200,18 +206,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { const { accountA: tokenHolderA } = ctx.accounts; const { withdrawalAmount } = ctx.common; - const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; + const { l1Token, l2Token, l2ERC20ExtendedTokensBridge } = ctx; const tokenHolderABalanceBefore = await l2Token.balanceOf( tokenHolderA.address ); const l2TotalSupplyBefore = await l2Token.totalSupply(); - const tx = await l2ERC20TokenBridge + const tx = await l2ERC20ExtendedTokensBridge .connect(tokenHolderA.l2Signer) .withdraw(l2Token.address, withdrawalAmount, 0, "0x"); - await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "WithdrawalInitiated", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -233,10 +239,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1CrossDomainMessenger, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2Token, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; const { withdrawalAmount } = ctx.common; @@ -244,20 +250,20 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address ); await l1CrossDomainMessenger .connect(l1Stranger) - .setXDomainMessageSender(l2ERC20TokenBridge.address); + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); const tx = await l1CrossDomainMessenger .connect(l1Stranger) .relayMessage( - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2CrossDomainMessenger.address, - l1ERC20TokenBridge.interface.encodeFunctionData( + l1LidoTokensBridge.interface.encodeFunctionData( "finalizeERC20Withdrawal", [ l1Token.address, @@ -271,7 +277,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 0 ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20WithdrawalFinalized", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -281,8 +287,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmount) ); assert.equalBN( @@ -295,8 +301,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l2Token, - l1ERC20TokenBridge, - l2ERC20TokenBridge, + l1LidoTokensBridge, + l2ERC20ExtendedTokensBridge, l1CrossDomainMessenger, } = ctx; const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; @@ -306,16 +312,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await l1Token .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, depositAmount); + .approve(l1LidoTokensBridge.address, depositAmount); const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20To( l1Token.address, @@ -326,16 +332,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + const dataToSend = await packedTokenRateAndTimestamp(ctx.l1Provider, l1Token); + + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1Token.address, l2Token.address, tokenHolderA.address, tokenHolderB.address, depositAmount, - "0x", + dataToSend, ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1Token.address, @@ -343,23 +351,23 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderB.address, depositAmount, - "0x", + dataToSend, ] ); const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmount) ); assert.equalBN( @@ -371,10 +379,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Finalize deposit on L2", async (ctx) => { const { l1Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2Token, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, @@ -388,26 +396,28 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderB.address ); + const dataToReceive = await packedTokenRateAndTimestamp(ctx.l2Provider, l1Token); + const tx = await l2CrossDomainMessenger .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, - l2ERC20TokenBridge.address, + l1LidoTokensBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1Token.address, l2Token.address, tokenHolderA.address, tokenHolderB.address, depositAmount, - "0x", + dataToReceive, ]), { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -427,7 +437,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .step("L2 -> L1 withdrawal via withdrawTo()", async (ctx) => { - const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; + const { l1Token, l2Token, l2ERC20ExtendedTokensBridge } = ctx; const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; const { withdrawalAmount } = ctx.common; @@ -436,7 +446,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); const l2TotalSupplyBefore = await l2Token.totalSupply(); - const tx = await l2ERC20TokenBridge + const tx = await l2ERC20ExtendedTokensBridge .connect(tokenHolderB.l2Signer) .withdrawTo( l2Token.address, @@ -446,7 +456,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "WithdrawalInitiated", [ l1Token.address, l2Token.address, tokenHolderB.address, @@ -470,10 +480,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1CrossDomainMessenger, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2Token, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, @@ -485,20 +495,20 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( + l1LidoTokensBridge.address ); await l1CrossDomainMessenger .connect(l1Stranger) - .setXDomainMessageSender(l2ERC20TokenBridge.address); + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); const tx = await l1CrossDomainMessenger .connect(l1Stranger) .relayMessage( - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2CrossDomainMessenger.address, - l1ERC20TokenBridge.interface.encodeFunctionData( + l1LidoTokensBridge.interface.encodeFunctionData( "finalizeERC20Withdrawal", [ l1Token.address, @@ -512,7 +522,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 0 ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20WithdrawalFinalized", [ l1Token.address, l2Token.address, tokenHolderB.address, @@ -522,8 +532,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + await l1Token.balanceOf(l1LidoTokensBridge.address), + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmount) ); assert.equalBN( @@ -540,8 +550,8 @@ async function ctxFactory() { const { l1Provider, l2Provider, - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, ...contracts } = await optimism.testing(networkName).getIntegrationTestSetup(); @@ -563,13 +573,13 @@ async function ctxFactory() { ); await testing.setBalance( - await l1ERC20TokenBridgeAdmin.getAddress(), + await l1ERC20ExtendedTokensBridgeAdmin.getAddress(), wei.toBigNumber(wei`1 ether`), l1Provider ); await testing.setBalance( - await l2ERC20TokenBridgeAdmin.getAddress(), + await l2ERC20ExtendedTokensBridgeAdmin.getAddress(), wei.toBigNumber(wei`1 ether`), l2Provider ); @@ -597,8 +607,8 @@ async function ctxFactory() { accountA, accountB, l1Stranger: testing.accounts.stranger(l1Provider), - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, l1CrossDomainMessengerAliased, }, common: { @@ -611,3 +621,12 @@ async function ctxFactory() { }, }; } + +async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapperStub) { + const stEthPerToken = await l1Token.stEthPerToken(); + const blockNumber = await l1Provider.getBlockNumber(); + const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + const stEthPerTokenStr = ethers.utils.hexZeroPad(stEthPerToken.toHexString(), 12); + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); +} diff --git a/test/optimism/deployment.acceptance.test.ts b/test/optimism/deployment.acceptance.test.ts index be49d3c5..ec30d93f 100644 --- a/test/optimism/deployment.acceptance.test.ts +++ b/test/optimism/deployment.acceptance.test.ts @@ -13,55 +13,55 @@ import { wei } from "../../utils/wei"; scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) .step("L1 Bridge :: proxy admin", async (ctx) => { assert.equal( - await ctx.l1ERC20TokenBridgeProxy.proxy__getAdmin(), + await ctx.l1LidoTokensBridgeProxy.proxy__getAdmin(), ctx.deployment.l1.proxyAdmin ); }) .step("L1 Bridge :: bridge admin", async (ctx) => { const currentAdmins = await getRoleHolders( - ctx.l1ERC20TokenBridge, + ctx.l1LidoTokensBridge, BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash ); assert.equal(currentAdmins.size, 1); assert.isTrue(currentAdmins.has(ctx.deployment.l1.bridgeAdmin)); await assert.isTrue( - await ctx.l1ERC20TokenBridge.hasRole( + await ctx.l1LidoTokensBridge.hasRole( BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash, ctx.deployment.l1.bridgeAdmin ) ); }) .step("L1 bridge :: L1 token", async (ctx) => { - assert.equal(await ctx.l1ERC20TokenBridge.l1Token(), ctx.deployment.token); + assert.equal(await ctx.l1LidoTokensBridge.L1_TOKEN_NON_REBASABLE(), ctx.deployment.token); }) .step("L1 bridge :: L2 token", async (ctx) => { assert.equal( - await ctx.l1ERC20TokenBridge.l2Token(), + await ctx.l1LidoTokensBridge.L2_TOKEN_NON_REBASABLE(), ctx.erc20Bridged.address ); }) .step("L1 bridge :: L2 token bridge", async (ctx) => { assert.equal( - await ctx.l1ERC20TokenBridge.l2TokenBridge(), - ctx.l2ERC20TokenBridge.address + await ctx.l1LidoTokensBridge.l2TokenBridge(), + ctx.l2ERC20ExtendedTokensBridge.address ); }) .step("L1 Bridge :: is deposits enabled", async (ctx) => { assert.equal( - await ctx.l1ERC20TokenBridge.isDepositsEnabled(), + await ctx.l1LidoTokensBridge.isDepositsEnabled(), ctx.deployment.l1.depositsEnabled ); }) .step("L1 Bridge :: is withdrawals enabled", async (ctx) => { assert.equal( - await ctx.l1ERC20TokenBridge.isWithdrawalsEnabled(), + await ctx.l1LidoTokensBridge.isWithdrawalsEnabled(), ctx.deployment.l1.withdrawalsEnabled ); }) .step("L1 Bridge :: deposits enablers", async (ctx) => { const actualDepositsEnablers = await getRoleHolders( - ctx.l1ERC20TokenBridge, + ctx.l1LidoTokensBridge, BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash ); const expectedDepositsEnablers = ctx.deployment.l1.depositsEnablers || []; @@ -73,7 +73,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) }) .step("L1 Bridge :: deposits disablers", async (ctx) => { const actualDepositsDisablers = await getRoleHolders( - ctx.l1ERC20TokenBridge, + ctx.l1LidoTokensBridge, BridgingManagerRole.DEPOSITS_DISABLER_ROLE.hash ); const expectedDepositsDisablers = ctx.deployment.l1.depositsDisablers || []; @@ -87,7 +87,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) }) .step("L1 Bridge :: withdrawals enablers", async (ctx) => { const actualWithdrawalsEnablers = await getRoleHolders( - ctx.l1ERC20TokenBridge, + ctx.l1LidoTokensBridge, BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash ); const expectedWithdrawalsEnablers = @@ -103,7 +103,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) }) .step("L1 Bridge :: withdrawals disablers", async (ctx) => { const actualWithdrawalsDisablers = await getRoleHolders( - ctx.l1ERC20TokenBridge, + ctx.l1LidoTokensBridge, BridgingManagerRole.WITHDRAWALS_DISABLER_ROLE.hash ); const expectedWithdrawalsDisablers = @@ -122,55 +122,55 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) .step("L2 Bridge :: proxy admin", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridgeProxy.proxy__getAdmin(), + await ctx.l2ERC20ExtendedTokensBridgeProxy.proxy__getAdmin(), ctx.deployment.l2.proxyAdmin ); }) .step("L2 Bridge :: bridge admin", async (ctx) => { const currentAdmins = await getRoleHolders( - ctx.l2ERC20TokenBridge, + ctx.l2ERC20ExtendedTokensBridge, BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash ); assert.equal(currentAdmins.size, 1); assert.isTrue(currentAdmins.has(ctx.deployment.l2.bridgeAdmin)); await assert.isTrue( - await ctx.l2ERC20TokenBridge.hasRole( + await ctx.l2ERC20ExtendedTokensBridge.hasRole( BridgingManagerRole.DEFAULT_ADMIN_ROLE.hash, ctx.deployment.l2.bridgeAdmin ) ); }) .step("L2 bridge :: L1 token", async (ctx) => { - assert.equal(await ctx.l2ERC20TokenBridge.l1Token(), ctx.deployment.token); + assert.equal(await ctx.l2ERC20ExtendedTokensBridge.L1_TOKEN_NON_REBASABLE(), ctx.deployment.token); }) .step("L2 bridge :: L2 token", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridge.l2Token(), + await ctx.l2ERC20ExtendedTokensBridge.L2_TOKEN_NON_REBASABLE(), ctx.erc20Bridged.address ); }) .step("L2 bridge :: L1 token bridge", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridge.l1TokenBridge(), - ctx.l1ERC20TokenBridge.address + await ctx.l2ERC20ExtendedTokensBridge.l1TokenBridge(), + ctx.l1LidoTokensBridge.address ); }) .step("L2 Bridge :: is deposits enabled", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridge.isDepositsEnabled(), + await ctx.l2ERC20ExtendedTokensBridge.isDepositsEnabled(), ctx.deployment.l2.depositsEnabled ); }) .step("L2 Bridge :: is withdrawals enabled", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridge.isWithdrawalsEnabled(), + await ctx.l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled(), ctx.deployment.l2.withdrawalsEnabled ); }) .step("L2 Bridge :: deposits enablers", async (ctx) => { const actualDepositsEnablers = await getRoleHolders( - ctx.l2ERC20TokenBridge, + ctx.l2ERC20ExtendedTokensBridge, BridgingManagerRole.DEPOSITS_ENABLER_ROLE.hash ); const expectedDepositsEnablers = ctx.deployment.l2.depositsEnablers || []; @@ -182,7 +182,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) }) .step("L2 Bridge :: deposits disablers", async (ctx) => { const actualDepositsDisablers = await getRoleHolders( - ctx.l2ERC20TokenBridge, + ctx.l2ERC20ExtendedTokensBridge, BridgingManagerRole.DEPOSITS_DISABLER_ROLE.hash ); const expectedDepositsDisablers = ctx.deployment.l2.depositsDisablers || []; @@ -197,7 +197,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) }) .step("L2 Bridge :: withdrawals enablers", async (ctx) => { const actualWithdrawalsEnablers = await getRoleHolders( - ctx.l2ERC20TokenBridge, + ctx.l2ERC20ExtendedTokensBridge, BridgingManagerRole.WITHDRAWALS_ENABLER_ROLE.hash ); const expectedWithdrawalsEnablers = @@ -213,7 +213,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) }) .step("L2 Bridge :: withdrawals disablers", async (ctx) => { const actualWithdrawalsDisablers = await getRoleHolders( - ctx.l2ERC20TokenBridge, + ctx.l2ERC20ExtendedTokensBridge, BridgingManagerRole.WITHDRAWALS_DISABLER_ROLE.hash ); const expectedWithdrawalsDisablers = @@ -251,7 +251,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) .step("L2 token :: bridge", async (ctx) => { assert.equalBN( await ctx.erc20Bridged.bridge(), - ctx.l2ERC20TokenBridge.address + ctx.l2ERC20ExtendedTokensBridge.address ); }) @@ -282,14 +282,14 @@ async function ctxFactory() { symbol, decimals, }, - l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, - l1ERC20TokenBridgeProxy: OssifiableProxy__factory.connect( - testingSetup.l1ERC20TokenBridge.address, + l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, + l1LidoTokensBridgeProxy: OssifiableProxy__factory.connect( + testingSetup.l1LidoTokensBridge.address, testingSetup.l1Provider ), - l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, - l2ERC20TokenBridgeProxy: OssifiableProxy__factory.connect( - testingSetup.l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge: testingSetup.l2ERC20ExtendedTokensBridge, + l2ERC20ExtendedTokensBridgeProxy: OssifiableProxy__factory.connect( + testingSetup.l2ERC20ExtendedTokensBridge.address, testingSetup.l2Provider ), erc20Bridged: testingSetup.l2Token, diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts new file mode 100644 index 00000000..e6d4857e --- /dev/null +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -0,0 +1,217 @@ +import { assert } from "chai"; + +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import optimism from "../../utils/optimism"; +import testing, { scenario } from "../../utils/testing"; + +scenario("Optimism :: Bridging integration test", ctxFactory) + .after(async (ctx) => { + await ctx.l1Provider.send("evm_revert", [ctx.snapshot.l1]); + await ctx.l2Provider.send("evm_revert", [ctx.snapshot.l2]); + }) + + .step("Activate bridging on L1", async (ctx) => { + const { l1LidoTokensBridge } = ctx; + const { l1ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l1LidoTokensBridge + .connect(l1ERC20ExtendedTokensBridgeAdmin) + .enableDeposits(); + } else { + console.log("L1 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l1LidoTokensBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l1LidoTokensBridge + .connect(l1ERC20ExtendedTokensBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L1 withdrawals already enabled"); + } + + assert.isTrue(await l1LidoTokensBridge.isDepositsEnabled()); + assert.isTrue(await l1LidoTokensBridge.isWithdrawalsEnabled()); + }) + + .step("Activate bridging on L2", async (ctx) => { + const { l2ERC20ExtendedTokensBridge } = ctx; + const { l2ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l2ERC20ExtendedTokensBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l2ERC20ExtendedTokensBridge + .connect(l2ERC20ExtendedTokensBridgeAdmin) + .enableDeposits(); + } else { + console.log("L2 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l2ERC20ExtendedTokensBridge + .connect(l2ERC20ExtendedTokensBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L2 withdrawals already enabled"); + } + + assert.isTrue(await l2ERC20ExtendedTokensBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20ExtendedTokensBridge.isWithdrawalsEnabled()); + }) + + .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { + const { + l1Token, + l2Token, + l1TokenRebasable, + l1LidoTokensBridge, + l2TokenRebasable + } = ctx; + + const { accountA: tokenHolderA } = ctx.accounts; + const stEthPerToken = await l1Token.stEthPerToken(); + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1LidoTokensBridge.address, 10); + + await l1Token + .connect(tokenHolderA.l1Signer) + .approve(l1LidoTokensBridge.address, 10); + + const tokenHolderABalanceBefore = await l1Token.balanceOf( + tokenHolderA.address + ); + console.log("tokenHolderABalanceBefore=",tokenHolderABalanceBefore); + + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1LidoTokensBridge.address + ); + + const tx0 = await l1LidoTokensBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1Token.address, + l2Token.address, + 10, + 200_000, + "0x" + ); + + const receipt0 = await tx0.wait(); + console.log("l1Token gasUsed=",receipt0.gasUsed); + + const tx1 = await l1LidoTokensBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + 10, + 200_000, + "0x" + ); + + const receipt1 = await tx1.wait(); + console.log("l1TokenRebasable gasUsed=",receipt1.gasUsed); + + const gasDifference = receipt1.gasUsed.sub(receipt0.gasUsed); + console.log("gasUsed difference=", gasDifference); + }) + + + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); + console.log("networkName=",networkName); + + const { + l1Provider, + l2Provider, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, + ...contracts + } = await optimism.testing(networkName).getIntegrationTestSetup(); + + const l1Snapshot = await l1Provider.send("evm_snapshot", []); + const l2Snapshot = await l2Provider.send("evm_snapshot", []); + + const accountA = testing.accounts.accountA(l1Provider, l2Provider); + const accountB = testing.accounts.accountB(l1Provider, l2Provider); + + const depositAmount = wei`0.15 ether`; + const withdrawalAmount = wei`0.05 ether`; + + await testing.setBalance( + await contracts.l1TokensHolder.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l1ERC20ExtendedTokensBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l2ERC20ExtendedTokensBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + await contracts.l1Token + .connect(contracts.l1TokensHolder) + .transfer(accountA.l1Signer.address, depositAmount); + + await contracts.l1TokenRebasable + .connect(contracts.l1TokensHolder) + .transfer(accountA.l1Signer.address, wei.toBigNumber(depositAmount).mul(2)); + + const l1CrossDomainMessengerAliased = await testing.impersonate( + testing.accounts.applyL1ToL2Alias(contracts.l1CrossDomainMessenger.address), + l2Provider + ); + + console.log("l1CrossDomainMessengerAliased=",l1CrossDomainMessengerAliased); + console.log("contracts.l1CrossDomainMessenger.address=",contracts.l1CrossDomainMessenger.address); + + await testing.setBalance( + await l1CrossDomainMessengerAliased.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + return { + l1Provider, + l2Provider, + ...contracts, + accounts: { + accountA, + accountB, + l1Stranger: testing.accounts.stranger(l1Provider), + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, + l1CrossDomainMessengerAliased, + }, + common: { + depositAmount, + withdrawalAmount, + }, + snapshot: { + l1: l1Snapshot, + l2: l2Snapshot, + }, + }; +} diff --git a/test/optimism/managing-deposits.e2e.test.ts b/test/optimism/managing-deposits.e2e.test.ts index 1566d551..36712d50 100644 --- a/test/optimism/managing-deposits.e2e.test.ts +++ b/test/optimism/managing-deposits.e2e.test.ts @@ -2,7 +2,7 @@ import { assert } from "chai"; import { TransactionResponse } from "@ethersproject/providers"; import { - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, GovBridgeExecutor__factory, } from "../../typechain"; import { @@ -33,27 +33,27 @@ const scenarioTest = scenario( assert.gte(await l1LDOHolder.getBalance(), gasAmount); }) - .step("Checking deposits status", async ({ l2ERC20TokenBridge }) => { - l2DepositsInitialState = await l2ERC20TokenBridge.isDepositsEnabled(); + .step("Checking deposits status", async ({ l2ERC20ExtendedTokensBridge }) => { + l2DepositsInitialState = await l2ERC20ExtendedTokensBridge.isDepositsEnabled(); }) .step(`Starting DAO vote`, async (ctx) => { const grantRoleCalldata = - ctx.l2ERC20TokenBridge.interface.encodeFunctionData("grantRole", [ + ctx.l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("grantRole", [ l2DepositsInitialState ? DEPOSIT_DISABLER_ROLE : DEPOSIT_ENABLER_ROLE, ctx.govBridgeExecutor.address, ]); const grantRoleData = "0x" + grantRoleCalldata.substring(10); const actionCalldata = l2DepositsInitialState - ? ctx.l2ERC20TokenBridge.interface.encodeFunctionData("disableDeposits") - : ctx.l2ERC20TokenBridge.interface.encodeFunctionData("enableDeposits"); + ? ctx.l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("disableDeposits") + : ctx.l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("enableDeposits"); const actionData = "0x" + actionCalldata.substring(10); const executorCalldata = await ctx.govBridgeExecutor.interface.encodeFunctionData("queue", [ - [ctx.l2ERC20TokenBridge.address, ctx.l2ERC20TokenBridge.address], + [ctx.l2ERC20ExtendedTokensBridge.address, ctx.l2ERC20ExtendedTokensBridge.address], [0, 0], [ "grantRole(bytes32,address)", @@ -124,9 +124,9 @@ const scenarioTest = scenario( await tx.wait(); }) - .step("Checking deposits state", async ({ l2ERC20TokenBridge }) => { + .step("Checking deposits state", async ({ l2ERC20ExtendedTokensBridge }) => { assert.equal( - await l2ERC20TokenBridge.isDepositsEnabled(), + await l2ERC20ExtendedTokensBridge.isDepositsEnabled(), !l2DepositsInitialState ); }); @@ -158,8 +158,8 @@ async function ctxFactory() { l1Tester, l2Tester, l1LDOHolder, - l2ERC20TokenBridge: L2ERC20TokenBridge__factory.connect( - E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge: L2ERC20ExtendedTokensBridge__factory.connect( + E2E_TEST_CONTRACTS.l2.l2ERC20ExtendedTokensBridge, l2Tester ), govBridgeExecutor: GovBridgeExecutor__factory.connect( diff --git a/test/optimism/managing-executor.e2e.test.ts b/test/optimism/managing-executor.e2e.test.ts index 340a050e..f48112e9 100644 --- a/test/optimism/managing-executor.e2e.test.ts +++ b/test/optimism/managing-executor.e2e.test.ts @@ -2,7 +2,7 @@ import { assert } from "chai"; import { TransactionResponse } from "@ethersproject/providers"; import { - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, GovBridgeExecutor__factory, } from "../../typechain"; import { @@ -134,8 +134,8 @@ async function ctxFactory() { gasAmount: wei`0.1 ether`, l2Tester, l1LDOHolder, - l2ERC20TokenBridge: L2ERC20TokenBridge__factory.connect( - E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge: L2ERC20ExtendedTokensBridge__factory.connect( + E2E_TEST_CONTRACTS.l2.l2ERC20ExtendedTokensBridge, l2Tester ), govBridgeExecutor: GovBridgeExecutor__factory.connect( diff --git a/test/optimism/managing-proxy.e2e.test.ts b/test/optimism/managing-proxy.e2e.test.ts index 632de88c..20ff14af 100644 --- a/test/optimism/managing-proxy.e2e.test.ts +++ b/test/optimism/managing-proxy.e2e.test.ts @@ -5,7 +5,7 @@ import { ERC20Bridged__factory, GovBridgeExecutor__factory, OssifiableProxy__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, } from "../../typechain"; import { E2E_TEST_CONTRACTS_OPTIMISM as E2E_TEST_CONTRACTS } from "../../utils/testing/e2e"; import env from "../../utils/env"; @@ -32,7 +32,7 @@ scenario( .step("Proxy upgrade: send crosschain message", async (ctx) => { const implBefore = await await ctx.proxyToOssify.proxy__getImplementation(); - assert.equal(implBefore, ctx.l2ERC20TokenBridge.address); + assert.equal(implBefore, ctx.l2ERC20ExtendedTokensBridge.address); const executorCalldata = await ctx.govBridgeExecutor.interface.encodeFunctionData("queue", [ [ctx.proxyToOssify.address], @@ -204,8 +204,8 @@ async function ctxFactory() { E2E_TEST_CONTRACTS.l2.l2Token, l2Tester ), - l2ERC20TokenBridge: L2ERC20TokenBridge__factory.connect( - E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge: L2ERC20ExtendedTokensBridge__factory.connect( + E2E_TEST_CONTRACTS.l2.l2ERC20ExtendedTokensBridge, l2Tester ), govBridgeExecutor: GovBridgeExecutor__factory.connect( @@ -213,7 +213,7 @@ async function ctxFactory() { l2Tester ), proxyToOssify: await new OssifiableProxy__factory(l2Tester).deploy( - E2E_TEST_CONTRACTS.l2.l2ERC20TokenBridge, + E2E_TEST_CONTRACTS.l2.l2ERC20ExtendedTokensBridge, E2E_TEST_CONTRACTS.l2.govBridgeExecutor, "0x" ), diff --git a/test/optimism/pushingTokenRate.e2e.test.ts b/test/optimism/pushingTokenRate.e2e.test.ts new file mode 100644 index 00000000..06fb2fb5 --- /dev/null +++ b/test/optimism/pushingTokenRate.e2e.test.ts @@ -0,0 +1,96 @@ +import { assert } from "chai"; +import env from "../../utils/env"; +import network, { SignerOrProvider } from "../../utils/network"; +import testingUtils, { scenario } from "../../utils/testing"; +import { + ERC20WrapperStub__factory, + TokenRateNotifier__factory, + TokenRateOracle__factory +} from "../../typechain"; + +scenario("Optimism :: Push token rate to Oracle E2E test", ctxFactory) + + .step("Push Token Rate", async (ctx) => { + await ctx.tokenRateNotifier + .connect(ctx.l1Tester) + .handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7); + }) + + .step("Receive token rate", async (ctx) => { + const tokenRate = await ctx.l1Token.stEthPerToken(); + + const answer = await ctx.tokenRateOracle.latestAnswer(); + assert.equalBN(answer, tokenRate); + + const [ + , + latestRoundDataAnswer, + , + , + ] = await ctx.tokenRateOracle.latestRoundData(); + assert.equalBN(latestRoundDataAnswer, tokenRate); + }) + + .run(); + +async function ctxFactory() { + const testingSetup = await getE2ETestSetup(); + + return { + l1Tester: testingSetup.l1Tester, + l2Tester: testingSetup.l2Tester, + l1Provider: testingSetup.l1Provider, + l2Provider: testingSetup.l2Provider, + l1Token: testingSetup.l1Token, + tokenRateNotifier: testingSetup.tokenRateNotifier, + tokenRateOracle: testingSetup.tokenRateOracle + }; +} + +async function getE2ETestSetup() { + const testerPrivateKey = testingUtils.env.TESTING_PRIVATE_KEY(); + const networkName = env.network("TESTING_OPT_NETWORK", "sepolia"); + + const ethOptNetworks = network.multichain(["eth", "opt"], networkName); + + const [ethProvider, optProvider] = ethOptNetworks.getProviders({ + forking: false, + }); + const [l1Tester, l2Tester] = ethOptNetworks.getSigners(testerPrivateKey, { + forking: false, + }); + + const contracts = await loadDeployedContracts(l1Tester, l2Tester); + + // await printLoadedTestConfig(networkName, bridgeContracts, l1Tester); + + return { + l1Tester, + l2Tester, + l1Provider: ethProvider, + l2Provider: optProvider, + ...contracts, + }; +} + +async function loadDeployedContracts( + l1SignerOrProvider: SignerOrProvider, + l2SignerOrProvider: SignerOrProvider +) { + return { + l1Token: ERC20WrapperStub__factory.connect( + testingUtils.env.OPT_L1_TOKEN(), + l1SignerOrProvider + ), + tokenRateNotifier: TokenRateNotifier__factory.connect( + testingUtils.env.OPT_L1_TOKEN_RATE_NOTIFIER(), + l1SignerOrProvider + ), + tokenRateOracle: TokenRateOracle__factory.connect( + testingUtils.env.OPT_L2_TOKEN_RATE_ORACLE(), + l2SignerOrProvider + ), + l1SignerOrProvider, + l2SignerOrProvider + }; +} diff --git a/test/optimism/pushingTokenRate.integration.test.ts b/test/optimism/pushingTokenRate.integration.test.ts new file mode 100644 index 00000000..f5d9606c --- /dev/null +++ b/test/optimism/pushingTokenRate.integration.test.ts @@ -0,0 +1,217 @@ +import { assert } from "chai"; +import { ethers } from "hardhat"; +import env from "../../utils/env"; +import { wei } from "../../utils/wei"; +import optimism from "../../utils/optimism"; +import network from "../../utils/network"; +import testing, { scenario } from "../../utils/testing"; +import deploymentOracle from "../../utils/optimism/deploymentOracle"; +import { getBridgeExecutorParams } from "../../utils/bridge-executor"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { BigNumber } from "ethers"; +import { + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + OptimismBridgeExecutor__factory, + TokenRateNotifier__factory, + TokenRateOracle__factory +} from "../../typechain"; + +scenario("Optimism :: Token Rate Oracle integration test", ctxFactory) + + .step("Push Token Rate", async (ctx) => { + const { + tokenRateNotifier, + tokenRateOracle, + opTokenRatePusher, + l1CrossDomainMessenger, + l1Token, + l1Provider + } = ctx; + + const tokenRate = await l1Token.stEthPerToken(); + + const account = ctx.accounts.accountA; + + const tx = await tokenRateNotifier + .connect(account.l1Signer) + .handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + const [stEthPerTokenStr, blockTimestampStr] = await tokenRateAndTimestamp(l1Provider, tokenRate); + const l2Calldata = tokenRateOracle.interface.encodeFunctionData( + "updateRate", + [ + stEthPerTokenStr, + blockTimestampStr + ] + ); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + tokenRateOracle.address, + opTokenRatePusher, + l2Calldata, + messageNonce, + 1000, + ]); + }) + + .step("Finalize pushing rate", async (ctx) => { + const { + opTokenRatePusher, + tokenRateOracle, + l1Token, + l1Provider, + l1CrossDomainMessenger + } = ctx; + + const account = ctx.accounts.accountA; + await l1CrossDomainMessenger + .connect(account.l1Signer) + .setXDomainMessageSender(opTokenRatePusher); + + const tokenRate = await l1Token.stEthPerToken(); + const [stEthPerTokenStr, blockTimestampStr] = await tokenRateAndTimestamp(l1Provider, tokenRate); + + const tx = await ctx.l2CrossDomainMessenger + .connect(ctx.accounts.l1CrossDomainMessengerAliased) + .relayMessage( + 1, + opTokenRatePusher, + tokenRateOracle.address, + 0, + 300_000, + tokenRateOracle.interface.encodeFunctionData("updateRate", [ + stEthPerTokenStr, + blockTimestampStr + ]), + { gasLimit: 5_000_000 } + ); + + const answer = await tokenRateOracle.latestAnswer(); + assert.equalBN(answer, tokenRate); + + const [ + , + tokenRateAnswer, + , + updatedAt, + + ] = await tokenRateOracle.latestRoundData(); + + assert.equalBN(tokenRateAnswer, tokenRate); + assert.equalBN(updatedAt, blockTimestampStr); + }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); + const [l1Provider, l2Provider] = network + .multichain(["eth", "opt"], networkName) + .getProviders({ forking: true }); + const l1Deployer = testing.accounts.deployer(l1Provider); + const l2Deployer = testing.accounts.deployer(l2Provider); + + const optContracts = optimism.contracts(networkName, { forking: true }); + const l2CrossDomainMessenger = optContracts.L2CrossDomainMessenger; + const testingOnDeployedContracts = testing.env.USE_DEPLOYED_CONTRACTS(false); + const optAddresses = optimism.addresses(networkName); + + const govBridgeExecutor = testingOnDeployedContracts + ? OptimismBridgeExecutor__factory.connect( + testing.env.OPT_GOV_BRIDGE_EXECUTOR(), + l2Provider + ) + : await new OptimismBridgeExecutor__factory(l2Deployer).deploy( + optAddresses.L2CrossDomainMessenger, + l1Deployer.address, + ...getBridgeExecutorParams(), + l2Deployer.address + ); + + const l1TokenRebasable = await new ERC20BridgedStub__factory(l1Deployer).deploy( + "Test Token Rebasable", + "TTR" + ); + const l1Token = await new ERC20WrapperStub__factory(l1Deployer).deploy( + l1TokenRebasable.address, + "Test Token", + "TT" + ); + const [ethDeployScript, optDeployScript] = await deploymentOracle( + networkName + ).oracleDeployScript( + l1Token.address, + 1000, + 86400, + { + deployer: l1Deployer, + admins: { + proxy: l1Deployer.address, + bridge: l1Deployer.address + }, + contractsShift: 0 + }, + { + deployer: l2Deployer, + admins: { + proxy: govBridgeExecutor.address, + bridge: govBridgeExecutor.address, + }, + contractsShift: 0 + } + ); + + await ethDeployScript.run(); + await optDeployScript.run(); + + await optimism.testing(networkName).stubL1CrossChainMessengerContract(); + + const l1CrossDomainMessengerAliased = await testing.impersonate( + testing.accounts.applyL1ToL2Alias(optContracts.L1CrossDomainMessengerStub.address), + l2Provider + ); + await testing.setBalance( + await l1CrossDomainMessengerAliased.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + const tokenRateNotifier = TokenRateNotifier__factory.connect( + ethDeployScript.tokenRateNotifierImplAddress, + l1Provider + ); + await tokenRateNotifier + .connect(l1Deployer) + .addObserver(ethDeployScript.opStackTokenRatePusherImplAddress); + const tokenRateOracle = TokenRateOracle__factory.connect( + optDeployScript.tokenRateOracleProxyAddress, + l2Provider + ); + + const accountA = testing.accounts.accountA(l1Provider, l2Provider); + const l1CrossDomainMessenger = optContracts.L1CrossDomainMessengerStub; + + return { + tokenRateNotifier, + tokenRateOracle, + opTokenRatePusher: ethDeployScript.opStackTokenRatePusherImplAddress, + l1CrossDomainMessenger, + l2CrossDomainMessenger, + l1Token, + l1Provider, + accounts: { + accountA, + l1CrossDomainMessengerAliased + } + }; +} + +async function tokenRateAndTimestamp(provider: JsonRpcProvider, tokenRate: BigNumber) { + const blockNumber = await provider.getBlockNumber(); + const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; + const stEthPerTokenStr = ethers.utils.hexZeroPad(tokenRate.toHexString(), 12); + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); + return [stEthPerTokenStr, blockTimestampStr]; +} diff --git a/test/token/ERC20Bridged.unit.test.ts b/test/token/ERC20Bridged.unit.test.ts index 2ec65bfa..a3359635 100644 --- a/test/token/ERC20Bridged.unit.test.ts +++ b/test/token/ERC20Bridged.unit.test.ts @@ -306,175 +306,6 @@ unit("ERC20Bridged", ctxFactory) ); }) - .test("increaseAllowance() :: initial allowance is zero", async (ctx) => { - const { erc20Bridged } = ctx; - const { holder, spender } = ctx.accounts; - - // validate allowance before increasing - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - "0" - ); - - const allowanceIncrease = wei`1 ether`; - - // increase allowance - const tx = await erc20Bridged.increaseAllowance( - spender.address, - allowanceIncrease - ); - - // validate Approval event was emitted - await assert.emits(erc20Bridged, tx, "Approval", [ - holder.address, - spender.address, - allowanceIncrease, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - allowanceIncrease - ); - }) - - .test("increaseAllowance() :: initial allowance is not zero", async (ctx) => { - const { erc20Bridged } = ctx; - const { holder, spender } = ctx.accounts; - - const initialAllowance = wei`2 ether`; - - // set initial allowance - await erc20Bridged.approve(spender.address, initialAllowance); - - // validate allowance before increasing - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - initialAllowance - ); - - const allowanceIncrease = wei`1 ether`; - - // increase allowance - const tx = await erc20Bridged.increaseAllowance( - spender.address, - allowanceIncrease - ); - - const expectedAllowance = wei - .toBigNumber(initialAllowance) - .add(allowanceIncrease); - - // validate Approval event was emitted - await assert.emits(erc20Bridged, tx, "Approval", [ - holder.address, - spender.address, - expectedAllowance, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - expectedAllowance - ); - }) - - .test("increaseAllowance() :: the increase is not zero", async (ctx) => { - const { erc20Bridged } = ctx; - const { holder, spender } = ctx.accounts; - - const initialAllowance = wei`2 ether`; - - // set initial allowance - await erc20Bridged.approve(spender.address, initialAllowance); - - // validate allowance before increasing - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - initialAllowance - ); - - // increase allowance - const tx = await erc20Bridged.increaseAllowance(spender.address, "0"); - - // validate Approval event was emitted - await assert.emits(erc20Bridged, tx, "Approval", [ - holder.address, - spender.address, - initialAllowance, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - initialAllowance - ); - }) - - .test( - "decreaseAllowance() :: decrease is greater than current allowance", - async (ctx) => { - const { erc20Bridged } = ctx; - const { holder, spender } = ctx.accounts; - - // validate allowance before increasing - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - "0" - ); - - const allowanceDecrease = wei`1 ether`; - - // decrease allowance - await assert.revertsWith( - erc20Bridged.decreaseAllowance(spender.address, allowanceDecrease), - "ErrorDecreasedAllowanceBelowZero()" - ); - } - ) - - .group([wei`1 ether`, "0"], (allowanceDecrease) => [ - `decreaseAllowance() :: the decrease is ${allowanceDecrease} wei`, - async (ctx) => { - const { erc20Bridged } = ctx; - const { holder, spender } = ctx.accounts; - - const initialAllowance = wei`2 ether`; - - // set initial allowance - await erc20Bridged.approve(spender.address, initialAllowance); - - // validate allowance before increasing - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - initialAllowance - ); - - // decrease allowance - const tx = await erc20Bridged.decreaseAllowance( - spender.address, - allowanceDecrease - ); - - const expectedAllowance = wei - .toBigNumber(initialAllowance) - .sub(allowanceDecrease); - - // validate Approval event was emitted - await assert.emits(erc20Bridged, tx, "Approval", [ - holder.address, - spender.address, - expectedAllowance, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await erc20Bridged.allowance(holder.address, spender.address), - expectedAllowance - ); - }, - ]) - .test("bridgeMint() :: not owner", async (ctx) => { const { erc20Bridged } = ctx; const { stranger } = ctx.accounts; diff --git a/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts new file mode 100644 index 00000000..20e2ae7f --- /dev/null +++ b/test/token/ERC20Permit.unit.test.ts @@ -0,0 +1,505 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { BigNumber } from "ethers"; +import { unit, UnitTest } from "../../utils/testing"; +import { wei } from "../../utils/wei"; +import { makeDomainSeparator, signPermit, calculateTransferAuthorizationDigest, signEOAorEIP1271 } from "../../utils/testing/permit-helpers"; +import testing from "../../utils/testing"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { + TokenRateOracle__factory, + OssifiableProxy__factory, + ERC20RebasableBridgedPermit__factory, + ERC1271PermitSignerMock__factory, + ERC20BridgedPermit__factory, +} from "../../typechain"; + + +type ContextType = Awaited>> + +const SIGNING_DOMAIN_VERSION = '2' // aka token version, used in signing permit +const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + +// derived from mnemonic: want believe mosquito cat design route voice cause gold benefit gospel bulk often attitude rural +const ACCOUNTS_AND_KEYS = [ + { + address: '0xF4C028683CAd61ff284d265bC0F77EDd67B4e65A', + privateKey: '0x5f7edf5892efb4a5cd75dedd496598f48e579b562a70eb1360474cc83a982987', + }, + { + address: '0x7F94c1F9e4BfFccc8Cd79195554E0d83a0a5c5f2', + privateKey: '0x3fe2f6bd9dbc7d507a6cb95ec36a36787706617e34385292b66c74cd39874605', + }, +] + +function getChainId() { + return hre.network.config.chainId as number; +} + +const getAccountsEOA = async () => { + return { + alice: ACCOUNTS_AND_KEYS[0], + bob: ACCOUNTS_AND_KEYS[1], + } +} + +const getAccountsEIP1271 = async () => { + const deployer = (await hre.ethers.getSigners())[0] + const alice = await new ERC1271PermitSignerMock__factory(deployer).deploy() + const bob = await new ERC1271PermitSignerMock__factory(deployer).deploy() + return { alice, bob } +} + +function permitTestsSuit(unitInstance: UnitTest) { + unitInstance + + // .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { + // const { rebasableProxied, wrappedToken } = ctx.contracts; + // assert.equal(await rebasableProxied.TOKEN_TO_WRAP_FROM(), wrappedToken.address) + // }) + + .test('eip712Domain() is correct', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const [, name, version, chainId, verifyingContract, ,] = await token.eip712Domain() + + assert.equal(name, ctx.constants.name) + assert.equal(version, SIGNING_DOMAIN_VERSION) + assert.isDefined(hre.network.config.chainId) + assert.equal(chainId.toNumber(), getChainId()) + assert.equal(verifyingContract, token.address) + }) + + .test('DOMAIN_SEPARATOR() is correct', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const domainSeparator = makeDomainSeparator(ctx.constants.name, SIGNING_DOMAIN_VERSION, getChainId(), token.address) + assert.equal(await ctx.contracts.rebasableProxied.DOMAIN_SEPARATOR(), domainSeparator) + }) + + .test('grants allowance when a valid permit is given', async (ctx) => { + const token = ctx.contracts.rebasableProxied + + const { owner, spender, deadline } = ctx.permitParams + let { value } = ctx.permitParams + // create a signed permit to grant Bob permission to spend Alice's funds + // on behalf, and sign with Alice's key + let nonce = 0 + const charlie = ctx.accounts.user2 + + let { v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator) + + // check that the allowance is initially zero + assert.equalBN(await token.allowance(owner.address, spender.address), BigNumber.from(0)) + // check that the next nonce expected is zero + assert.equalBN(await token.nonces(owner.address), BigNumber.from(0)) + // check domain separator + assert.equal(await token.DOMAIN_SEPARATOR(), ctx.domainSeparator) + + // a third-party, Charlie (not Alice) submits the permit + // TODO: handle unpredictable gas limit somehow better than setting it to a random constant + const tx = await token.connect(charlie) + .permit(owner.address, spender.address, value, deadline, v, r, s, { gasLimit: '0xffffff' }) + + // check that allowance is updated + assert.equalBN(await token.allowance(owner.address, spender.address), BigNumber.from(value)) + await assert.emits(token, tx, 'Approval', [owner.address, spender.address, value]) + assert.equalBN(await token.nonces(owner.address), BigNumber.from(1)) + + + // increment nonce + nonce = 1 + value = 4e5 + ; ({ v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator)) + + // submit the permit + const tx2 = await token.connect(charlie).permit(owner.address, spender.address, value, deadline, v, r, s) + + // check that allowance is updated + assert.equalBN(await token.allowance(owner.address, spender.address), BigNumber.from(value)) + assert.emits(token, tx2, 'Approval', [owner.address, spender.address, BigNumber.from(value)]) + assert.equalBN(await token.nonces(owner.address), BigNumber.from(2)) + }) + + + .test('reverts if the signature does not match given parameters', async (ctx) => { + const { owner, spender, value, nonce, deadline } = ctx.permitParams + const token = ctx.contracts.rebasableProxied + const charlie = ctx.accounts.user2 + + // create a signed permit + const { v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator) + + // try to cheat by claiming the approved amount + 1 + await assert.revertsWith( + token.connect(charlie).permit( + owner.address, + spender.address, + value + 1, // pass more than signed value + deadline, + v, + r, + s, + ), + 'ErrorInvalidSignature()' + ) + + // check that msg is incorrect even if claim the approved amount - 1 + await assert.revertsWith( + token.connect(charlie).permit( + owner.address, + spender.address, + value - 1, // pass less than signed + deadline, + v, + r, + s, + ), + 'ErrorInvalidSignature()' + ) + }) + + .test('reverts if the signature is not signed with the right key', async (ctx) => { + const { owner, spender, value, nonce, deadline } = ctx.permitParams + const token = ctx.contracts.rebasableProxied + const spenderSigner = await hre.ethers.getSigner(spender.address) + const charlie = ctx.accounts.user2 + + // create a signed permit to grant Bob permission to spend + // Alice's funds on behalf, but sign with Bob's key instead of Alice's + const { v, r, s } = await signPermit(owner.address, spender, spender.address, value, deadline, nonce, ctx.domainSeparator) + + // try to cheat by submitting the permit that is signed by a + // wrong person + await assert.revertsWith( + token.connect(charlie).permit(owner.address, spender.address, value, deadline, v, r, s), + 'ErrorInvalidSignature()' + ) + + await testing.impersonate(spender.address) + await testing.setBalance(spender.address, wei.toBigNumber(wei`10 ether`)) + + // even Bob himself can't call permit with the invalid sig + await assert.revertsWith( + token.connect(spenderSigner).permit(owner.address, spender.address, value, deadline, v, r, s), + 'ErrorInvalidSignature()' + ) + }) + + .test('reverts if the permit is expired', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const { owner, spender, value, nonce } = ctx.permitParams + const charlie = ctx.accounts.user2 + + // create a signed permit that already invalid + const deadline = ((await hre.ethers.provider.getBlock('latest')).timestamp - 1).toString() + const { v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator) + + // try to submit the permit that is expired + await assert.revertsWith( + token.connect(charlie).permit(owner.address, spender.address, value, deadline, v, r, s, { gasLimit: '0xffffff' }), + 'ErrorDeadlineExpired()' + ) + + { + // create a signed permit that valid for 1 minute (approximately) + const deadline1min = ((await hre.ethers.provider.getBlock('latest')).timestamp + 60).toString() + const { v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline1min, nonce, ctx.domainSeparator) + const tx = await token.connect(charlie).permit(owner.address, spender.address, value, deadline1min, v, r, s) + + assert.equalBN(await token.nonces(owner.address), BigNumber.from(1)) + assert.emits(token, tx, 'Approval', [owner, spender, BigNumber.from(value)]) + } + }) + + .test('reverts if the nonce given does not match the next nonce expected', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const { owner, spender, value, deadline } = ctx.permitParams + const charlie = ctx.accounts.user2 + const nonce = 1 + // create a signed permit + const { v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator) + // check that the next nonce expected is 0, not 1 + assert.equalBN(await token.nonces(owner.address), BigNumber.from(0)) + + // try to submit the permit + await assert.revertsWith( + token.connect(charlie).permit(owner.address, spender.address, value, deadline, v, r, s), + 'ErrorInvalidSignature()' + ) + }) + + .test('reverts if the permit has already been used', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const { owner, spender, value, nonce, deadline } = ctx.permitParams + const charlie = ctx.accounts.user2 + // create a signed permit + const { v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator) + + // submit the permit + await token.connect(charlie).permit(owner.address, spender.address, value, deadline, v, r, s) + + // try to submit the permit again + await assert.revertsWith( + token.connect(charlie).permit(owner.address, spender.address, value, deadline, v, r, s), + 'ErrorInvalidSignature()' + ) + + await testing.impersonate(owner.address) + await testing.setBalance(owner.address, wei.toBigNumber(wei`10 ether`)) + + // try to submit the permit again from Alice herself + await assert.revertsWith( + token.connect(await hre.ethers.getSigner(owner.address)).permit(owner.address, spender.address, value, deadline, v, r, s), + 'ErrorInvalidSignature()' + ) + }) + + .test('reverts if the permit has a nonce that has already been used by the signer', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const { owner, spender, value, nonce, deadline } = ctx.permitParams + const charlie = ctx.accounts.user2 + // create a signed permit + const permit = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator) + + // submit the permit + await token.connect(charlie).permit(owner.address, spender.address, value, deadline, permit.v, permit.r, permit.s) + + // create another signed permit with the same nonce, but + // with different parameters + const permit2 = await signPermit(owner.address, owner, spender.address, 1e6, deadline, nonce, ctx.domainSeparator) + + // try to submit the permit again + await assert.revertsWith( + token.connect(charlie).permit(owner.address, spender.address, 1e6, deadline, permit2.v, permit2.r, permit2.s), + 'ErrorInvalidSignature()' + ) + }) + + .test('reverts if the permit includes invalid approval parameters', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const { owner, value, nonce, deadline } = ctx.permitParams + const charlie = ctx.accounts.user2 + // create a signed permit that attempts to grant allowance to the + // zero address + const spender = hre.ethers.constants.AddressZero + const { v, r, s } = await signPermit(owner.address, owner, spender, value, deadline, nonce, ctx.domainSeparator) + + // try to submit the permit with invalid approval parameters + await assert.revertsWith( + token.connect(charlie).permit(owner.address, spender, value, deadline, v, r, s), + 'ErrorAccountIsZeroAddress()' + ) + }) + + .test('reverts if the permit is not for an approval', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const charlie = ctx.accounts.user2 + const { owner: from, spender: to, value, deadline: validBefore } = ctx.permitParams + // create a signed permit for a transfer + const validAfter = '0' + const nonce = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + const digest = calculateTransferAuthorizationDigest( + from.address, + to.address, + value, + validAfter, + validBefore, + nonce, + ctx.domainSeparator + ) + const { v, r, s } = await signEOAorEIP1271(digest, from) + + // try to submit the transfer permit + await assert.revertsWith( + token.connect(charlie).permit(from.address, to.address, value, validBefore, v, r, s), + 'ErrorInvalidSignature()' + ) + }) + + .run(); +} + +function ctxFactoryFactory( + name: string, + symbol: string, + isRebasable: boolean, + signingAccountsFuncFactory: typeof getAccountsEIP1271 | typeof getAccountsEOA +) { + return async () => { + const decimalsToSet = 18; + const decimals = BigNumber.from(10).pow(decimalsToSet); + const rate = BigNumber.from('12').pow(decimalsToSet - 1); + const premintShares = wei.toBigNumber(wei`100 ether`); + const premintTokens = BigNumber.from(rate).mul(premintShares).div(decimals); + + const [ + deployer, + owner, + recipient, + spender, + holder, + stranger, + user1, + user2, + ] = await hre.ethers.getSigners(); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [hre.ethers.constants.AddressZero], + }); + + const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); + + const rebasableProxied = await tokenProxied( + name, + symbol, + decimalsToSet, + rate, + isRebasable, + owner, + deployer, + holder + ); + + const { alice, bob } = await signingAccountsFuncFactory(); + + return { + accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, + constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate }, + contracts: { rebasableProxied }, + permitParams: { + owner: alice, + spender: bob, + value: 6e6, + nonce: 0, + deadline: MAX_UINT256, + }, + domainSeparator: makeDomainSeparator(name, SIGNING_DOMAIN_VERSION, getChainId(), rebasableProxied.address), + }; + } +} + +async function tokenProxied( + name: string, + symbol: string, + decimalsToSet: number, + rate: BigNumber, + isRebasable: boolean, + owner: SignerWithAddress, + deployer: SignerWithAddress, + holder: SignerWithAddress) { + + if (isRebasable) { + + const wrappedToken = await new ERC20BridgedPermit__factory(deployer).deploy( + "WstETH Test Token", + "WstETH", + SIGNING_DOMAIN_VERSION, + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + hre.ethers.constants.AddressZero, + owner.address, + hre.ethers.constants.AddressZero, + 86400 + ); + const rebasableTokenImpl = await new ERC20RebasableBridgedPermit__factory(deployer).deploy( + name, + symbol, + SIGNING_DOMAIN_VERSION, + decimalsToSet, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); + + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( + rebasableTokenImpl.address, + deployer.address, + ERC20RebasableBridgedPermit__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const rebasableProxied = ERC20RebasableBridgedPermit__factory.connect( + l2TokensProxy.address, + holder + ); + + await tokenRateOracle.connect(owner).updateRate(rate, 1000); + const premintShares = wei.toBigNumber(wei`100 ether`); + await rebasableProxied.connect(owner).bridgeMintShares(holder.address, premintShares); + + return rebasableProxied; + } + + const wrappedToken = await new ERC20BridgedPermit__factory(deployer).deploy( + name, + symbol, + SIGNING_DOMAIN_VERSION, + decimalsToSet, + owner.address + ); + + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( + wrappedToken.address, + deployer.address, + ERC20BridgedPermit__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const nonRebasableProxied = ERC20BridgedPermit__factory.connect( + l2TokensProxy.address, + holder + ); + + return nonRebasableProxied; +} + +permitTestsSuit( + unit("ERC20RebasableBridgedPermit with EIP1271 (contract) signing", + ctxFactoryFactory( + "Liquid staked Ether 2.0", + "stETH", + true, + getAccountsEIP1271 + ) + ) +); + +permitTestsSuit( + unit("ERC20RebasableBridgedPermit with ECDSA (EOA) signing", + ctxFactoryFactory( + "Liquid staked Ether 2.0", + "stETH", + true, + getAccountsEOA + ) + ) +); + +permitTestsSuit( + unit("ERC20BridgedPermit with EIP1271 (contract) signing", + ctxFactoryFactory( + "Wrapped liquid staked Ether 2.0", + "wstETH", + false, + getAccountsEIP1271 + ) + ) +); + +permitTestsSuit( + unit("ERC20BridgedPermit with ECDSA (EOA) signing", + ctxFactoryFactory( + "Wrapped liquid staked Ether 2.0", + "WstETH", + false, + getAccountsEOA + ) + ) +); diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts new file mode 100644 index 00000000..e71c6c73 --- /dev/null +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -0,0 +1,899 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; + +import { + ERC20Bridged__factory, + TokenRateOracle__factory, + ERC20RebasableBridged__factory, + OssifiableProxy__factory, + CrossDomainMessengerStub__factory +} from "../../typechain"; +import { BigNumber } from "ethers"; + +unit("ERC20RebasableBridged", ctxFactory) + + .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { + const { rebasableProxied, wrappedToken } = ctx.contracts; + assert.equal(await rebasableProxied.TOKEN_TO_WRAP_FROM(), wrappedToken.address) + }) + + .test("tokenRateOracle() :: has the same address is in constructor", async (ctx) => { + const { rebasableProxied, tokenRateOracle } = ctx.contracts; + assert.equal(await rebasableProxied.TOKEN_RATE_ORACLE(), tokenRateOracle.address) + }) + + .test("name() :: has the same value is in constructor", async (ctx) => + assert.equal(await ctx.contracts.rebasableProxied.name(), ctx.constants.name) + ) + + .test("symbol() :: has the same value is in constructor", async (ctx) => + assert.equal(await ctx.contracts.rebasableProxied.symbol(), ctx.constants.symbol) + ) + + .test("initialize() :: name already set", async (ctx) => { + const { deployer, owner, zero } = ctx.accounts; + const { decimalsToSet } = ctx.constants; + + // deploy new implementation + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, + owner.address, + zero.address, + 86400 + ); + const rebasableTokenImpl = await new ERC20RebasableBridged__factory(deployer).deploy( + "name", + "", + 10, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); + await assert.revertsWith( + rebasableTokenImpl.initialize("New Name", ""), + "ErrorNameAlreadySet()" + ); + }) + + .test("initialize() :: symbol already set", async (ctx) => { + const { deployer, owner, zero } = ctx.accounts; + const { decimalsToSet } = ctx.constants; + + // deploy new implementation + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, + owner.address, + zero.address, + 86400 + ); + const rebasableTokenImpl = await new ERC20RebasableBridged__factory(deployer).deploy( + "", + "symbol", + 10, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); + await assert.revertsWith( + rebasableTokenImpl.initialize("", "New Symbol"), + "ErrorSymbolAlreadySet()" + ); + }) + + .test("decimals() :: has the same value as is in constructor", async (ctx) => + assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimalsToSet) + ) + + .test("getTotalShares() :: returns preminted amount", async (ctx) => { + const { premintShares } = ctx.constants; + assert.equalBN(await ctx.contracts.rebasableProxied.getTotalShares(), premintShares); + }) + + .test("wrap() :: revert if wrap 0 wstETH", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { user1 } = ctx.accounts; + await assert.revertsWith(rebasableProxied.connect(user1).wrap(0), "ErrorZeroSharesWrap()"); + }) + + .test("wrap() :: wrong oracle update time", async (ctx) => { + + const { deployer, user1, owner, zero } = ctx.accounts; + const { decimalsToSet } = ctx.constants; + + // deploy new implementation to test initial oracle state + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, + owner.address, + zero.address, + 86400 + ); + const rebasableProxied = await new ERC20RebasableBridged__factory(deployer).deploy( + "", + "symbol", + 10, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); + + await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); + await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); + + await assert.revertsWith(rebasableProxied.connect(user1).wrap(5), "ErrorWrongOracleUpdateTime()"); +}) + + .test("wrap() :: when no balance", async (ctx) => { + const { rebasableProxied, wrappedToken } = ctx.contracts; + const { user1 } = ctx.accounts; + + await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(2), "ErrorNotEnoughBalance()"); + }) + + .test("wrap() :: happy path", async (ctx) => { + + const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; + const {user1, user2, owner, zero } = ctx.accounts; + const { rate, decimals, premintShares } = ctx.constants; + + await tokenRateOracle.connect(owner).updateRate(rate, 1000); + + const totalSupply = rate.mul(premintShares).div(decimals); + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); + + // user1 + const user1Shares = wei`100 ether`; + const user1Tokens = rate.mul(user1Shares).div(decimals); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + await wrappedToken.connect(owner).bridgeMint(user1.address, user1Tokens); + await wrappedToken.connect(user1).approve(rebasableProxied.address, user1Shares); + + assert.equalBN(await rebasableProxied.connect(user1).callStatic.wrap(user1Shares), user1Tokens); + const tx = await rebasableProxied.connect(user1).wrap(user1Shares); + + await assert.emits(rebasableProxied, tx, "Transfer", [zero.address, user1.address, user1Tokens]); + await assert.emits(rebasableProxied, tx, "TransferShares", [zero.address, user1.address, user1Shares]); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); + assert.equalBN(await wrappedToken.balanceOf(rebasableProxied.address), user1Shares); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens)); + + // user2 + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + + const user2Shares = wei`50 ether`; + const user2Tokens = rate.mul(user2Shares).div(decimals); + + await wrappedToken.connect(owner).bridgeMint(user2.address, user2Tokens); + await wrappedToken.connect(user2).approve(rebasableProxied.address, user2Shares); + + assert.equalBN(await rebasableProxied.connect(user2).callStatic.wrap(user2Shares), user2Tokens); + const tx2 = await rebasableProxied.connect(user2).wrap(user2Shares); + + await assert.emits(rebasableProxied, tx2, "Transfer", [zero.address, user2.address, user2Tokens]); + await assert.emits(rebasableProxied, tx2, "TransferShares", [zero.address, user2.address, user2Shares]); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); + assert.equalBN(await wrappedToken.balanceOf(rebasableProxied.address), BigNumber.from(user1Shares).add(user2Shares)); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); + }) + + .test("unwrap() :: revert if unwrap 0 wstETH", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { user1 } = ctx.accounts; + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(0), "ErrorZeroTokensUnwrap()"); + }) + + .test("unwrap() :: happy path", async (ctx) => { + + const { rebasableProxied, wrappedToken } = ctx.contracts; + const {user1, user2, owner } = ctx.accounts; + const { rate, decimals, premintShares } = ctx.constants; + + const totalSupply = BigNumber.from(rate).mul(premintShares).div(decimals); + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); + + // user1 + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + const user1SharesToWrap = wei`100 ether`; + const user1SharesToUnwrap = wei`59 ether`; + const user1TokensToUnwrap = rate.mul(user1SharesToUnwrap).div(decimals); + + const user1Shares = BigNumber.from(user1SharesToWrap).sub(user1SharesToUnwrap); + const user1Tokens = BigNumber.from(rate).mul(user1Shares).div(decimals); + + await wrappedToken.connect(owner).bridgeMint(user1.address, user1SharesToWrap); + await wrappedToken.connect(user1).approve(rebasableProxied.address, user1SharesToWrap); + + const tx0 = await rebasableProxied.connect(user1).wrap(user1SharesToWrap); + assert.equalBN(await rebasableProxied.connect(user1).callStatic.unwrap(user1TokensToUnwrap), user1SharesToUnwrap); + const tx = await rebasableProxied.connect(user1).unwrap(user1TokensToUnwrap); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); + assert.equalBN(await wrappedToken.balanceOf(rebasableProxied.address), user1Shares); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens)); + + // user2 + const user2SharesToWrap = wei`145 ether`; + const user2SharesToUnwrap = wei`14 ether`; + const user2TokensToUnwrap = rate.mul(user2SharesToUnwrap).div(decimals); + + const user2Shares = BigNumber.from(user2SharesToWrap).sub(user2SharesToUnwrap); + const user2Tokens = BigNumber.from(rate).mul(user2Shares).div(decimals); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + + await wrappedToken.connect(owner).bridgeMint(user2.address, user2SharesToWrap); + await wrappedToken.connect(user2).approve(rebasableProxied.address, user2SharesToWrap); + + await rebasableProxied.connect(user2).wrap(user2SharesToWrap); + assert.equalBN(await rebasableProxied.connect(user2).callStatic.unwrap(user2TokensToUnwrap), user2SharesToUnwrap); + const tx2 = await rebasableProxied.connect(user2).unwrap(user2TokensToUnwrap); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); + assert.equalBN(await wrappedToken.balanceOf(rebasableProxied.address), BigNumber.from(user1Shares).add(user2Shares)); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); + }) + + .test("unwrap() :: with wrong oracle update time", async (ctx) => { + + const { deployer, user1, owner, zero } = ctx.accounts; + const { decimalsToSet } = ctx.constants; + + // deploy new implementation to test initial oracle state + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, + owner.address, + zero.address, + 86400 + ); + const rebasableProxied = await new ERC20RebasableBridged__factory(deployer).deploy( + "", + "symbol", + 10, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); + + await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); + await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); + + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(5), "ErrorWrongOracleUpdateTime()"); + }) + + .test("unwrap() :: when no balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { user1 } = ctx.accounts; + + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`4 ether`), "ErrorNotEnoughBalance()"); + }) + + .test("bridgeMintShares() :: happy path", async (ctx) => { + + const { rebasableProxied } = ctx.contracts; + const {user1, user2, owner, zero } = ctx.accounts; + const { rate, decimals, premintShares, premintTokens } = ctx.constants; + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); + + // user1 + const user1SharesToMint = wei`44 ether`; + const user1TokensMinted = rate.mul(user1SharesToMint).div(decimals); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + const tx0 = await rebasableProxied.connect(owner).bridgeMintShares(user1.address, user1SharesToMint); + await assert.emits(rebasableProxied, tx0, "Transfer", [zero.address, user1.address, user1TokensMinted]); + await assert.emits(rebasableProxied, tx0, "TransferShares", [zero.address, user1.address, user1SharesToMint]); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1TokensMinted); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1SharesToMint)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted)); + + // // user2 + const user2SharesToMint = wei`75 ether`; + const user2TokensMinted = rate.mul(user2SharesToMint).div(decimals); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + + const tx1 = await rebasableProxied.connect(owner).bridgeMintShares(user2.address, user2SharesToMint); + await assert.emits(rebasableProxied, tx1, "Transfer", [zero.address, user2.address, user2TokensMinted]); + await assert.emits(rebasableProxied, tx1, "TransferShares", [zero.address, user2.address, user2SharesToMint]); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1SharesToMint).add(user2SharesToMint)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted).add(user2TokensMinted)); + }) + + .test("bridgeBurnShares() :: happy path", async (ctx) => { + + const { rebasableProxied } = ctx.contracts; + const {user1, user2, owner } = ctx.accounts; + const { rate, decimals, premintShares, premintTokens } = ctx.constants; + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); + + // user1 + const user1SharesToMint = wei`12 ether`; + const user1TokensMinted = rate.mul(user1SharesToMint).div(decimals); + + const user1SharesToBurn = wei`4 ether`; + const user1TokensBurned = rate.mul(user1SharesToBurn).div(decimals); + + const user1Shares = BigNumber.from(user1SharesToMint).sub(user1SharesToBurn); + const user1Tokens = user1TokensMinted.sub(user1TokensBurned); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); + + await rebasableProxied.connect(owner).bridgeMintShares(user1.address, user1SharesToMint); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1TokensMinted); + + await rebasableProxied.connect(owner).bridgeBurnShares(user1.address, user1SharesToBurn); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1Tokens)); + + // // user2 + const user2SharesToMint = wei`64 ether`; + const user2TokensMinted = rate.mul(user2SharesToMint).div(decimals); + + const user2SharesToBurn = wei`22 ether`; + const user2TokensBurned = rate.mul(user2SharesToBurn).div(decimals); + + const user2Shares = BigNumber.from(user2SharesToMint).sub(user2SharesToBurn); + const user2Tokens = user2TokensMinted.sub(user2TokensBurned); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + + await rebasableProxied.connect(owner).bridgeMintShares(user2.address, user2SharesToMint); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); + await rebasableProxied.connect(owner).bridgeBurnShares(user2.address, user2SharesToBurn); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); + + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1Tokens).add(user2Tokens)); + }) + + .test("approve() :: happy path", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + // validate initially allowance is zero + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + "0" + ); + + const amount = wei`1 ether`; + + // validate return value of the method + assert.isTrue( + await rebasableProxied.callStatic.approve(spender.address, amount) + ); + + // approve tokens + const tx = await rebasableProxied.approve(spender.address, amount); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + amount, + ]); + + // validate allowance was set + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + amount + ); + }) + + .test("transfer() :: sender is zero address", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + + const { + accounts: { zero, recipient }, + } = ctx; + await assert.revertsWith( + rebasableProxied.connect(zero).transfer(recipient.address, wei`1 ether`), + "ErrorAccountIsZeroAddress()" + ); + }) + + .test("transfer() :: recipient is zero address", async (ctx) => { + const { zero, holder } = ctx.accounts; + await assert.revertsWith( + ctx.contracts.rebasableProxied.connect(holder).transfer(zero.address, wei`1 ether`), + "ErrorAccountIsZeroAddress()" + ); + }) + + .test("transfer() :: zero balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder } = ctx.accounts; + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + // transfer tokens + await rebasableProxied.connect(holder).transfer(recipient.address, "0"); + + // validate balance stays same + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + }) + + .test("transfer() :: not enough balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder } = ctx.accounts; + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = premintTokens.add(wei`1 ether`); + + // transfer tokens + await assert.revertsWith( + rebasableProxied.connect(holder).transfer(recipient.address, amount), + "ErrorNotEnoughBalance()" + ); + }) + + .test("transfer() :: happy path", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder } = ctx.accounts; + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + const sharesToTransfer = await rebasableProxied.getSharesByTokens(amount); + + // transfer tokens + const tx = await rebasableProxied + .connect(holder) + .transfer(recipient.address, amount); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + recipient.address, + amount, + ]); + + await assert.emits(rebasableProxied, tx, "TransferShares", [ + holder.address, + recipient.address, + sharesToTransfer, + ]); + + // validate balance was updated + assert.equalBN( + await rebasableProxied.balanceOf(holder.address), + premintTokens.sub(amount) + ); + + // validate total supply stays same + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); + }) + + .test("transferFrom() :: happy path", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // holder sets allowance for spender + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance is set + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + const holderBalanceBefore = await rebasableProxied.balanceOf(holder.address); + + // transfer tokens + const tx = await rebasableProxied + .connect(spender) + .transferFrom(holder.address, recipient.address, amount); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + wei.toBigNumber(initialAllowance).sub(amount), + ]); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + recipient.address, + amount, + ]); + + // validate allowance updated + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + wei.toBigNumber(initialAllowance).sub(amount) + ); + + // validate holder balance updated + assert.equalBN( + await rebasableProxied.balanceOf(holder.address), + holderBalanceBefore.sub(amount) + ); + + const recipientBalance = await rebasableProxied.balanceOf(recipient.address); + + // validate recipient balance updated + assert.equalBN(BigNumber.from(amount).sub(recipientBalance), "1"); + }) + + .test("transferFrom() :: max allowance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder, spender } = ctx.accounts; + + const initialAllowance = hre.ethers.constants.MaxUint256; + + // set allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance is set + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + const holderBalanceBefore = await rebasableProxied.balanceOf(holder.address); + + // transfer tokens + const tx = await rebasableProxied + .connect(spender) + .transferFrom(holder.address, recipient.address, amount); + + // validate Approval event was not emitted + await assert.notEmits(rebasableProxied, tx, "Approval"); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + recipient.address, + amount, + ]); + + // validate allowance wasn't changed + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // validate holder balance updated + assert.equalBN( + await rebasableProxied.balanceOf(holder.address), + holderBalanceBefore.sub(amount) + ); + + // validate recipient balance updated + const recipientBalance = await rebasableProxied.balanceOf(recipient.address); + assert.equalBN(BigNumber.from(amount).sub(recipientBalance), "1"); + }) + + .test("transferFrom() :: not enough allowance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintTokens } = ctx.constants; + const { recipient, holder, spender } = ctx.accounts; + + const initialAllowance = wei`0.9 ether`; + + // set allowance + await rebasableProxied.approve(recipient.address, initialAllowance); + + // validate allowance is set + assert.equalBN( + await rebasableProxied.allowance(holder.address, recipient.address), + initialAllowance + ); + + // validate balance before transfer + assert.equalBN(await rebasableProxied.balanceOf(holder.address), premintTokens); + + const amount = wei`1 ether`; + + // transfer tokens + await assert.revertsWith( + rebasableProxied + .connect(spender) + .transferFrom(holder.address, recipient.address, amount), + "ErrorNotEnoughAllowance()" + ); + }) + + .test("bridgeMint() :: not owner", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { stranger } = ctx.accounts; + + await assert.revertsWith( + rebasableProxied + .connect(stranger) + .bridgeMintShares(stranger.address, wei`1000 ether`), + "ErrorNotBridge()" + ); + }) + + .group([wei`1000 ether`, "0"], (mintAmount) => [ + `bridgeMint() :: amount is ${mintAmount} wei`, + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintShares } = ctx.constants; + const { recipient, owner, zero } = ctx.accounts; + + // validate balance before mint + assert.equalBN(await rebasableProxied.balanceOf(recipient.address), 0); + + // validate total supply before mint + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + + // mint tokens + const tx = await rebasableProxied + .connect(owner) + .bridgeMintShares(recipient.address, mintAmount); + + // validate Transfer event was emitted + const mintAmountInTokens = await rebasableProxied.getTokensByShares(mintAmount); + await assert.emits(rebasableProxied, tx, "Transfer", [ + zero.address, + recipient.address, + mintAmountInTokens, + ]); + await assert.emits(rebasableProxied, tx, "TransferShares", [ + zero.address, + recipient.address, + mintAmount, + ]); + + // validate balance was updated + assert.equalBN( + await rebasableProxied.sharesOf(recipient.address), + mintAmount + ); + + // validate total supply was updated + assert.equalBN( + await rebasableProxied.getTotalShares(), + premintShares.add(mintAmount) + ); + }, + ]) + + .test("bridgeBurn() :: not owner", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, stranger } = ctx.accounts; + + await assert.revertsWith( + rebasableProxied.connect(stranger).bridgeBurnShares(holder.address, wei`100 ether`), + "ErrorNotBridge()" + ); + }) + + .test("bridgeBurn() :: amount exceeds balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { owner, stranger } = ctx.accounts; + + // validate stranger has no tokens + assert.equalBN(await rebasableProxied.balanceOf(stranger.address), 0); + + await assert.revertsWith( + rebasableProxied.connect(owner).bridgeBurnShares(stranger.address, wei`100 ether`), + "ErrorNotEnoughBalance()" + ); + }) + + .group([wei`10 ether`, "0"], (burnAmount) => [ + `bridgeBurn() :: amount is ${burnAmount} wei`, + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { premintShares } = ctx.constants; + const { owner, holder } = ctx.accounts; + + // validate balance before mint + assert.equalBN(await rebasableProxied.sharesOf(holder.address), premintShares); + + // validate total supply before mint + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + + // burn tokens + const tx = await rebasableProxied + .connect(owner) + .bridgeBurnShares(holder.address, burnAmount); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + holder.address, + hre.ethers.constants.AddressZero, + burnAmount, + ]); + + const expectedBalanceAndTotalSupply = premintShares + .sub(burnAmount); + + // validate balance was updated + assert.equalBN( + await rebasableProxied.sharesOf(holder.address), + expectedBalanceAndTotalSupply + ); + + // validate total supply was updated + assert.equalBN( + await rebasableProxied.getTotalShares(), + expectedBalanceAndTotalSupply + ); + }, + ]) + + .run(); + +async function ctxFactory() { + const name = "StETH Test Token"; + const symbol = "StETH"; + const decimalsToSet = 18; + const decimals = BigNumber.from(10).pow(decimalsToSet); + const rate = BigNumber.from('12').pow(decimalsToSet - 1); + const premintShares = wei.toBigNumber(wei`100 ether`); + const premintTokens = BigNumber.from(rate).mul(premintShares).div(decimals); + + const provider = await hre.ethers.provider; + const blockNumber = await provider.getBlockNumber(); + const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; + + const [ + deployer, + owner, + recipient, + spender, + holder, + stranger, + user1, + user2 + ] = await hre.ethers.getSigners(); + const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); + + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, + owner.address, + zero.address, + 86400 + ); + const rebasableTokenImpl = await new ERC20RebasableBridged__factory(deployer).deploy( + name, + symbol, + decimalsToSet, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [hre.ethers.constants.AddressZero], + }); + + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( + rebasableTokenImpl.address, + deployer.address, + ERC20RebasableBridged__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const rebasableProxied = ERC20RebasableBridged__factory.connect( + l2TokensProxy.address, + holder + ); + + await tokenRateOracle.connect(owner).updateRate(rate, blockTimestamp - 1000); + await rebasableProxied.connect(owner).bridgeMintShares(holder.address, premintShares); + + return { + accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, + constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate, blockTimestamp }, + contracts: { rebasableProxied, wrappedToken, tokenRateOracle } + }; +} diff --git a/utils/arbitrum/testing.ts b/utils/arbitrum/testing.ts index e34dce4c..081628a5 100644 --- a/utils/arbitrum/testing.ts +++ b/utils/arbitrum/testing.ts @@ -206,15 +206,15 @@ async function deployTestGateway( await ethDeployScript.run(); await arbDeployScript.run(); - const l1ERC20TokenBridgeProxyDeployStepIndex = 1; + const l1ERC20ExtendedTokensBridgeProxyDeployStepIndex = 1; const l1BridgingManagement = new BridgingManagement( - ethDeployScript.getContractAddress(l1ERC20TokenBridgeProxyDeployStepIndex), + ethDeployScript.getContractAddress(l1ERC20ExtendedTokensBridgeProxyDeployStepIndex), ethDeployer ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 3; + const l2ERC20ExtendedTokensBridgeProxyDeployStepIndex = 3; const l2BridgingManagement = new BridgingManagement( - arbDeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + arbDeployScript.getContractAddress(l2ERC20ExtendedTokensBridgeProxyDeployStepIndex), arbDeployer ); diff --git a/utils/deployment.ts b/utils/deployment.ts index 617ca5b9..d9037d42 100644 --- a/utils/deployment.ts +++ b/utils/deployment.ts @@ -10,14 +10,32 @@ interface ChainDeploymentConfig extends BridgingManagerSetupConfig { } interface MultiChainDeploymentConfig { - token: string; + l1Token: string; + l1RebasableToken: string; + l1OpStackTokenRatePusher: string; + l2GasLimitForPushingTokenRate: number; + tokenRateOutdatedDelay: number; + l1TokenBridge: string; + l2TokenBridge: string; + l2Token: string; + l2TokenRateOracle: string; + govBridgeExecutor: string; l1: ChainDeploymentConfig; l2: ChainDeploymentConfig; } export function loadMultiChainDeploymentConfig(): MultiChainDeploymentConfig { return { - token: env.address("TOKEN"), + l1Token: env.address("TOKEN"), + l1RebasableToken: env.address("REBASABLE_TOKEN"), + l1OpStackTokenRatePusher: env.address("L1_OP_STACK_TOKEN_RATE_PUSHER"), + l2GasLimitForPushingTokenRate: Number(env.string("L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE")), + tokenRateOutdatedDelay: Number(env.string("TOKEN_RATE_OUTDATED_DELAY")), + l1TokenBridge: env.address("L1_TOKEN_BRIDGE"), + l2TokenBridge: env.address("L2_TOKEN_BRIDGE"), + l2Token: env.address("L2_TOKEN"), + l2TokenRateOracle: env.address("L2_TOKEN_RATE_ORACLE"), + govBridgeExecutor: env.address("GOV_BRIDGE_EXECUTOR"), l1: { proxyAdmin: env.address("L1_PROXY_ADMIN"), bridgeAdmin: env.address("L1_BRIDGE_ADMIN"), @@ -49,8 +67,8 @@ export async function printMultiChainDeploymentConfig( l1DeployScript: DeployScript, l2DeployScript: DeployScript ) { - const { token, l1, l2 } = deploymentParams; - console.log(chalk.bold(`${title} :: ${chalk.underline(token)}\n`)); + const { l1Token, l1RebasableToken, l1, l2 } = deploymentParams; + console.log(chalk.bold(`${title} :: ${chalk.underline(l1Token)} :: ${chalk.underline(l1RebasableToken)}\n`)); console.log(chalk.bold(" · L1 Deployment Params:")); await printChainDeploymentConfig(l1Deployer, l1); console.log(); diff --git a/utils/lido.ts b/utils/lido.ts index ac3de401..f31f0ef3 100644 --- a/utils/lido.ts +++ b/utils/lido.ts @@ -16,9 +16,9 @@ const ARAGON_MAINNET = { }; const ARAGON_SEPOLIA = { - agent: "0x4333218072D5d7008546737786663c38B4D561A4", - voting: "0xbc0B67b4553f4CF52a913DE9A6eD0057E2E758Db", - tokenManager: "0xDfe76d11b365f5e0023343A367f0b311701B3bc1", + agent: "0x32A0E5828B62AAb932362a4816ae03b860b65e83", + voting: "0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A", + tokenManager: "0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66", }; const ARAGON_CONTRACTS_BY_NAME = { diff --git a/utils/optimism/LidoBridgeAdapter.ts b/utils/optimism/LidoBridgeAdapter.ts new file mode 100644 index 00000000..ac561d4b --- /dev/null +++ b/utils/optimism/LidoBridgeAdapter.ts @@ -0,0 +1,80 @@ +import { StandardBridgeAdapter, toAddress } from "@eth-optimism/sdk"; +import { hexStringEquals } from "@eth-optimism/core-utils"; +import { Contract } from 'ethers'; + +export class LidoBridgeAdapter extends StandardBridgeAdapter { + async supportsTokenPair(l1Token: Contract, l2Token: Contract) { + const l1Bridge = new Contract(this.l1Bridge.address, [ + { + inputs: [], + name: 'L1_TOKEN_NON_REBASABLE', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'L2_TOKEN_NON_REBASABLE', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'L1_TOKEN_REBASABLE', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'L2_TOKEN_REBASABLE', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ], this.messenger.l1Provider); + + const allowedL1RebasableToken = await l1Bridge.L1_TOKEN_REBASABLE(); + const allowedL1NonRebasableToken = await l1Bridge.L1_TOKEN_NON_REBASABLE(); + + if ((!(0, hexStringEquals)(allowedL1RebasableToken, (0, toAddress)(l1Token))) && + (!(0, hexStringEquals)(allowedL1NonRebasableToken, (0, toAddress)(l1Token)))) + { + return false; + } + + const allowedL2RebasableToken = await l1Bridge.L2_TOKEN_REBASABLE(); + const allowedL2NonRebasableToken = await l1Bridge.L2_TOKEN_NON_REBASABLE(); + + if ((!(0, hexStringEquals)(allowedL2RebasableToken, (0, toAddress)(l2Token))) && + (!(0, hexStringEquals)(allowedL2NonRebasableToken, (0, toAddress)(l2Token)))) { + return false; + } + return true; + } +} diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts deleted file mode 100644 index c3b6aa3a..00000000 --- a/utils/optimism/deployment.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { assert } from "chai"; -import { Overrides, Wallet } from "ethers"; -import { - ERC20Bridged__factory, - IERC20Metadata__factory, - L1ERC20TokenBridge__factory, - L2ERC20TokenBridge__factory, - OssifiableProxy__factory, -} from "../../typechain"; - -import addresses from "./addresses"; -import { CommonOptions } from "./types"; -import network, { NetworkName } from "../network"; -import { DeployScript, Logger } from "../deployment/DeployScript"; - -interface OptL1DeployScriptParams { - deployer: Wallet; - admins: { proxy: string; bridge: string }; -} - -interface OptL2DeployScriptParams extends OptL1DeployScriptParams { - l2Token?: { name?: string; symbol?: string }; -} - -interface OptDeploymentOptions extends CommonOptions { - logger?: Logger; - overrides?: Overrides; -} - -export default function deployment( - networkName: NetworkName, - options: OptDeploymentOptions = {} -) { - const optAddresses = addresses(networkName, options); - return { - async erc20TokenBridgeDeployScript( - l1Token: string, - l1Params: OptL1DeployScriptParams, - l2Params: OptL2DeployScriptParams - ) { - const [ - expectedL1TokenBridgeImplAddress, - expectedL1TokenBridgeProxyAddress, - ] = await network.predictAddresses(l1Params.deployer, 2); - - const [ - expectedL2TokenImplAddress, - expectedL2TokenProxyAddress, - expectedL2TokenBridgeImplAddress, - expectedL2TokenBridgeProxyAddress, - ] = await network.predictAddresses(l2Params.deployer, 4); - - const l1DeployScript = new DeployScript( - l1Params.deployer, - options?.logger - ) - .addStep({ - factory: L1ERC20TokenBridge__factory, - args: [ - optAddresses.L1CrossDomainMessenger, - expectedL2TokenBridgeProxyAddress, - l1Token, - expectedL2TokenProxyAddress, - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL1TokenBridgeImplAddress), - }) - .addStep({ - factory: OssifiableProxy__factory, - args: [ - expectedL1TokenBridgeImplAddress, - l1Params.admins.proxy, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( - "initialize", - [l1Params.admins.bridge] - ), - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL1TokenBridgeProxyAddress), - }); - - const l1TokenInfo = IERC20Metadata__factory.connect( - l1Token, - l1Params.deployer - ); - - const [decimals, l2TokenName, l2TokenSymbol] = await Promise.all([ - l1TokenInfo.decimals(), - l2Params.l2Token?.name ?? l1TokenInfo.name(), - l2Params.l2Token?.symbol ?? l1TokenInfo.symbol(), - ]); - - const l2DeployScript = new DeployScript( - l2Params.deployer, - options?.logger - ) - .addStep({ - factory: ERC20Bridged__factory, - args: [ - l2TokenName, - l2TokenSymbol, - decimals, - expectedL2TokenBridgeProxyAddress, - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL2TokenImplAddress), - }) - .addStep({ - factory: OssifiableProxy__factory, - args: [ - expectedL2TokenImplAddress, - l2Params.admins.proxy, - ERC20Bridged__factory.createInterface().encodeFunctionData( - "initialize", - [l2TokenName, l2TokenSymbol] - ), - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL2TokenProxyAddress), - }) - .addStep({ - factory: L2ERC20TokenBridge__factory, - args: [ - optAddresses.L2CrossDomainMessenger, - expectedL1TokenBridgeProxyAddress, - l1Token, - expectedL2TokenProxyAddress, - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL2TokenBridgeImplAddress), - }) - .addStep({ - factory: OssifiableProxy__factory, - args: [ - expectedL2TokenBridgeImplAddress, - l2Params.admins.proxy, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( - "initialize", - [l2Params.admins.bridge] - ), - options?.overrides, - ], - }); - - return [l1DeployScript, l2DeployScript]; - }, - }; -} diff --git a/utils/optimism/deploymentAllFromScratch.ts b/utils/optimism/deploymentAllFromScratch.ts new file mode 100644 index 00000000..6fdfaad4 --- /dev/null +++ b/utils/optimism/deploymentAllFromScratch.ts @@ -0,0 +1,321 @@ +import { assert } from "chai"; +import { Wallet } from "ethers"; +import addresses from "./addresses"; +import { OptDeploymentOptions, DeployScriptParams } from "./types"; +import network, { NetworkName } from "../network"; +import { DeployScript, Logger } from "../deployment/DeployScript"; +import { + ERC20Bridged__factory, + ERC20RebasableBridged__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20ExtendedTokensBridge__factory, + OssifiableProxy__factory, + TokenRateOracle__factory, + TokenRateNotifier__factory, + OpStackTokenRatePusher__factory +} from "../../typechain"; + +interface OptL1DeployScriptParams extends DeployScriptParams { +} +interface OptL2DeployScriptParams extends DeployScriptParams { + l2Token?: { + name?: string; + symbol?: string + }; + l2TokenRebasable?: { + name?: string; + symbol?: string + }; +} + +export class L1DeployAllScript extends DeployScript { + + constructor( + deployer: Wallet, + bridgeImplAddress: string, + bridgeProxyAddress: string, + tokenRateNotifierImplAddress: string, + opStackTokenRatePusherImplAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.bridgeImplAddress = bridgeImplAddress; + this.bridgeProxyAddress = bridgeProxyAddress; + this.tokenRateNotifierImplAddress = tokenRateNotifierImplAddress; + this.opStackTokenRatePusherImplAddress = opStackTokenRatePusherImplAddress; + } + + public bridgeImplAddress: string; + public bridgeProxyAddress: string; + public tokenRateNotifierImplAddress: string; + public opStackTokenRatePusherImplAddress: string; +} + +export class L2DeployAllScript extends DeployScript { + + constructor( + deployer: Wallet, + tokenImplAddress: string, + tokenProxyAddress: string, + tokenRebasableImplAddress: string, + tokenRebasableProxyAddress: string, + tokenBridgeImplAddress: string, + tokenBridgeProxyAddress: string, + tokenRateOracleImplAddress: string, + tokenRateOracleProxyAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.tokenImplAddress = tokenImplAddress; + this.tokenProxyAddress = tokenProxyAddress; + this.tokenRebasableImplAddress = tokenRebasableImplAddress; + this.tokenRebasableProxyAddress = tokenRebasableProxyAddress; + this.tokenBridgeImplAddress = tokenBridgeImplAddress; + this.tokenBridgeProxyAddress = tokenBridgeProxyAddress; + this.tokenRateOracleImplAddress = tokenRateOracleImplAddress; + this.tokenRateOracleProxyAddress = tokenRateOracleProxyAddress; + } + + public tokenImplAddress: string; + public tokenProxyAddress: string; + public tokenRebasableImplAddress: string; + public tokenRebasableProxyAddress: string; + public tokenBridgeImplAddress: string; + public tokenBridgeProxyAddress: string; + public tokenRateOracleImplAddress: string; + public tokenRateOracleProxyAddress: string; +} + +/// deploys from scratch +/// - wstETH on L2 +/// - stETH on L2 +/// - bridgeL1 +/// - bridgeL2 +/// - Oracle +export default function deploymentAll( + networkName: NetworkName, + options: OptDeploymentOptions = {} +) { + const optAddresses = addresses(networkName, options); + return { + async deployAllScript( + l1Token: string, + l1TokenRebasable: string, + l1Params: OptL1DeployScriptParams, + l2Params: OptL2DeployScriptParams, + ): Promise<[L1DeployAllScript, L2DeployAllScript]> { + + const [ + expectedL1TokenBridgeImplAddress, + expectedL1TokenBridgeProxyAddress, + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + ] = await network.predictAddresses(l1Params.deployer, l1Params.contractsShift + 4); + + const [ + expectedL2TokenImplAddress, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenBridgeProxyAddress, + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress + ] = await network.predictAddresses(l2Params.deployer, l2Params.contractsShift + 8); + + const l1DeployScript = new L1DeployAllScript( + l1Params.deployer, + expectedL1TokenBridgeImplAddress, + expectedL1TokenBridgeProxyAddress, + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + options?.logger + ) + .addStep({ + factory: L1LidoTokensBridge__factory, + args: [ + optAddresses.L1CrossDomainMessenger, + expectedL2TokenBridgeProxyAddress, + l1Token, + l1TokenRebasable, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL1TokenBridgeImplAddress, + l1Params.admins.proxy, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "initialize", + [l1Params.admins.bridge] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeProxyAddress), + }) + .addStep({ + factory: TokenRateNotifier__factory, + args: [ + l1Params.deployer.address, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenRateNotifierImplAddress), + }) + .addStep({ + factory: OpStackTokenRatePusher__factory, + args: [ + optAddresses.L1CrossDomainMessenger, + l1Token, + expectedL2TokenRateOracleProxyAddress, + 1000, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1OpStackTokenRatePusherImplAddress), + }); + + const l1TokenInfo = IERC20Metadata__factory.connect( + l1Token, + l1Params.deployer + ); + + const l1TokenRebasableInfo = IERC20Metadata__factory.connect( + l1TokenRebasable, + l1Params.deployer + ); + const [decimals, l2TokenName, l2TokenSymbol, l2TokenRebasableName, l2TokenRebasableSymbol] = await Promise.all([ + l1TokenInfo.decimals(), + l2Params.l2Token?.name ?? l1TokenInfo.name(), + l2Params.l2Token?.symbol ?? l1TokenInfo.symbol(), + l2Params.l2TokenRebasable?.name ?? l1TokenRebasableInfo.name(), + l2Params.l2TokenRebasable?.symbol ?? l1TokenRebasableInfo.symbol(), + ]); + + const l2DeployScript = new L2DeployAllScript( + l2Params.deployer, + expectedL2TokenImplAddress, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenBridgeProxyAddress, + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress, + options?.logger + ) + .addStep({ + factory: ERC20Bridged__factory, + args: [ + l2TokenName, + l2TokenSymbol, + decimals, + expectedL2TokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenImplAddress, + l2Params.admins.proxy, + ERC20Bridged__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenName, l2TokenSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenProxyAddress), + }) + .addStep({ + factory: ERC20RebasableBridged__factory, + args: [ + l2TokenRebasableName, + l2TokenRebasableSymbol, + decimals, + expectedL2TokenProxyAddress, + expectedL2TokenRateOracleProxyAddress, + expectedL2TokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenRebasableImplAddress, + l2Params.admins.proxy, + ERC20RebasableBridged__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ + factory: L2ERC20ExtendedTokensBridge__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + expectedL1TokenBridgeProxyAddress, + l1Token, + l1TokenRebasable, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenBridgeImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenBridgeImplAddress, + l2Params.admins.proxy, + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( + "initialize", + [l2Params.admins.bridge] + ), + options?.overrides, + ], + }) + .addStep({ + factory: TokenRateOracle__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + expectedL2TokenBridgeProxyAddress, + expectedL1OpStackTokenRatePusherImplAddress, + 86400, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRateOracleImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenRateOracleImplAddress, + l2Params.admins.proxy, + [], + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRateOracleProxyAddress), + }); + + return [l1DeployScript as L1DeployAllScript, l2DeployScript as L2DeployAllScript]; + }, + }; +} diff --git a/utils/optimism/deploymentBridgesAndRebasableToken.ts b/utils/optimism/deploymentBridgesAndRebasableToken.ts new file mode 100644 index 00000000..f5ad4bc4 --- /dev/null +++ b/utils/optimism/deploymentBridgesAndRebasableToken.ts @@ -0,0 +1,254 @@ +import { assert } from "chai"; +import { Overrides, Wallet } from "ethers"; +import addresses from "./addresses"; +import { CommonOptions } from "./types"; +import network, { NetworkName } from "../network"; +import { DeployScript, Logger } from "../deployment/DeployScript"; +import { + ERC20Bridged__factory, + ERC20RebasableBridged__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20ExtendedTokensBridge__factory, + OssifiableProxy__factory, + } from "../../typechain"; + +interface OptL1DeployScriptParams { + deployer: Wallet; + admins: { proxy: string; bridge: string }; + contractsShift: number; +} + +interface OptL2DeployScriptParams extends OptL1DeployScriptParams { + l2Token?: { name?: string; symbol?: string }; + l2TokenRebasable?: { name?: string; symbol?: string }; +} + +interface OptDeploymentOptions extends CommonOptions { + logger?: Logger; + overrides?: Overrides; +} + +export class BridgeL1DeployScript extends DeployScript { + + constructor( + deployer: Wallet, + bridgeImplAddress: string, + bridgeProxyAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.bridgeImplAddress = bridgeImplAddress; + this.bridgeProxyAddress = bridgeProxyAddress; + } + + public bridgeImplAddress: string; + public bridgeProxyAddress: string; +} + +export class BridgeL2DeployScript extends DeployScript { + + constructor( + deployer: Wallet, + tokenImplAddress: string, + tokenProxyAddress: string, + tokenRebasableImplAddress: string, + tokenRebasableProxyAddress: string, + tokenBridgeImplAddress: string, + tokenBridgeProxyAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.tokenImplAddress = tokenImplAddress; + this.tokenProxyAddress = tokenProxyAddress; + this.tokenRebasableImplAddress = tokenRebasableImplAddress; + this.tokenRebasableProxyAddress = tokenRebasableProxyAddress; + this.tokenBridgeImplAddress = tokenBridgeImplAddress; + this.tokenBridgeProxyAddress = tokenBridgeProxyAddress; + } + + public tokenImplAddress: string; + public tokenProxyAddress: string; + public tokenRebasableImplAddress: string; + public tokenRebasableProxyAddress: string; + public tokenBridgeImplAddress: string; + public tokenBridgeProxyAddress: string; +} + +/// deploy Oracle first +/// deploys from scratch wstETH on L2, stETH on L2, bridgeL1, bridgeL2 +export default function deployment( + networkName: NetworkName, + options: OptDeploymentOptions = {} +) { + const optAddresses = addresses(networkName, options); + return { + async erc20TokenBridgeDeployScript( + l1Token: string, + l1TokenRebasable: string, + l2TokenRateOracle: string, + l1Params: OptL1DeployScriptParams, + l2Params: OptL2DeployScriptParams, + ): Promise<[BridgeL1DeployScript, BridgeL2DeployScript]> { + + const [ + expectedL1TokenBridgeImplAddress, + expectedL1TokenBridgeProxyAddress, + ] = await network.predictAddresses(l1Params.deployer, l1Params.contractsShift + 2); + + const [ + expectedL2TokenImplAddress, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenBridgeProxyAddress, + ] = await network.predictAddresses(l2Params.deployer, l2Params.contractsShift + 6); + + const l1DeployScript = new BridgeL1DeployScript( + l1Params.deployer, + expectedL1TokenBridgeImplAddress, + expectedL1TokenBridgeProxyAddress, + options?.logger + ) + .addStep({ + factory: L1LidoTokensBridge__factory, + args: [ + optAddresses.L1CrossDomainMessenger, + expectedL2TokenBridgeProxyAddress, + l1Token, + l1TokenRebasable, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL1TokenBridgeImplAddress, + l1Params.admins.proxy, + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( + "initialize", + [l1Params.admins.bridge] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeProxyAddress), + }); + + const l1TokenInfo = IERC20Metadata__factory.connect( + l1Token, + l1Params.deployer + ); + + const l1TokenRebasableInfo = IERC20Metadata__factory.connect( + l1TokenRebasable, + l1Params.deployer + ); + const [decimals, l2TokenName, l2TokenSymbol, l2TokenRebasableName, l2TokenRebasableSymbol] = await Promise.all([ + l1TokenInfo.decimals(), + l2Params.l2Token?.name ?? l1TokenInfo.name(), + l2Params.l2Token?.symbol ?? l1TokenInfo.symbol(), + l2Params.l2TokenRebasable?.name ?? l1TokenRebasableInfo.name(), + l2Params.l2TokenRebasable?.symbol ?? l1TokenRebasableInfo.symbol(), + ]); + + const l2DeployScript = new BridgeL2DeployScript( + l2Params.deployer, + expectedL2TokenImplAddress, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenBridgeProxyAddress, + options?.logger + ) + .addStep({ + factory: ERC20Bridged__factory, + args: [ + l2TokenName, + l2TokenSymbol, + decimals, + expectedL2TokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenImplAddress, + l2Params.admins.proxy, + ERC20Bridged__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenName, l2TokenSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenProxyAddress), + }) + .addStep({ + factory: ERC20RebasableBridged__factory, + args: [ + l2TokenRebasableName, + l2TokenRebasableSymbol, + decimals, + expectedL2TokenProxyAddress, + l2TokenRateOracle, + expectedL2TokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenRebasableImplAddress, + l2Params.admins.proxy, + ERC20RebasableBridged__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ + factory: L2ERC20ExtendedTokensBridge__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + expectedL1TokenBridgeProxyAddress, + l1Token, + l1TokenRebasable, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenBridgeImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenBridgeImplAddress, + l2Params.admins.proxy, + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( + "initialize", + [l2Params.admins.bridge] + ), + options?.overrides, + ], + }); + + return [l1DeployScript as BridgeL1DeployScript, l2DeployScript as BridgeL2DeployScript]; + }, + }; +} diff --git a/utils/optimism/deploymentNewImplementations.ts b/utils/optimism/deploymentNewImplementations.ts new file mode 100644 index 00000000..776fe8df --- /dev/null +++ b/utils/optimism/deploymentNewImplementations.ts @@ -0,0 +1,229 @@ +import { assert } from "chai"; +import { Wallet } from "ethers"; +import addresses from "./addresses"; +import { OptDeploymentOptions, DeployScriptParams } from "./types"; +import network, { NetworkName } from "../network"; +import { DeployScript, Logger } from "../deployment/DeployScript"; +import { + ERC20Bridged__factory, + ERC20RebasableBridged__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20ExtendedTokensBridge__factory, + OssifiableProxy__factory, + TokenRateOracle__factory +} from "../../typechain"; + +interface OptL1DeployScriptParams extends DeployScriptParams { + tokenProxyAddress: string; + tokenRebasableProxyAddress: string; + opStackTokenRatePusherImplAddress: string; + tokenBridgeProxyAddress: string; + deployer: Wallet; + admins: { + proxy: string; + bridge: string + }; + contractsShift: number; +} + +interface OptL2DeployScriptParams extends DeployScriptParams { + tokenBridgeProxyAddress: string; + tokenProxyAddress: string; + tokenRateOracleProxyAddress: string; + tokenRateOracleRateOutdatedDelay: number; + token?: { + name?: string; + symbol?: string + }; + tokenRebasable?: { + name?: string; + symbol?: string + }; +} + +export class BridgeL1DeployScript extends DeployScript { + + constructor( + deployer: Wallet, + bridgeImplAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.bridgeImplAddress = bridgeImplAddress; + } + + public bridgeImplAddress: string; +} + +export class BridgeL2DeployScript extends DeployScript { + + constructor( + deployer: Wallet, + tokenImplAddress: string, + tokenRebasableImplAddress: string, + tokenRebasableProxyAddress: string, + tokenBridgeImplAddress: string, + tokenRateOracleImplAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.tokenImplAddress = tokenImplAddress; + this.tokenRebasableImplAddress = tokenRebasableImplAddress; + this.tokenRebasableProxyAddress = tokenRebasableProxyAddress; + this.tokenBridgeImplAddress = tokenBridgeImplAddress; + this.tokenRateOracleImplAddress = tokenRateOracleImplAddress; + } + + public tokenImplAddress: string; + public tokenRebasableImplAddress: string; + public tokenRebasableProxyAddress: string; + public tokenBridgeImplAddress: string; + public tokenRateOracleImplAddress: string; +} + +/// deploys +/// - new L1Bridge Impl +/// - new L2Bridge Impl +/// - RebasableToken(stETH) Impl and Proxy (because it was never deployed before) +/// - Non-rebasable token (wstETH) new Impl with Permissions +export default function deploymentNewImplementations( + networkName: NetworkName, + options: OptDeploymentOptions = {} +) { + const optAddresses = addresses(networkName, options); + return { + async deployScript( + l1Params: OptL1DeployScriptParams, + l2Params: OptL2DeployScriptParams, + ): Promise<[BridgeL1DeployScript, BridgeL2DeployScript]> { + + const [ + expectedL1TokenBridgeImplAddress, + ] = await network.predictAddresses(l1Params.deployer, l1Params.contractsShift + 1); + + const [ + expectedL2TokenImplAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenRateOracleImplAddress + ] = await network.predictAddresses(l2Params.deployer, l2Params.contractsShift + 5); + + const l1DeployScript = new BridgeL1DeployScript( + l1Params.deployer, + expectedL1TokenBridgeImplAddress, + options?.logger + ) + .addStep({ + factory: L1LidoTokensBridge__factory, + args: [ + optAddresses.L1CrossDomainMessenger, + l2Params.tokenBridgeProxyAddress, + l1Params.tokenProxyAddress, + l1Params.tokenRebasableProxyAddress, + l2Params.tokenProxyAddress, + expectedL2TokenRebasableProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenBridgeImplAddress), + }); + + const l1TokenInfo = IERC20Metadata__factory.connect( + l1Params.tokenProxyAddress, + l1Params.deployer + ); + + const l1TokenRebasableInfo = IERC20Metadata__factory.connect( + l1Params.tokenRebasableProxyAddress, + l1Params.deployer + ); + const [decimals, l2TokenName, l2TokenSymbol, l2TokenRebasableName, l2TokenRebasableSymbol] = await Promise.all([ + l1TokenInfo.decimals(), + l2Params.token?.name ?? l1TokenInfo.name(), + l2Params.token?.symbol ?? l1TokenInfo.symbol(), + l2Params.tokenRebasable?.name ?? l1TokenRebasableInfo.name(), + l2Params.tokenRebasable?.symbol ?? l1TokenRebasableInfo.symbol(), + ]); + + const l2DeployScript = new BridgeL2DeployScript( + l2Params.deployer, + expectedL2TokenImplAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenRateOracleImplAddress, + options?.logger + ) + .addStep({ + factory: ERC20Bridged__factory, + args: [ + l2TokenName, + l2TokenSymbol, + decimals, + l2Params.tokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenImplAddress), + }) + .addStep({ + factory: ERC20RebasableBridged__factory, + args: [ + l2TokenRebasableName, + l2TokenRebasableSymbol, + decimals, + l2Params.tokenProxyAddress, + l2Params.tokenRateOracleProxyAddress, + l2Params.tokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenRebasableImplAddress, + l2Params.admins.proxy, + ERC20RebasableBridged__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ + factory: L2ERC20ExtendedTokensBridge__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + l1Params.tokenBridgeProxyAddress, + l1Params.tokenProxyAddress, + l1Params.tokenRebasableProxyAddress, + l2Params.tokenProxyAddress, + expectedL2TokenRebasableProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenBridgeImplAddress), + }) + .addStep({ + factory: TokenRateOracle__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + l2Params.tokenBridgeProxyAddress, + l1Params.opStackTokenRatePusherImplAddress, + l2Params.tokenRateOracleRateOutdatedDelay, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRateOracleImplAddress), + }); + + return [l1DeployScript as BridgeL1DeployScript, l2DeployScript as BridgeL2DeployScript]; + }, + }; +} diff --git a/utils/optimism/deploymentOracle.ts b/utils/optimism/deploymentOracle.ts new file mode 100644 index 00000000..5be8d466 --- /dev/null +++ b/utils/optimism/deploymentOracle.ts @@ -0,0 +1,133 @@ +import { assert } from "chai"; +import { Wallet } from "ethers"; +import { ethers } from "hardhat"; +import addresses from "./addresses"; +import { DeployScriptParams, OptDeploymentOptions } from "./types"; +import network, { NetworkName } from "../network"; +import { DeployScript, Logger } from "../deployment/DeployScript"; +import { + OssifiableProxy__factory, + TokenRateOracle__factory, + TokenRateNotifier__factory, + OpStackTokenRatePusher__factory +} from "../../typechain"; + +interface OptDeployScriptParams extends DeployScriptParams {} +export class OracleL1DeployScript extends DeployScript { + constructor( + deployer: Wallet, + tokenRateNotifierImplAddress: string, + opStackTokenRatePusherImplAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.tokenRateNotifierImplAddress = tokenRateNotifierImplAddress; + this.opStackTokenRatePusherImplAddress = opStackTokenRatePusherImplAddress; + } + + public tokenRateNotifierImplAddress: string; + public opStackTokenRatePusherImplAddress: string; +} + +export class OracleL2DeployScript extends DeployScript { + constructor( + deployer: Wallet, + tokenRateOracleImplAddress: string, + tokenRateOracleProxyAddress: string, + logger?: Logger + ) { + super(deployer, logger); + this.tokenRateOracleImplAddress = tokenRateOracleImplAddress; + this.tokenRateOracleProxyAddress = tokenRateOracleProxyAddress; + } + + public tokenRateOracleImplAddress: string; + public tokenRateOracleProxyAddress: string; +} + +export default function deploymentOracle( + networkName: NetworkName, + options: OptDeploymentOptions = {} +) { + const optAddresses = addresses(networkName, options); + return { + async oracleDeployScript( + l1Token: string, + l2GasLimitForPushingTokenRate: number, + tokenRateOutdatedDelay: number, + l1Params: OptDeployScriptParams, + l2Params: OptDeployScriptParams, + ): Promise<[OracleL1DeployScript, OracleL2DeployScript]> { + + const [ + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + ] = await network.predictAddresses(l1Params.deployer, 2); + + const [ + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress + ] = await network.predictAddresses(l2Params.deployer, 2); + + const l1DeployScript = new OracleL1DeployScript( + l1Params.deployer, + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + options?.logger + ) + .addStep({ + factory: TokenRateNotifier__factory, + args: [ + l1Params.deployer.address, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1TokenRateNotifierImplAddress), + }) + .addStep({ + factory: OpStackTokenRatePusher__factory, + args: [ + optAddresses.L1CrossDomainMessenger, + l1Token, + expectedL2TokenRateOracleProxyAddress, + l2GasLimitForPushingTokenRate, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL1OpStackTokenRatePusherImplAddress), + }); + + const l2DeployScript = new OracleL2DeployScript( + l2Params.deployer, + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress, + options?.logger + ) + .addStep({ + factory: TokenRateOracle__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + ethers.constants.AddressZero, + expectedL1OpStackTokenRatePusherImplAddress, + tokenRateOutdatedDelay, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRateOracleImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenRateOracleImplAddress, + l2Params.admins.proxy, + [], + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRateOracleProxyAddress), + }); + + return [l1DeployScript as OracleL1DeployScript, l2DeployScript as OracleL2DeployScript]; + }, + }; +} diff --git a/utils/optimism/index.ts b/utils/optimism/index.ts index 0ca0e9a2..9b00eed7 100644 --- a/utils/optimism/index.ts +++ b/utils/optimism/index.ts @@ -1,6 +1,7 @@ import addresses from "./addresses"; import contracts from "./contracts"; -import deployment from "./deployment"; +import deployment from "./deploymentBridgesAndRebasableToken"; +import deploymentOracle from "./deploymentOracle"; import testing from "./testing"; import messaging from "./messaging"; @@ -10,4 +11,6 @@ export default { contracts, messaging, deployment, + deploymentOracle }; + diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 635d2e6a..af114de0 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -5,17 +5,20 @@ import { IERC20, ERC20Bridged, IERC20__factory, - L1ERC20TokenBridge, - L2ERC20TokenBridge, + L1LidoTokensBridge, + L2ERC20ExtendedTokensBridge, ERC20Bridged__factory, ERC20BridgedStub__factory, - L1ERC20TokenBridge__factory, - L2ERC20TokenBridge__factory, + ERC20WrapperStub__factory, + TokenRateOracle__factory, + L1LidoTokensBridge__factory, + L2ERC20ExtendedTokensBridge__factory, CrossDomainMessengerStub__factory, + ERC20RebasableBridged__factory, } from "../../typechain"; import addresses from "./addresses"; import contracts from "./contracts"; -import deployment from "./deployment"; +import deploymentAll from "./deploymentAllFromScratch"; import testingUtils from "../testing"; import { BridgingManagement } from "../bridging-management"; import network, { NetworkName, SignerOrProvider } from "../network"; @@ -55,11 +58,11 @@ export default function testing(networkName: NetworkName) { ? await loadDeployedBridges(ethProvider, optProvider) : await deployTestBridge(networkName, ethProvider, optProvider); - const [l1ERC20TokenBridgeAdminAddress] = - await BridgingManagement.getAdmins(bridgeContracts.l1ERC20TokenBridge); + const [l1ERC20ExtendedTokensAdminAddress] = + await BridgingManagement.getAdmins(bridgeContracts.l1LidoTokensBridge); - const [l2ERC20TokenBridgeAdminAddress] = - await BridgingManagement.getAdmins(bridgeContracts.l2ERC20TokenBridge); + const [l2ERC20ExtendedTokensBridgeAdminAddress] = + await BridgingManagement.getAdmins(bridgeContracts.l2ERC20ExtendedTokensBridge); const l1TokensHolder = hasDeployedContracts ? await testingUtils.impersonate( @@ -79,13 +82,13 @@ export default function testing(networkName: NetworkName) { // if the L1 bridge admin is a contract, remove it's code to // make it behave as EOA await ethProvider.send("hardhat_setCode", [ - l1ERC20TokenBridgeAdminAddress, + l1ERC20ExtendedTokensAdminAddress, "0x", ]); // same for the L2 bridge admin await optProvider.send("hardhat_setCode", [ - l2ERC20TokenBridgeAdminAddress, + l2ERC20ExtendedTokensBridgeAdminAddress, "0x", ]); @@ -98,12 +101,12 @@ export default function testing(networkName: NetworkName) { ...bridgeContracts, l1CrossDomainMessenger: optContracts.L1CrossDomainMessengerStub, l2CrossDomainMessenger: optContracts.L2CrossDomainMessenger, - l1ERC20TokenBridgeAdmin: await testingUtils.impersonate( - l1ERC20TokenBridgeAdminAddress, + l1ERC20ExtendedTokensBridgeAdmin: await testingUtils.impersonate( + l1ERC20ExtendedTokensAdminAddress, ethProvider ), - l2ERC20TokenBridgeAdmin: await testingUtils.impersonate( - l2ERC20TokenBridgeAdminAddress, + l2ERC20ExtendedTokensBridgeAdmin: await testingUtils.impersonate( + l2ERC20ExtendedTokensBridgeAdminAddress, optProvider ) }; @@ -152,15 +155,22 @@ async function loadDeployedBridges( l2SignerOrProvider: SignerOrProvider ) { return { - l1Token: IERC20__factory.connect( + l1Token: ERC20WrapperStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), + l1TokenRebasable: IERC20__factory.connect( + testingUtils.env.OPT_L1_REBASABLE_TOKEN(), + l1SignerOrProvider + ), + ...connectBridgeContracts( { + tokenRateOracle: testingUtils.env.OPT_L2_TOKEN_RATE_ORACLE(), l2Token: testingUtils.env.OPT_L2_TOKEN(), - l1ERC20TokenBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), - l2ERC20TokenBridge: testingUtils.env.OPT_L2_ERC20_TOKEN_BRIDGE(), + l2TokenRebasable: testingUtils.env.OPT_L2_REBASABLE_TOKEN(), + l1LidoTokensBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), + l2ERC20ExtendedTokensBridge: testingUtils.env.OPT_L2_ERC20_TOKEN_BRIDGE(), }, l1SignerOrProvider, l2SignerOrProvider @@ -176,37 +186,44 @@ async function deployTestBridge( const ethDeployer = testingUtils.accounts.deployer(ethProvider); const optDeployer = testingUtils.accounts.deployer(optProvider); - const l1Token = await new ERC20BridgedStub__factory(ethDeployer).deploy( + const l1TokenRebasable = await new ERC20BridgedStub__factory(ethDeployer).deploy( + "Test Token Rebasable", + "TTR" + ); + + const l1Token = await new ERC20WrapperStub__factory(ethDeployer).deploy( + l1TokenRebasable.address, "Test Token", "TT" ); - const [ethDeployScript, optDeployScript] = await deployment( + const [ethDeployScript, optDeployScript] = await deploymentAll( networkName - ).erc20TokenBridgeDeployScript( + ).deployAllScript( l1Token.address, + l1TokenRebasable.address, { deployer: ethDeployer, admins: { proxy: ethDeployer.address, bridge: ethDeployer.address }, + contractsShift: 0 }, { deployer: optDeployer, admins: { proxy: optDeployer.address, bridge: optDeployer.address }, + contractsShift: 0 } ); await ethDeployScript.run(); await optDeployScript.run(); - const l1ERC20TokenBridgeProxyDeployStepIndex = 1; const l1BridgingManagement = new BridgingManagement( - ethDeployScript.getContractAddress(l1ERC20TokenBridgeProxyDeployStepIndex), + ethDeployScript.bridgeProxyAddress, ethDeployer ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 3; const l2BridgingManagement = new BridgingManagement( - optDeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + optDeployScript.tokenBridgeProxyAddress, optDeployer ); @@ -224,11 +241,14 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), + l1TokenRebasable: l1TokenRebasable.connect(ethProvider), ...connectBridgeContracts( { - l2Token: optDeployScript.getContractAddress(1), - l1ERC20TokenBridge: ethDeployScript.getContractAddress(1), - l2ERC20TokenBridge: optDeployScript.getContractAddress(3), + tokenRateOracle: optDeployScript.tokenRateOracleProxyAddress, + l2Token: optDeployScript.tokenProxyAddress, + l2TokenRebasable: optDeployScript.tokenRebasableProxyAddress, + l1LidoTokensBridge: ethDeployScript.bridgeProxyAddress, + l2ERC20ExtendedTokensBridge: optDeployScript.tokenBridgeProxyAddress }, ethProvider, optProvider @@ -238,29 +258,42 @@ async function deployTestBridge( function connectBridgeContracts( addresses: { + tokenRateOracle: string; l2Token: string; - l1ERC20TokenBridge: string; - l2ERC20TokenBridge: string; + l2TokenRebasable: string; + l1LidoTokensBridge: string; + l2ERC20ExtendedTokensBridge: string; }, ethSignerOrProvider: SignerOrProvider, optSignerOrProvider: SignerOrProvider ) { - const l1ERC20TokenBridge = L1ERC20TokenBridge__factory.connect( - addresses.l1ERC20TokenBridge, + + const l1LidoTokensBridge = L1LidoTokensBridge__factory.connect( + addresses.l1LidoTokensBridge, ethSignerOrProvider ); - const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( - addresses.l2ERC20TokenBridge, + const l2ERC20ExtendedTokensBridge = L2ERC20ExtendedTokensBridge__factory.connect( + addresses.l2ERC20ExtendedTokensBridge, optSignerOrProvider ); const l2Token = ERC20Bridged__factory.connect( addresses.l2Token, optSignerOrProvider ); + const l2TokenRebasable = ERC20RebasableBridged__factory.connect( + addresses.l2TokenRebasable, + optSignerOrProvider + ); + const tokenRateOracle = TokenRateOracle__factory.connect( + addresses.tokenRateOracle, + optSignerOrProvider + ); return { + tokenRateOracle, l2Token, - l1ERC20TokenBridge, - l2ERC20TokenBridge, + l2TokenRebasable, + l1LidoTokensBridge, + l2ERC20ExtendedTokensBridge }; } @@ -269,8 +302,8 @@ async function printLoadedTestConfig( bridgeContracts: { l1Token: IERC20; l2Token: ERC20Bridged; - l1ERC20TokenBridge: L1ERC20TokenBridge; - l2ERC20TokenBridge: L2ERC20TokenBridge; + l1LidoTokensBridge: L1LidoTokensBridge; + l2ERC20ExtendedTokensBridge: L2ERC20ExtendedTokensBridge; }, l1TokensHolder?: Signer ) { @@ -290,10 +323,10 @@ async function printLoadedTestConfig( console.log(` · L1 Tokens Holder Balance: ${holderBalance.toString()}`); } console.log( - ` · L1 ERC20 Token Bridge: ${bridgeContracts.l1ERC20TokenBridge.address}` + ` · L1 ERC20 Token Bridge: ${bridgeContracts.l1LidoTokensBridge.address}` ); console.log( - ` · L2 ERC20 Token Bridge: ${bridgeContracts.l2ERC20TokenBridge.address}` + ` · L2 ERC20 Token Bridge: ${bridgeContracts.l2ERC20ExtendedTokensBridge.address}` ); console.log(); } diff --git a/utils/optimism/types.ts b/utils/optimism/types.ts index 38cea940..461be4c1 100644 --- a/utils/optimism/types.ts +++ b/utils/optimism/types.ts @@ -1,3 +1,6 @@ +import { Overrides, Wallet } from "ethers"; +import { Logger } from "../deployment/DeployScript"; + export type OptContractNames = | "L1CrossDomainMessenger" | "L2CrossDomainMessenger"; @@ -7,3 +10,17 @@ export type CustomOptContractAddresses = Partial; export interface CommonOptions { customAddresses?: CustomOptContractAddresses; } + +export interface DeployScriptParams { + deployer: Wallet; + admins: { + proxy: string; + bridge: string + }; + contractsShift: number; +} + +export interface OptDeploymentOptions extends CommonOptions { + logger?: Logger; + overrides?: Overrides; +} diff --git a/utils/testing/e2e.ts b/utils/testing/e2e.ts index 010596e7..203633e5 100644 --- a/utils/testing/e2e.ts +++ b/utils/testing/e2e.ts @@ -6,18 +6,18 @@ const abiCoder = ethers.utils.defaultAbiCoder; export const E2E_TEST_CONTRACTS_OPTIMISM = { l1: { - l1Token: "0xaF8a2F0aE374b03376155BF745A3421Dac711C12", - l1LDOToken: "0xcAdf242b97BFdD1Cb4Fd282E5FcADF965883065f", - l1ERC20TokenBridge: "0x2DD0CD60b6048549ab576f06BC20EC53B457244E", - aragonVoting: "0x86f4C03aB9fCE83970Ce3FC7C23f25339f484EE5", - tokenManager: "0x4A63e41611B7c70DA6f42a806dFBcECB0B2D314F", - agent: "0x80720229bdB8caf9f67ddf871e98a76655A39AFe", - l1CrossDomainMessenger: "0x4361d0F75A0186C05f971c566dC6bEa5957483fD", + l1Token: "0xB82381A3fBD3FaFA77B3a7bE693342618240067b", + l1LDOToken: "0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB", + l1ERC20ExtendedTokensBridge: "0x4Abf633d9c0F4aEebB4C2E3213c7aa1b8505D332", + aragonVoting: "0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A", + tokenManager: "0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66", + agent: "0x32A0E5828B62AAb932362a4816ae03b860b65e83", + l1CrossDomainMessenger: "0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef", }, l2: { - l2Token: "0x4c2ECf847C89d5De3187F1b0081E4dcdBe063C68", - l2ERC20TokenBridge: "0x0A5c6AB7B41E066b5C40907dd06063703be21B19", - govBridgeExecutor: "0x2365F00fFD70958EC2c20B601D501e4b75798D93", + l2Token: "0x24B47cd3A74f1799b32B2de11073764Cb1bb318B", + l2ERC20ExtendedTokensBridge: "0xdBA2760246f315203F8B716b3a7590F0FFdc704a", + govBridgeExecutor: "0xf695357C66bA514150Da95b189acb37b46DDe602", }, }; diff --git a/utils/testing/env.ts b/utils/testing/env.ts index faf8acd6..0f61419e 100644 --- a/utils/testing/env.ts +++ b/utils/testing/env.ts @@ -33,6 +33,18 @@ export default { OPT_L2_TOKEN() { return env.address("TESTING_OPT_L2_TOKEN"); }, + OPT_L1_TOKEN_RATE_NOTIFIER() { + return env.address("TESTING_OPT_L1_TOKEN_RATE_NOTIFIER"); + }, + OPT_L2_TOKEN_RATE_ORACLE() { + return env.address("TESTING_OPT_L2_TOKEN_RATE_ORACLE"); + }, + OPT_L1_REBASABLE_TOKEN() { + return env.address("TESTING_OPT_L1_REBASABLE_TOKEN"); + }, + OPT_L2_REBASABLE_TOKEN() { + return env.address("TESTING_OPT_L2_REBASABLE_TOKEN"); + }, OPT_L1_ERC20_TOKEN_BRIDGE() { return env.address("TESTING_OPT_L1_ERC20_TOKEN_BRIDGE"); }, diff --git a/utils/testing/permit-helpers.ts b/utils/testing/permit-helpers.ts new file mode 100644 index 00000000..16531c79 --- /dev/null +++ b/utils/testing/permit-helpers.ts @@ -0,0 +1,116 @@ +import { BigNumberish, Signer } from "ethers"; +import { ExternallyOwnedAccount } from "@ethersproject/abstract-signer"; + +import { keccak256, toUtf8Bytes, defaultAbiCoder } from "ethers/lib/utils"; +import { ecsign as ecSignBuf } from "ethereumjs-util"; + +const PERMIT_TYPE_HASH = streccak( + 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' +) +const TRANSFER_WITH_AUTHORIZATION_TYPE_HASH = streccak( + 'TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)' +) + +interface Eip1271Contract { + address: string; + sign( + hash: string + ): Promise<[string, string, string] & { v: string; r: string; s: string }>; +} + +async function signEOA(digest: string, account: ExternallyOwnedAccount) { + return ecSign(digest, account.privateKey) +} + +async function signEIP1271(digest: string, eip1271Contract: Eip1271Contract) { + const sig = await eip1271Contract.sign(digest) + return { v: sig.v, r: sig.r, s: sig.s } +} + +export async function signEOAorEIP1271(digest: string, signer: Eip1271Contract | ExternallyOwnedAccount) { + if (signer.hasOwnProperty('sign')) { + return await signEIP1271(digest, signer as Eip1271Contract); + } else { + return await signEOA(digest, signer as ExternallyOwnedAccount); + } +} + +export function makeDomainSeparator(name: string, version: string, chainId: BigNumberish, verifyingContract: string) { + return keccak256( + defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], + [ + streccak('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + streccak(name), + streccak(version), + chainId, + verifyingContract, + ] + ) + ) +} + +export async function signPermit( + owner: string, + signer: ExternallyOwnedAccount | Eip1271Contract, + spender: string, + value: number, + deadline: string, + nonce: number, + domainSeparator: string +) { + const digest = calculatePermitDigest(owner, spender, value, nonce, deadline, domainSeparator) + return await signEOAorEIP1271(digest, signer) +} + +export function calculatePermitDigest(owner: string, spender: string, value: number, nonce: number, deadline: string, domainSeparator: string) { + return calculateEIP712Digest( + domainSeparator, + PERMIT_TYPE_HASH, + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [owner, spender, value, nonce, deadline] + ) +} + +export function calculateTransferAuthorizationDigest(from: string, to: string, value: number, validAfter: string, validBefore: string, nonce: string, domainSeparator: string) { + return calculateEIP712Digest( + domainSeparator, + TRANSFER_WITH_AUTHORIZATION_TYPE_HASH, + ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes32'], + [from, to, value, validAfter, validBefore, nonce] + ) +} + +function calculateEIP712Digest(domainSeparator: string, typeHash: string, types: string[], parameters: unknown[]) { + const structHash = keccak256(defaultAbiCoder.encode(['bytes32', ...types], [typeHash, ...parameters])); + const data = '0x1901' + strip0x(domainSeparator) + strip0x(structHash) + return keccak256(data) +} + +function ecSign(digest: string, privateKey: string) { + const { v, r, s } = ecSignBuf(bufferFromHexString(digest), bufferFromHexString(privateKey)) + return { v, r: hexStringFromBuffer(r), s: hexStringFromBuffer(s) } +} + +function strip0x(s: string) { + return s.substr(0, 2) === '0x' ? s.substr(2) : s +} + + +function hex(n: number, byteLen = undefined) { + const s = n.toString(16) + return byteLen === undefined ? s : s.padStart(byteLen * 2, '0') +} + + +export function streccak(s: string) { + return keccak256(toUtf8Bytes(s)); +} + +function hexStringFromBuffer(buf: Buffer) { + return '0x' + buf.toString('hex') +} + +function bufferFromHexString(hex: string) { + return Buffer.from(strip0x(hex), 'hex') +} diff --git a/utils/testing/unit.ts b/utils/testing/unit.ts index a282e77e..f6d83c95 100644 --- a/utils/testing/unit.ts +++ b/utils/testing/unit.ts @@ -5,7 +5,7 @@ export function unit(title: string, ctxFactory: CtxFactory) return new UnitTest(title, ctxFactory); } -class UnitTest { +export class UnitTest { public readonly title: string; private readonly ctxFactory: CtxFactory;