From 3256d9f5ea087810c559c51c0116ceb2d686bfce Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 10 Oct 2023 08:15:13 +0200 Subject: [PATCH 01/61] create new contract that represents stETH on L2 --- contracts/token/StETH.sol | 51 +++++++++++++++++++++++++++ contracts/token/interfaces/IStETH.sol | 39 ++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 contracts/token/StETH.sol create mode 100644 contracts/token/interfaces/IStETH.sol diff --git a/contracts/token/StETH.sol b/contracts/token/StETH.sol new file mode 100644 index 00000000..be0a36e7 --- /dev/null +++ b/contracts/token/StETH.sol @@ -0,0 +1,51 @@ +// 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 {IStETH} from "./interfaces/IStETH.sol"; +import {ERC20Bridged} from "./ERC20Bridged.sol"; + +/// @author kovalgek +/// @notice Extends the ERC20Bridged functionality +contract StETH is ERC20Bridged, IStETH { + + IERC20 public wstETH; + + /// @param wstETH_ address of the WstETH token to wrap + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param decimals_ The decimals places of the token + /// @param bridge_ The bridge address which allowd to mint/burn tokens + constructor( + IERC20 wstETH_, + string memory name_, + string memory symbol_, + uint8 decimals_, + address bridge_ + ) ERC20Bridged(name_, symbol_, decimals_, bridge_) { + wstETH = wstETH_; + } + + function wstETH_to_stETH_rate() public pure returns (uint256) { + return 2; + } + + function wrap(uint256 wstETHAmount_) external returns (uint256) { + require(wstETHAmount_ > 0, "stETH: can't wrap zero wstETH"); + uint256 stETHAmount = wstETHAmount_ / wstETH_to_stETH_rate(); + _mint(msg.sender, stETHAmount); + wstETH.transferFrom(msg.sender, address(this), wstETHAmount_); + return stETHAmount; + } + + function unwrap(uint256 stETHAmount_) external returns (uint256) { + require(stETHAmount_ > 0, "stETH: zero amount unwrap not allowed"); + uint256 wstETHAmount = stETHAmount_ * wstETH_to_stETH_rate(); + _burn(msg.sender, stETHAmount_); + wstETH.transfer(msg.sender, wstETHAmount); + return wstETHAmount; + } +} \ No newline at end of file diff --git a/contracts/token/interfaces/IStETH.sol b/contracts/token/interfaces/IStETH.sol new file mode 100644 index 00000000..a6acdaf2 --- /dev/null +++ b/contracts/token/interfaces/IStETH.sol @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + + +/// @author kovalgek +/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens +interface IStETH { + function wrap(uint256 _stETHAmount) external returns (uint256); + function unwrap(uint256 _wstETHAmount) external returns (uint256); + + + // /** + // * @notice Get amount of wstETH for a given amount of stETH + // * @param _stETHAmount amount of stETH + // * @return Amount of wstETH for a given stETH amount + // */ + // function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); + + // /** + // * @notice Get amount of stETH for a given amount of wstETH + // * @param _wstETHAmount amount of wstETH + // * @return Amount of stETH for a given wstETH amount + // */ + // function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256); + + // /** + // * @notice Get amount of stETH for a one wstETH + // * @return Amount of stETH for 1 wstETH + // */ + // function stEthPerToken() external view returns (uint256); + + // /** + // * @notice Get amount of wstETH for a one stETH + // * @return Amount of wstETH for a 1 stETH + // */ + // function tokensPerStEth() external view returns (uint256); +} \ No newline at end of file From e4b2cbd987e603a4a51b7a903a60b544daa7603e Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 11 Oct 2023 17:56:21 +0200 Subject: [PATCH 02/61] add wrap/unwrap functions --- contracts/token/ERC20Rebasable.sol | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 contracts/token/ERC20Rebasable.sol diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol new file mode 100644 index 00000000..7bdc718e --- /dev/null +++ b/contracts/token/ERC20Rebasable.sol @@ -0,0 +1,65 @@ +// 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 {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; +import {ITokensRateOracle} from "./interfaces/ITokensRateOracle.sol"; + +import {ERC20Core} from "./ERC20Core.sol"; +import {ERC20Metadata} from "./ERC20Metadata.sol"; + +/// @author kovalgek +/// @notice Extends the ERC20Core functionality +contract ERC20Rebasable is IERC20Wrapable, ERC20Core, ERC20Metadata { + + IERC20 public immutable wrappedToken; + ITokensRateOracle public immutable tokensRateOracle; + + /// @param wrappedToken_ address of the ERC20 token to wrap + /// @param tokensRateOracle_ address of oracle that returns tokens rate + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param decimals_ The decimals places of the token + constructor( + IERC20 wrappedToken_, + ITokensRateOracle tokensRateOracle_, + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20Metadata(name_, symbol_, decimals_) { + wrappedToken = wrappedToken_; + tokensRateOracle = tokensRateOracle_; + } + + /// @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 IERC20Wrapable + function wrap(uint256 wstETHAmount_) external returns (uint256) { + require(wstETHAmount_ > 0, "stETH: can't wrap zero wstETH"); + uint256 stETHAmount = wstETHAmount_ / tokensRateOracle.wstETH_to_stETH_rate(); // check how to divide. + // + _mint(msg.sender, stETHAmount); + wrappedToken.transferFrom(msg.sender, address(this), wstETHAmount_); + return stETHAmount; + } + + // tests when rate is different <1, >1. + + /// @inheritdoc IERC20Wrapable + function unwrap(uint256 stETHAmount_) external returns (uint256) { + require(stETHAmount_ > 0, "stETH: zero amount unwrap not allowed"); + uint256 wstETHAmount = stETHAmount_ * tokensRateOracle.wstETH_to_stETH_rate(); + _burn(msg.sender, stETHAmount_); + wrappedToken.transfer(msg.sender, wstETHAmount); + return wstETHAmount; + } +} \ No newline at end of file From 82038ea0b823a44dd6c86cff146a52d951c95ec6 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 13 Oct 2023 11:54:40 +0200 Subject: [PATCH 03/61] add shares to rebasable token --- contracts/stubs/TokensRateOracleStub.sol | 41 +++ contracts/token/ERC20Rebasable.sol | 260 ++++++++++++++++-- contracts/token/StETH.sol | 51 ---- .../{IStETH.sol => IERC20Wrapable.sol} | 32 ++- .../token/interfaces/ITokensRateOracle.sol | 28 ++ 5 files changed, 335 insertions(+), 77 deletions(-) create mode 100644 contracts/stubs/TokensRateOracleStub.sol delete mode 100644 contracts/token/StETH.sol rename contracts/token/interfaces/{IStETH.sol => IERC20Wrapable.sol} (53%) create mode 100644 contracts/token/interfaces/ITokensRateOracle.sol diff --git a/contracts/stubs/TokensRateOracleStub.sol b/contracts/stubs/TokensRateOracleStub.sol new file mode 100644 index 00000000..15771d72 --- /dev/null +++ b/contracts/stubs/TokensRateOracleStub.sol @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokensRateOracle} from "../token/interfaces/ITokensRateOracle.sol"; + +contract TokensRateOracleStub is ITokensRateOracle { + + uint8 public _decimals; + + function setDecimals(uint8 decimals_) external { + _decimals = decimals_; + } + + function decimals() external view returns (uint8) { + return _decimals; + } + + int256 public latestRoundDataAnswer; + + function setLatestRoundDataAnswer(int256 answer_) external { + latestRoundDataAnswer = answer_; + } + + /** + * @notice get data about the latest round. + */ + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + return (0,latestRoundDataAnswer,0,0,0); + } +} \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 7bdc718e..219dfd44 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -4,16 +4,13 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; import {ITokensRateOracle} from "./interfaces/ITokensRateOracle.sol"; - -import {ERC20Core} from "./ERC20Core.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; /// @author kovalgek -/// @notice Extends the ERC20Core functionality -contract ERC20Rebasable is IERC20Wrapable, ERC20Core, ERC20Metadata { +/// @notice Extends the ERC20Shared functionality +contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { IERC20 public immutable wrappedToken; ITokensRateOracle public immutable tokensRateOracle; @@ -42,24 +39,247 @@ contract ERC20Rebasable is IERC20Wrapable, ERC20Core, ERC20Metadata { _setERC20MetadataSymbol(symbol_); } + + /// ------------IERC20Wrapable------------ + /// @inheritdoc IERC20Wrapable - function wrap(uint256 wstETHAmount_) external returns (uint256) { - require(wstETHAmount_ > 0, "stETH: can't wrap zero wstETH"); - uint256 stETHAmount = wstETHAmount_ / tokensRateOracle.wstETH_to_stETH_rate(); // check how to divide. - // - _mint(msg.sender, stETHAmount); - wrappedToken.transferFrom(msg.sender, address(this), wstETHAmount_); - return stETHAmount; - } + function wrap(uint256 sharesAmount_) external returns (uint256) { + require(sharesAmount_ > 0, "Rebasable: can't wrap zero shares"); + + _mintShares(msg.sender, sharesAmount_); + wrappedToken.transferFrom(msg.sender, address(this), sharesAmount_); - // tests when rate is different <1, >1. + return getTokensByShares(sharesAmount_); + } /// @inheritdoc IERC20Wrapable - function unwrap(uint256 stETHAmount_) external returns (uint256) { - require(stETHAmount_ > 0, "stETH: zero amount unwrap not allowed"); - uint256 wstETHAmount = stETHAmount_ * tokensRateOracle.wstETH_to_stETH_rate(); - _burn(msg.sender, stETHAmount_); - wrappedToken.transfer(msg.sender, wstETHAmount); - return wstETHAmount; + function unwrap(uint256 tokenAmount_) external returns (uint256) { + require(tokenAmount_ > 0, "Rebasable: zero amount unwrap not allowed"); + + uint256 sharesAmount = getSharesByTokens(tokenAmount_); + + _burnShares(msg.sender, sharesAmount); + wrappedToken.transfer(msg.sender, sharesAmount); + + return sharesAmount; + } + + + /// ------------ERC20------------ + + /// @inheritdoc IERC20 + mapping(address => mapping(address => uint256)) public allowance; + + /// @inheritdoc IERC20 + function totalSupply() external view returns (uint256) { + return getTokensByShares(totalShares); } + + /// @inheritdoc IERC20 + function balanceOf(address account_) external view returns (uint256) { + return getTokensByShares(_sharesOf(account_)); + } + + /// @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; + } + + /// @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 + /// @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); + emit Transfer(from_, to_, amount_); + } + + /// @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 = allowance[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_) { + allowance[owner_][spender_] = amount_; + emit Approval(owner_, spender_, amount_); + } + + + /// ------------Shares------------ + + mapping (address => uint256) private shares; + + uint256 private totalShares; + + function _sharesOf(address account_) internal view returns (uint256) { + return shares[account_]; + } + + function getTokensByShares(uint256 sharesAmount_) public view returns (uint256) { + (uint256 tokensRate, uint8 decimals) = _getTokensRate(); + return sharesAmount_ * (10 ** uint256(decimals)) / tokensRate; + } + + function getSharesByTokens(uint256 tokenAmount_) public view returns (uint256) { + (uint256 tokensRate, uint8 decimals) = _getTokensRate(); + return tokenAmount_ * tokensRate / (10 ** uint256(decimals)); + } + + function _getTokensRate() internal view returns (uint256, uint8) { + uint8 priceDecimals = tokensRateOracle.decimals(); + + require(priceDecimals > uint8(0) && priceDecimals <= uint8(18), "Invalid priceDecimals"); + + (, + int256 answer + , + , + uint256 updatedAt + ,) = tokensRateOracle.latestRoundData(); + + require(updatedAt != 0); + + return (uint256(answer), priceDecimals); + } + + /// @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_) returns (uint256) { + totalShares = totalShares + amount_; + shares[recipient_] = shares[recipient_] + amount_; + return totalShares; + } + + /// @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_) returns (uint256) { + uint256 accountShares = shares[account_]; + require(amount_ <= accountShares, "BALANCE_EXCEEDED"); + totalShares = totalShares - amount_; + shares[account_] = accountShares - amount_; + return totalShares; + } + + /// @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_) { + + require(recipient_ != address(this), "TRANSFER_TO_REBASABLE_CONTRACT"); + + uint256 currentSenderShares = shares[sender_]; + require(sharesAmount_ <= currentSenderShares, "BALANCE_EXCEEDED"); + + shares[sender_] = currentSenderShares - sharesAmount_; + shares[recipient_] = shares[recipient_] + sharesAmount_; + } + + /// @dev validates that account_ is not zero address + modifier onlyNonZeroAccount(address account_) { + if (account_ == address(0)) { + revert ErrorAccountIsZeroAddress(); + } + _; + } + + error ErrorNotEnoughBalance(); + error ErrorNotEnoughAllowance(); + error ErrorAccountIsZeroAddress(); + error ErrorDecreasedAllowanceBelowZero(); } \ No newline at end of file diff --git a/contracts/token/StETH.sol b/contracts/token/StETH.sol deleted file mode 100644 index be0a36e7..00000000 --- a/contracts/token/StETH.sol +++ /dev/null @@ -1,51 +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 {IStETH} from "./interfaces/IStETH.sol"; -import {ERC20Bridged} from "./ERC20Bridged.sol"; - -/// @author kovalgek -/// @notice Extends the ERC20Bridged functionality -contract StETH is ERC20Bridged, IStETH { - - IERC20 public wstETH; - - /// @param wstETH_ address of the WstETH token to wrap - /// @param name_ The name of the token - /// @param symbol_ The symbol of the token - /// @param decimals_ The decimals places of the token - /// @param bridge_ The bridge address which allowd to mint/burn tokens - constructor( - IERC20 wstETH_, - string memory name_, - string memory symbol_, - uint8 decimals_, - address bridge_ - ) ERC20Bridged(name_, symbol_, decimals_, bridge_) { - wstETH = wstETH_; - } - - function wstETH_to_stETH_rate() public pure returns (uint256) { - return 2; - } - - function wrap(uint256 wstETHAmount_) external returns (uint256) { - require(wstETHAmount_ > 0, "stETH: can't wrap zero wstETH"); - uint256 stETHAmount = wstETHAmount_ / wstETH_to_stETH_rate(); - _mint(msg.sender, stETHAmount); - wstETH.transferFrom(msg.sender, address(this), wstETHAmount_); - return stETHAmount; - } - - function unwrap(uint256 stETHAmount_) external returns (uint256) { - require(stETHAmount_ > 0, "stETH: zero amount unwrap not allowed"); - uint256 wstETHAmount = stETHAmount_ * wstETH_to_stETH_rate(); - _burn(msg.sender, stETHAmount_); - wstETH.transfer(msg.sender, wstETHAmount); - return wstETHAmount; - } -} \ No newline at end of file diff --git a/contracts/token/interfaces/IStETH.sol b/contracts/token/interfaces/IERC20Wrapable.sol similarity index 53% rename from contracts/token/interfaces/IStETH.sol rename to contracts/token/interfaces/IERC20Wrapable.sol index a6acdaf2..1791842e 100644 --- a/contracts/token/interfaces/IStETH.sol +++ b/contracts/token/interfaces/IERC20Wrapable.sol @@ -3,14 +3,34 @@ pragma solidity 0.8.10; - /// @author kovalgek /// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens -interface IStETH { - function wrap(uint256 _stETHAmount) external returns (uint256); - function unwrap(uint256 _wstETHAmount) external returns (uint256); - - +interface IERC20Wrapable { + + /** + * @notice Exchanges wstETH to stETH + * @param wrappedTokenAmount_ amount of wstETH to wrap in exchange for stETH + * @dev Requirements: + * - `wstETHAmount_` must be non-zero + * - msg.sender must approve at least `wstETHAmount_` stETH to this + * contract. + * - msg.sender must have at least `wstETHAmount_` of stETH. + * User should first approve wstETHAmount_ to the StETH contract + * @return Amount of StETH user receives after wrap + */ + function wrap(uint256 wrappedTokenAmount_) external returns (uint256); + + /** + * @notice Exchanges stETH to wstETH + * @param wrapableTokenAmount_ amount of stETH to uwrap in exchange for wstETH + * @dev Requirements: + * - `stETHAmount_` must be non-zero + * - msg.sender must have at least `stETHAmount_` stETH. + * @return Amount of wstETH user receives after unwrap + */ + function unwrap(uint256 wrapableTokenAmount_) external returns (uint256); + + // TODO: // /** // * @notice Get amount of wstETH for a given amount of stETH // * @param _stETHAmount amount of stETH diff --git a/contracts/token/interfaces/ITokensRateOracle.sol b/contracts/token/interfaces/ITokensRateOracle.sol new file mode 100644 index 00000000..843be503 --- /dev/null +++ b/contracts/token/interfaces/ITokensRateOracle.sol @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice Oracle interface for two tokens rate +interface ITokensRateOracle { + + /** + * @notice represents the number of decimals the oracle responses represent. + */ + function decimals() external view returns (uint8); + + /** + * @notice get data about the latest round. + */ + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} \ No newline at end of file From 380caaae66494dced5c3443131c266d773312b1b Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 17 Oct 2023 14:34:01 +0200 Subject: [PATCH 04/61] add unit tests --- contracts/stubs/TokensRateOracleStub.sol | 8 +- contracts/token/ERC20Rebasable.sol | 91 ++++-- contracts/token/interfaces/IERC20Wrapable.sol | 4 +- test/token/ERC20Rebasable.unit.test.ts | 278 ++++++++++++++++++ 4 files changed, 348 insertions(+), 33 deletions(-) create mode 100644 test/token/ERC20Rebasable.unit.test.ts diff --git a/contracts/stubs/TokensRateOracleStub.sol b/contracts/stubs/TokensRateOracleStub.sol index 15771d72..6f397515 100644 --- a/contracts/stubs/TokensRateOracleStub.sol +++ b/contracts/stubs/TokensRateOracleStub.sol @@ -23,6 +23,12 @@ contract TokensRateOracleStub is ITokensRateOracle { latestRoundDataAnswer = answer_; } + uint256 public latestRoundDataUpdatedAt; + + function setUpdatedAt(uint256 updatedAt_) external { + latestRoundDataUpdatedAt = updatedAt_; + } + /** * @notice get data about the latest round. */ @@ -36,6 +42,6 @@ contract TokensRateOracleStub is ITokensRateOracle { uint256 updatedAt, uint80 answeredInRound ) { - return (0,latestRoundDataAnswer,0,0,0); + return (0,latestRoundDataAnswer,0,latestRoundDataUpdatedAt,0); } } \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 219dfd44..871f0ccf 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -12,6 +12,17 @@ import {ERC20Metadata} from "./ERC20Metadata.sol"; /// @notice Extends the ERC20Shared functionality contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { + error ErrorZeroSharesWrap(); + error ErrorZeroTokensUnwrap(); + error ErrorInvalidRateDecimals(uint8); + error ErrorWrongOracleUpdateTime(); + error ErrorOracleAnswerIsNegative(); + error ErrorTrasferToRebasableContract(); + error ErrorNotEnoughBalance(); + error ErrorNotEnoughAllowance(); + error ErrorAccountIsZeroAddress(); + error ErrorDecreasedAllowanceBelowZero(); + IERC20 public immutable wrappedToken; ITokensRateOracle public immutable tokensRateOracle; @@ -39,24 +50,23 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { _setERC20MetadataSymbol(symbol_); } - /// ------------IERC20Wrapable------------ /// @inheritdoc IERC20Wrapable function wrap(uint256 sharesAmount_) external returns (uint256) { - require(sharesAmount_ > 0, "Rebasable: can't wrap zero shares"); - + if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); + _mintShares(msg.sender, sharesAmount_); wrappedToken.transferFrom(msg.sender, address(this), sharesAmount_); - return getTokensByShares(sharesAmount_); + return _getTokensByShares(sharesAmount_); } /// @inheritdoc IERC20Wrapable function unwrap(uint256 tokenAmount_) external returns (uint256) { - require(tokenAmount_ > 0, "Rebasable: zero amount unwrap not allowed"); + if (tokenAmount_ == 0) revert ErrorZeroTokensUnwrap(); - uint256 sharesAmount = getSharesByTokens(tokenAmount_); + uint256 sharesAmount = _getSharesByTokens(tokenAmount_); _burnShares(msg.sender, sharesAmount); wrappedToken.transfer(msg.sender, sharesAmount); @@ -64,7 +74,6 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } - /// ------------ERC20------------ /// @inheritdoc IERC20 @@ -72,12 +81,12 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { /// @inheritdoc IERC20 function totalSupply() external view returns (uint256) { - return getTokensByShares(totalShares); + return _getTokensByShares(totalShares); } /// @inheritdoc IERC20 function balanceOf(address account_) external view returns (uint256) { - return getTokensByShares(_sharesOf(account_)); + return _getTokensByShares(_sharesOf(account_)); } /// @inheritdoc IERC20 @@ -147,7 +156,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { address to_, uint256 amount_ ) internal onlyNonZeroAccount(from_) onlyNonZeroAccount(to_) { - uint256 sharesToTransfer = getSharesByTokens(amount_); + uint256 sharesToTransfer = _getSharesByTokens(amount_); _transferShares(from_, to_, sharesToTransfer); emit Transfer(from_, to_, amount_); } @@ -189,6 +198,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { /// ------------Shares------------ + // API + function sharesOf(address _account) external view returns (uint256) { + return _sharesOf(_account); + } + + function getTotalShares() external view returns (uint256) { + return _getTotalShares(); + } + + function getTokensByShares(uint256 sharesAmount_) external view returns (uint256) { + return _getTokensByShares(sharesAmount_); + } + + function getSharesByTokens(uint256 tokenAmount_) external view returns (uint256) { + return _getSharesByTokens(tokenAmount_); + } + + function getTokensRateAndDecimal() external view returns (uint256, uint256) { + return _getTokensRateAndDecimal(); + } + + // private/internal mapping (address => uint256) private shares; @@ -198,20 +229,24 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return shares[account_]; } - function getTokensByShares(uint256 sharesAmount_) public view returns (uint256) { - (uint256 tokensRate, uint8 decimals) = _getTokensRate(); - return sharesAmount_ * (10 ** uint256(decimals)) / tokensRate; + function _getTotalShares() internal view returns (uint256) { + return totalShares; + } + + function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { + (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + return (sharesAmount_ * (10 ** decimals)) / tokensRate; } - function getSharesByTokens(uint256 tokenAmount_) public view returns (uint256) { - (uint256 tokensRate, uint8 decimals) = _getTokensRate(); - return tokenAmount_ * tokensRate / (10 ** uint256(decimals)); + function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) { + (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + return (tokenAmount_ * tokensRate) / (10 ** decimals); } - function _getTokensRate() internal view returns (uint256, uint8) { - uint8 priceDecimals = tokensRateOracle.decimals(); + function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { + uint8 rateDecimals = tokensRateOracle.decimals(); - require(priceDecimals > uint8(0) && priceDecimals <= uint8(18), "Invalid priceDecimals"); + if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); (, int256 answer @@ -220,9 +255,10 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { uint256 updatedAt ,) = tokensRateOracle.latestRoundData(); - require(updatedAt != 0); - - return (uint256(answer), priceDecimals); + if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); + if (answer <= 0) revert ErrorOracleAnswerIsNegative(); + + return (uint256(answer), uint256(rateDecimals)); } /// @dev Creates amount_ shares and assigns them to account_, increasing the total shares supply @@ -245,7 +281,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { uint256 amount_ ) internal onlyNonZeroAccount(account_) returns (uint256) { uint256 accountShares = shares[account_]; - require(amount_ <= accountShares, "BALANCE_EXCEEDED"); + if (accountShares < amount_) revert ErrorNotEnoughBalance(); totalShares = totalShares - amount_; shares[account_] = accountShares - amount_; return totalShares; @@ -261,10 +297,10 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { uint256 sharesAmount_ ) internal onlyNonZeroAccount(sender_) onlyNonZeroAccount(recipient_) { - require(recipient_ != address(this), "TRANSFER_TO_REBASABLE_CONTRACT"); + if (recipient_ == address(this)) revert ErrorTrasferToRebasableContract(); uint256 currentSenderShares = shares[sender_]; - require(sharesAmount_ <= currentSenderShares, "BALANCE_EXCEEDED"); + if (sharesAmount_ > currentSenderShares) revert ErrorNotEnoughBalance(); shares[sender_] = currentSenderShares - sharesAmount_; shares[recipient_] = shares[recipient_] + sharesAmount_; @@ -277,9 +313,4 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { } _; } - - error ErrorNotEnoughBalance(); - error ErrorNotEnoughAllowance(); - error ErrorAccountIsZeroAddress(); - error ErrorDecreasedAllowanceBelowZero(); } \ No newline at end of file diff --git a/contracts/token/interfaces/IERC20Wrapable.sol b/contracts/token/interfaces/IERC20Wrapable.sol index 1791842e..15213ccd 100644 --- a/contracts/token/interfaces/IERC20Wrapable.sol +++ b/contracts/token/interfaces/IERC20Wrapable.sol @@ -9,7 +9,7 @@ interface IERC20Wrapable { /** * @notice Exchanges wstETH to stETH - * @param wrappedTokenAmount_ amount of wstETH to wrap in exchange for stETH + * @param sharesAmount_ amount of wstETH to wrap in exchange for stETH * @dev Requirements: * - `wstETHAmount_` must be non-zero * - msg.sender must approve at least `wstETHAmount_` stETH to this @@ -18,7 +18,7 @@ interface IERC20Wrapable { * User should first approve wstETHAmount_ to the StETH contract * @return Amount of StETH user receives after wrap */ - function wrap(uint256 wrappedTokenAmount_) external returns (uint256); + function wrap(uint256 sharesAmount_) external returns (uint256); /** * @notice Exchanges stETH to wstETH diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts new file mode 100644 index 00000000..849155a7 --- /dev/null +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -0,0 +1,278 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; + +import { ERC20Stub__factory, ERC20Rebasable__factory, TokensRateOracleStub__factory, OssifiableProxy__factory } from "../../typechain"; +import { BigNumber } from "ethers"; + + +unit("ERC20Rebasable", ctxFactory) + + .test("wrappedToken", async (ctx) => { + const { rebasableProxied, wrappedTokenStub } = ctx.contracts; + assert.equal(await rebasableProxied.wrappedToken(), wrappedTokenStub.address) + }) + + .test("tokensRateOracle", async (ctx) => { + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + assert.equal(await rebasableProxied.tokensRateOracle(), tokensRateOracleStub.address) + }) + + .test("name()", async (ctx) => + assert.equal(await ctx.contracts.rebasableProxied.name(), ctx.constants.name) + ) + + .test("symbol()", async (ctx) => + assert.equal(await ctx.contracts.rebasableProxied.symbol(), ctx.constants.symbol) + ) + + .test("decimals", async (ctx) => + assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimals) + ) + + .test("wrap(0)", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + await assert.revertsWith(rebasableProxied.wrap(0), "ErrorZeroSharesWrap()"); + }) + + .test("unwrap(0)", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + await assert.revertsWith(rebasableProxied.unwrap(0), "ErrorZeroTokensUnwrap()"); + }) + + .test("wrap() positive scenario", async (ctx) => { + const { rebasableProxied, tokensRateOracleStub, wrappedTokenStub } = ctx.contracts; + const {user1, user2 } = ctx.accounts; + + await tokensRateOracleStub.setDecimals(5); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(1000); + + // user1 + assert.equalBN(await rebasableProxied.callStatic.wrap(100), 83); + const tx = await rebasableProxied.wrap(100); + + assert.equalBN(await rebasableProxied.getTotalShares(), 100); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 100); + + assert.equal(await wrappedTokenStub.transferFromAddress(), user1.address); + assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); + assert.equalBN(await wrappedTokenStub.transferFromAmount(), 100); + + // user2 + assert.equalBN(await rebasableProxied.connect(user2).callStatic.wrap(50), 41); + const tx2 = await rebasableProxied.connect(user2).wrap(50); + + assert.equalBN(await rebasableProxied.getTotalShares(), 150); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 50); + + assert.equal(await wrappedTokenStub.transferFromAddress(), user2.address); + assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); + assert.equalBN(await wrappedTokenStub.transferFromAmount(), 50); + + // common state changes + assert.equalBN(await rebasableProxied.totalSupply(), 125); + }) + + .test("wrap() with wrong oracle decimals", async (ctx) => { + + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + + await tokensRateOracleStub.setDecimals(0); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(1000); + + await assert.revertsWith(rebasableProxied.wrap(23), "ErrorInvalidRateDecimals(0)"); + + await tokensRateOracleStub.setDecimals(19); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(1000); + + await assert.revertsWith(rebasableProxied.wrap(23), "ErrorInvalidRateDecimals(19)"); + }) + + .test("wrap() with wrong oracle update time", async (ctx) => { + + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + + await tokensRateOracleStub.setDecimals(10); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(0); + + await assert.revertsWith(rebasableProxied.wrap(5), "ErrorWrongOracleUpdateTime()"); + }) + + .test("wrap() with wrong oracle answer", async (ctx) => { + + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + + await tokensRateOracleStub.setDecimals(10); + await tokensRateOracleStub.setLatestRoundDataAnswer(0); + await tokensRateOracleStub.setUpdatedAt(10); + + await assert.revertsWith(rebasableProxied.wrap(21), "ErrorOracleAnswerIsNegative()"); + }) + + + .test("unwrap() positive scenario", async (ctx) => { + + const { rebasableProxied, tokensRateOracleStub, wrappedTokenStub } = ctx.contracts; + const {user1, user2 } = ctx.accounts; + + await tokensRateOracleStub.setDecimals(7); + await tokensRateOracleStub.setLatestRoundDataAnswer(14000000); + await tokensRateOracleStub.setUpdatedAt(14000); + + // user1 + const tx0 = await rebasableProxied.wrap(4500); + + assert.equalBN(await rebasableProxied.callStatic.unwrap(59), 82); + const tx = await rebasableProxied.unwrap(59); + + assert.equalBN(await rebasableProxied.getTotalShares(), 4418); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 4418); + + assert.equal(await wrappedTokenStub.transferTo(), user1.address); + assert.equalBN(await wrappedTokenStub.transferAmount(), 82); + + // // user2 + await rebasableProxied.connect(user2).wrap(200); + + assert.equalBN(await rebasableProxied.connect(user2).callStatic.unwrap(50), 70); + const tx2 = await rebasableProxied.connect(user2).unwrap(50); + + assert.equalBN(await rebasableProxied.getTotalShares(), 4548); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 130); + + assert.equal(await wrappedTokenStub.transferTo(), user2.address); + assert.equalBN(await wrappedTokenStub.transferAmount(), 70); + + // common state changes + assert.equalBN(await rebasableProxied.totalSupply(), 3248); + }) + + .test("unwrap() with wrong oracle decimals", async (ctx) => { + + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + + + await tokensRateOracleStub.setDecimals(10); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(1000); + + await rebasableProxied.wrap(100); + await tokensRateOracleStub.setDecimals(0); + + await assert.revertsWith(rebasableProxied.unwrap(23), "ErrorInvalidRateDecimals(0)"); + + await tokensRateOracleStub.setDecimals(19); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(1000); + + await assert.revertsWith(rebasableProxied.unwrap(23), "ErrorInvalidRateDecimals(19)"); + }) + + .test("unwrap() with wrong oracle update time", async (ctx) => { + + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + + await tokensRateOracleStub.setDecimals(10); + await tokensRateOracleStub.setLatestRoundDataAnswer(120000); + await tokensRateOracleStub.setUpdatedAt(300); + + await rebasableProxied.wrap(100); + await tokensRateOracleStub.setUpdatedAt(0); + + await assert.revertsWith(rebasableProxied.unwrap(5), "ErrorWrongOracleUpdateTime()"); + }) + + .test("unwrap() when no balance", async (ctx) => { + const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + + await tokensRateOracleStub.setDecimals(8); + await tokensRateOracleStub.setLatestRoundDataAnswer(12000000); + await tokensRateOracleStub.setUpdatedAt(1000); + + await assert.revertsWith(rebasableProxied.unwrap(10), "ErrorNotEnoughBalance()"); + }) + + .test("approve()", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { user1, user2 } = ctx.accounts; + + // validate initially allowance is zero + assert.equalBN( + await rebasableProxied.allowance(user1.address, user2.address), + "0" + ); + + const amount = 3; + + // validate return value of the method + assert.isTrue( + await rebasableProxied.callStatic.approve(user2.address, amount) + ); + + // approve tokens + const tx = await rebasableProxied.approve(user2.address, amount); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + user1.address, + user2.address, + amount, + ]); + + // validate allowance was set + assert.equalBN( + await rebasableProxied.allowance(user1.address, user2.address), + amount + ); + }) + + .run(); + +async function ctxFactory() { + const name = "StETH Test Token"; + const symbol = "StETH"; + const decimals = 18; + const [deployer, user1, user2] = await hre.ethers.getSigners(); + + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + + const tokensRateOracleStub = await new TokensRateOracleStub__factory(deployer).deploy(); + + const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + wrappedTokenStub.address, + tokensRateOracleStub.address, + name, + symbol, + decimals + ); + rebasableTokenImpl.wrap + 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, + ERC20Rebasable__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const rebasableProxied = ERC20Rebasable__factory.connect( + l2TokensProxy.address, + user1 + ); + + return { + accounts: { deployer, user1, user2 }, + constants: { name, symbol, decimals }, + contracts: { rebasableProxied, wrappedTokenStub, tokensRateOracleStub } + }; +} From eab8718a34311a3c38df676741cd1325694383a2 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Sat, 4 Nov 2023 15:16:04 +0100 Subject: [PATCH 05/61] add deposit flow for new rebasable token --- contracts/BridgeableTokens.sol | 14 +- .../arbitrum/InterchainERC20TokenGateway.sol | 8 +- contracts/arbitrum/L1ERC20TokenGateway.sol | 8 +- contracts/arbitrum/L2ERC20TokenGateway.sol | 9 +- contracts/optimism/L1ERC20TokenBridge.sol | 37 +- contracts/optimism/L2ERC20TokenBridge.sol | 26 +- contracts/stubs/ERC20Stub.sol | 66 ++ contracts/stubs/ERC20WrapableStub.sol | 56 ++ contracts/token/ERC20Core.sol | 2 + contracts/token/ERC20Rebasable.sol | 21 +- .../optimism.integration.test.ts | 16 +- .../bridging-rebase.integration.test.ts | 654 ++++++++++++++++++ utils/optimism/deployment.ts | 55 +- utils/optimism/testing.ts | 37 +- 14 files changed, 977 insertions(+), 32 deletions(-) create mode 100644 contracts/stubs/ERC20Stub.sol create mode 100644 contracts/stubs/ERC20WrapableStub.sol create mode 100644 test/optimism/bridging-rebase.integration.test.ts diff --git a/contracts/BridgeableTokens.sol b/contracts/BridgeableTokens.sol index 52ec31af..36a636b9 100644 --- a/contracts/BridgeableTokens.sol +++ b/contracts/BridgeableTokens.sol @@ -9,19 +9,27 @@ contract BridgeableTokens { /// @notice Address of the bridged token in the L1 chain address public immutable l1Token; + /// @notice Address of the bridged rebasable token in the L1 chain + address public immutable l1TokenRebasable; + /// @notice Address of the token minted on the L2 chain when token bridged address public immutable l2Token; + /// @notice Address of the rebasable token minted on the L2 chain when token bridged + address public immutable l2TokenRebasable; + /// @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 l1Token_, address l2Token_) { + constructor(address l1Token_, address l1TokenRebasable_, address l2Token_, address l2TokenRebasable_) { l1Token = l1Token_; + l1TokenRebasable = l1TokenRebasable_; l2Token = l2Token_; + l2TokenRebasable = l2TokenRebasable_; } /// @dev Validates that passed l1Token_ is supported by the bridge modifier onlySupportedL1Token(address l1Token_) { - if (l1Token_ != l1Token) { + if (l1Token_ != l1Token && l1Token_ != l1TokenRebasable) { revert ErrorUnsupportedL1Token(); } _; @@ -29,7 +37,7 @@ contract BridgeableTokens { /// @dev Validates that passed l2Token_ is supported by the bridge modifier onlySupportedL2Token(address l2Token_) { - if (l2Token_ != l2Token) { + if (l2Token_ != l2Token && l2Token_ != l2TokenRebasable) { revert ErrorUnsupportedL2Token(); } _; diff --git a/contracts/arbitrum/InterchainERC20TokenGateway.sol b/contracts/arbitrum/InterchainERC20TokenGateway.sol index 329f4c87..f75334d1 100644 --- a/contracts/arbitrum/InterchainERC20TokenGateway.sol +++ b/contracts/arbitrum/InterchainERC20TokenGateway.sol @@ -25,13 +25,17 @@ abstract contract InterchainERC20TokenGateway is /// @param router_ Address of the router in the corresponding chain /// @param counterpartGateway_ Address of the counterpart gateway used in the bridging process /// @param l1Token_ Address of the bridged token in the Ethereum chain + /// @param l1TokenRebasable_ Address of the bridged token in the Ethereum chain /// @param l2Token_ Address of the token minted on the Arbitrum chain when token bridged + /// @param l2TokenRebasable_ Address of the token minted on the Arbitrum chain when token bridged constructor( address router_, address counterpartGateway_, address l1Token_, - address l2Token_ - ) BridgeableTokens(l1Token_, l2Token_) { + address l1TokenRebasable_, + address l2Token_, + address l2TokenRebasable_ + ) BridgeableTokens(l1Token_, l1TokenRebasable_, l2Token_, l2TokenRebasable_) { router = router_; counterpartGateway = counterpartGateway_; } diff --git a/contracts/arbitrum/L1ERC20TokenGateway.sol b/contracts/arbitrum/L1ERC20TokenGateway.sol index 1be951aa..7b91b6a2 100644 --- a/contracts/arbitrum/L1ERC20TokenGateway.sol +++ b/contracts/arbitrum/L1ERC20TokenGateway.sol @@ -32,13 +32,17 @@ contract L1ERC20TokenGateway is address router_, address counterpartGateway_, address l1Token_, - address l2Token_ + address l1TokenRebasable_, + address l2Token_, + address l2TokenRebasable_ ) InterchainERC20TokenGateway( router_, counterpartGateway_, l1Token_, - l2Token_ + l1TokenRebasable_, + l2Token_, + l2TokenRebasable_ ) L1CrossDomainEnabled(inbox_) {} diff --git a/contracts/arbitrum/L2ERC20TokenGateway.sol b/contracts/arbitrum/L2ERC20TokenGateway.sol index 5853d0ac..d65fd3ac 100644 --- a/contracts/arbitrum/L2ERC20TokenGateway.sol +++ b/contracts/arbitrum/L2ERC20TokenGateway.sol @@ -22,19 +22,24 @@ contract L2ERC20TokenGateway is /// @param router_ Address of the router in the L2 chain /// @param counterpartGateway_ Address of the counterpart L1 gateway /// @param l1Token_ Address of the bridged token in the L1 chain + /// @param l1TokenRebasable_ Address of the bridged token in the L1 chain /// @param l2Token_ Address of the token minted on the Arbitrum chain when token bridged constructor( address arbSys_, address router_, address counterpartGateway_, address l1Token_, - address l2Token_ + address l1TokenRebasable_, + address l2Token_, + address l2TokenRebasable_ ) InterchainERC20TokenGateway( router_, counterpartGateway_, l1Token_, - l2Token_ + l1TokenRebasable_, + l2Token_, + l2TokenRebasable_ ) L2CrossDomainEnabled(arbSys_) {} diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index a4438b88..d543a961 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -14,6 +14,9 @@ import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +import "hardhat/console.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 @@ -32,19 +35,22 @@ contract L1ERC20TokenBridge is /// @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 l1TokenRebasable_ 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_) { + address l1Token_, // wstETH + address l1TokenRebasable_, // stETH + address l2Token_, + address l2TokenRebasable_ + ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l1TokenRebasable_, l2Token_, l2TokenRebasable_) { l2TokenBridge = l2TokenBridge_; } /// @inheritdoc IL1ERC20Bridge function depositERC20( - address l1Token_, + address l1Token_, // stETH or wstETH address l2Token_, uint256 amount_, uint32 l2Gas_, @@ -54,11 +60,26 @@ contract L1ERC20TokenBridge is whenDepositsEnabled onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) - { + { if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } - _initiateERC20Deposit(msg.sender, msg.sender, amount_, l2Gas_, data_); + + if(l1Token_ == l1TokenRebasable) { + bytes memory data = bytes.concat(hex'01', data_); // add rate + IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); + IERC20(l1TokenRebasable).approve(l1Token, amount_); + uint256 wstETHAmount = IERC20Wrapable(l1Token).wrap(amount_); + console.log("wstETHAmount=",wstETHAmount); + + uint256 balanceOfl1Token = IERC20(l1Token).balanceOf(address(this)); + console.log("balanceOfl1Token=",balanceOfl1Token); + + _initiateERC20Deposit(msg.sender, msg.sender, wstETHAmount, l2Gas_, data); + } else { + IERC20(l1Token).safeTransferFrom(msg.sender, address(this), amount_); + _initiateERC20Deposit(msg.sender, msg.sender, amount_, l2Gas_, data_); + } } /// @inheritdoc IL1ERC20Bridge @@ -120,9 +141,8 @@ contract L1ERC20TokenBridge is address to_, uint256 amount_, uint32 l2Gas_, - bytes calldata data_ + bytes memory data_ ) internal { - IERC20(l1Token).safeTransferFrom(from_, address(this), amount_); bytes memory message = abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, @@ -133,6 +153,7 @@ contract L1ERC20TokenBridge is amount_, data_ ); + console.log("SEND l2TokenBridge=%s l1Token=%s l2Token=%s", l2TokenBridge, l1Token, l2Token); sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 1b1870e0..32c72053 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -6,11 +6,14 @@ 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 {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import { console } from "hardhat/console.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 @@ -34,8 +37,10 @@ contract L2ERC20TokenBridge is address messenger_, address l1TokenBridge_, address l1Token_, - address l2Token_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l2Token_) { + address l1TokenRebasable_, + address l2Token_, + address l2TokenRebasable_ + ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l1TokenRebasable_, l2Token_, l2TokenRebasable_) { l1TokenBridge = l1TokenBridge_; } @@ -75,8 +80,21 @@ contract L2ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l1TokenBridge) { - IERC20Bridged(l2Token_).bridgeMint(to_, amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + if (data_.length > 0 && data_[0] == hex'01') { + console.log("finalizeDeposit1 l2TokenRebasable balanceBefore=",ERC20Rebasable(l2TokenRebasable).balanceOf(to_)); + + ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); + + console.log("finalizeDeposit1 l2TokenRebasable balanceafter==",ERC20Rebasable(l2TokenRebasable).balanceOf(to_)); + + bytes memory data = data_[1:]; + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data); + } else { + console.log("finalizeDeposit2 data=", data_.length); + + 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 diff --git a/contracts/stubs/ERC20Stub.sol b/contracts/stubs/ERC20Stub.sol new file mode 100644 index 00000000..686ea516 --- /dev/null +++ b/contracts/stubs/ERC20Stub.sol @@ -0,0 +1,66 @@ +// 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 { console } from "hardhat/console.sol"; + +contract ERC20Stub is IERC20 { + + uint256 totalSupply_; + uint256 balanceOf_; + bool transfer_; + uint256 allowance_; + bool approve_; + bool transferFrom_; + + constructor() { + totalSupply_ = 0; + balanceOf_ = 0; + transfer_ = true; + allowance_ = 0; + approve_ = true; + transferFrom_ = true; + } + + function totalSupply() external view returns (uint256) { + return totalSupply_; + } + + function balanceOf(address account) external view returns (uint256) { + return balanceOf_; + } + + address public transferTo; + uint256 public transferAmount; + + function transfer(address to, uint256 amount) external returns (bool) { + transferTo = to; + transferAmount = amount; + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return 0; + } + + function approve(address spender, uint256 amount) external returns (bool) { + return true; + } + + address public transferFromAddress; + address public transferFromTo; + uint256 public transferFromAmount; + + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool) { + transferFromAddress = from; + transferFromTo = to; + transferFromAmount = amount; + return true; + } +} \ No newline at end of file diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol new file mode 100644 index 00000000..fe49ff83 --- /dev/null +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +// import {ERC20Core} from "../token/ERC20Core.sol"; +import { console } from "hardhat/console.sol"; + +// represents wstETH on L1 +contract ERC20WrapableStub is IERC20Wrapable, ERC20 { + + IERC20 public stETH; + address public bridge; + uint256 public tokensRate; /// wst/st + + constructor(IERC20 stETH_, string memory name_, string memory symbol_) + ERC20(name_, symbol_) + { + stETH = stETH_; + console.log("constructor wrap stETH=",address(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 * tokensRate) / (10 ** uint256(decimals())); + + + console.log("wrap msg.sender=",msg.sender); + console.log("wrap address(this)=",address(this)); + console.log("wrap _stETHAmount=",_stETHAmount); + console.log("wrap wstETHAmount=",wstETHAmount); + + _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 * (10 ** uint256(decimals()))) / tokensRate; + // _burn(msg.sender, _wstETHAmount); + // stETH.transfer(msg.sender, stETHAmount); + + return 0; //stETHAmount; + } +} diff --git a/contracts/token/ERC20Core.sol b/contracts/token/ERC20Core.sol index bf4e67db..9fb618cb 100644 --- a/contracts/token/ERC20Core.sol +++ b/contracts/token/ERC20Core.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { console } from "hardhat/console.sol"; /// @author psirex /// @notice Contains the required logic of the ERC20 standard as defined in the EIP. Additionally @@ -121,6 +122,7 @@ contract ERC20Core is IERC20 { address spender_, uint256 amount_ ) internal virtual onlyNonZeroAccount(owner_) onlyNonZeroAccount(spender_) { + console.log("_approve %@ %@ %@", msg.sender, owner_, spender_); allowance[owner_][spender_] = amount_; emit Approval(owner_, spender_, amount_); } diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 871f0ccf..04899582 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -7,6 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; import {ITokensRateOracle} from "./interfaces/ITokensRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; +import { console } from "hardhat/console.sol"; /// @author kovalgek /// @notice Extends the ERC20Shared functionality @@ -74,6 +75,15 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } + function mintShares(address account_, uint256 amount_) external returns (uint256) { + return _mintShares(account_, amount_); + } + + function burnShares(address account_, uint256 amount_) external { + _burnShares(account_, amount_); + } + + /// ------------ERC20------------ /// @inheritdoc IERC20 @@ -234,7 +244,12 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { } function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { + console.log("_getTokensByShares"); (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + console.log("sharesAmount_=",sharesAmount_); + console.log("decimals=",decimals); + console.log("tokensRate=",tokensRate); + return (sharesAmount_ * (10 ** decimals)) / tokensRate; } @@ -245,8 +260,10 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { uint8 rateDecimals = tokensRateOracle.decimals(); + console.log("_getTokensRateAndDecimal1"); if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); + console.log("_getTokensRateAndDecimal2"); (, int256 answer @@ -254,10 +271,12 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { , uint256 updatedAt ,) = tokensRateOracle.latestRoundData(); + console.log("_getTokensRateAndDecimal3"); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); if (answer <= 0) revert ErrorOracleAnswerIsNegative(); - + console.log("_getTokensRateAndDecimal4"); + return (uint256(answer), uint256(rateDecimals)); } diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 7c33be93..de7f5902 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -5,6 +5,8 @@ import { OssifiableProxy__factory, OptimismBridgeExecutor__factory, ERC20Bridged__factory, + ERC20Rebasable__factory, + TokensRateOracleStub__factory, } from "../../typechain"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; @@ -212,6 +214,13 @@ async function ctxFactory() { "TT" ); + const l1TokenRebasable = await new ERC20Rebasable__factory(l1Deployer).deploy( + "Test Token Rebasable", + "TTR" + ); + + const tokensRateOracleStub = await new TokensRateOracleStub__factory(l2Deployer).deploy(); + const optAddresses = optimism.addresses(networkName); const govBridgeExecutor = testingOnDeployedContracts @@ -233,9 +242,14 @@ async function ctxFactory() { .deployment(networkName) .erc20TokenBridgeDeployScript( l1Token.address, + l1TokenRebasable.address, + tokensRateOracleStub.address, { deployer: l1Deployer, - admins: { proxy: l1Deployer.address, bridge: l1Deployer.address }, + admins: { + proxy: l1Deployer.address, + bridge: l1Deployer.address + }, }, { deployer: l2Deployer, diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts new file mode 100644 index 00000000..5c72b102 --- /dev/null +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -0,0 +1,654 @@ +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 hre, { ethers } from "hardhat"; +import { BigNumber, FixedNumber } from "ethers"; + +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 { l1ERC20TokenBridge } = ctx; + const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l1ERC20TokenBridge + .connect(l1ERC20TokenBridgeAdmin) + .enableDeposits(); + } else { + console.log("L1 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l1ERC20TokenBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l1ERC20TokenBridge + .connect(l1ERC20TokenBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L1 withdrawals already enabled"); + } + + assert.isTrue(await l1ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l1ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("Activate bridging on L2", async (ctx) => { + const { l2ERC20TokenBridge } = ctx; + const { l2ERC20TokenBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l2ERC20TokenBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l2ERC20TokenBridge + .connect(l2ERC20TokenBridgeAdmin) + .enableDeposits(); + } else { + console.log("L2 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l2ERC20TokenBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l2ERC20TokenBridge + .connect(l2ERC20TokenBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L2 withdrawals already enabled"); + } + + assert.isTrue(await l2ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("L1 -> L2 deposit via depositERC20() method", async (ctx) => { + const { + l1Token, // wstETH + l1TokenRebasable, // stETH + l1ERC20TokenBridge, + l2Token, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + } = ctx; + const { accountA: tokenHolderA } = ctx.accounts; + const { depositAmount: depositAmountInRebasableTokens } = ctx.common; + + const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); + console.log("depositAmount=",depositAmount); + console.log("depositAmountInRebasableTokens=",depositAmountInRebasableTokens); + + console.log("l1Token=",l1Token.address); + console.log("l1TokenRebasable=",l1TokenRebasable.address); + console.log("l1ERC20TokenBridge=",l1ERC20TokenBridge.address); + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1ERC20TokenBridge.address, depositAmountInRebasableTokens); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + console.log("tokenHolderA=", tokenHolderA.address); + console.log("tokenHolderABalanceBefore=", tokenHolderABalanceBefore); + + const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1ERC20TokenBridge.address + ); + console.log("l1ERC20TokenBridgeBalanceBefore=", l1ERC20TokenBridgeBalanceBefore); + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2Token.address, + depositAmountInRebasableTokens, + 200_000, + "0x" + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x01", + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1Token.address, + l2Token.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x01", + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + ); + + // console.log("qwe=",await l1TokenRebasable.balanceOf(tokenHolderA.address)); + // console.log("asd=",tokenHolderABalanceBefore.sub(depositAmount)); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH + tokenHolderABalanceBefore.sub(depositAmountInRebasableTokens) + ); + }) + + .step("Finalize deposit on L2", async (ctx) => { + const { + l1TokenRebasable, + l2Token, + l2TokenRebasable, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + } = ctx; + // const { depositAmount } = ctx.common; + const { depositAmount: depositAmountInRebasableTokens } = ctx.common; + const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); + + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + + console.log("Finalize1 depositAmount=",depositAmount); + + const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderA.address + ); + console.log("tokenHolderABalanceBefore=",tokenHolderABalanceBefore); + + const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); + + console.log("test1", l1ERC20TokenBridge.address, l2ERC20TokenBridge.address, l1TokenRebasable.address, l2Token.address); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x01", + ]), + { gasLimit: 5_000_000 } + ); + + console.log("test2"); + + await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + depositAmount, + "0x", + ]); + // console.log(await l1TokenRebasable.balanceOf(tokenHolderA.address)); + + console.log(await l2TokenRebasable.balanceOf(tokenHolderA.address)); + console.log(tokenHolderABalanceBefore.add(depositAmountInRebasableTokens)); + + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(depositAmountInRebasableTokens) + ); + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TokenRebasableTotalSupplyBefore.add(depositAmountInRebasableTokens) + ); + }) + +// .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { +// const { accountA: tokenHolderA } = ctx.accounts; +// const { withdrawalAmount } = ctx.common; +// const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; + +// const tokenHolderABalanceBefore = await l2Token.balanceOf( +// tokenHolderA.address +// ); +// const l2TotalSupplyBefore = await l2Token.totalSupply(); + +// const tx = await l2ERC20TokenBridge +// .connect(tokenHolderA.l2Signer) +// .withdraw(l2Token.address, withdrawalAmount, 0, "0x"); + +// await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderA.address, +// withdrawalAmount, +// "0x", +// ]); +// assert.equalBN( +// await l2Token.balanceOf(tokenHolderA.address), +// tokenHolderABalanceBefore.sub(withdrawalAmount) +// ); +// assert.equalBN( +// await l2Token.totalSupply(), +// l2TotalSupplyBefore.sub(withdrawalAmount) +// ); +// }) + +// .step("Finalize withdrawal on L1", async (ctx) => { +// const { +// l1Token, +// l1CrossDomainMessenger, +// l1ERC20TokenBridge, +// l2CrossDomainMessenger, +// l2Token, +// l2ERC20TokenBridge, +// } = ctx; +// const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; +// const { withdrawalAmount } = ctx.common; + +// const tokenHolderABalanceBefore = await l1Token.balanceOf( +// tokenHolderA.address +// ); +// const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( +// l1ERC20TokenBridge.address +// ); + +// await l1CrossDomainMessenger +// .connect(l1Stranger) +// .setXDomainMessageSender(l2ERC20TokenBridge.address); + +// const tx = await l1CrossDomainMessenger +// .connect(l1Stranger) +// .relayMessage( +// l1ERC20TokenBridge.address, +// l2CrossDomainMessenger.address, +// l1ERC20TokenBridge.interface.encodeFunctionData( +// "finalizeERC20Withdrawal", +// [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderA.address, +// withdrawalAmount, +// "0x", +// ] +// ), +// 0 +// ); + +// await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderA.address, +// withdrawalAmount, +// "0x", +// ]); + +// assert.equalBN( +// await l1Token.balanceOf(l1ERC20TokenBridge.address), +// l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) +// ); + +// assert.equalBN( +// await l1Token.balanceOf(tokenHolderA.address), +// tokenHolderABalanceBefore.add(withdrawalAmount) +// ); +// }) + +// .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { +// const { +// l1Token, +// l2Token, +// l1ERC20TokenBridge, +// l2ERC20TokenBridge, +// l1CrossDomainMessenger, +// } = ctx; +// const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; +// const { depositAmount } = ctx.common; + +// assert.notEqual(tokenHolderA.address, tokenHolderB.address); + +// await l1Token +// .connect(tokenHolderA.l1Signer) +// .approve(l1ERC20TokenBridge.address, depositAmount); + +// const tokenHolderABalanceBefore = await l1Token.balanceOf( +// tokenHolderA.address +// ); +// const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( +// l1ERC20TokenBridge.address +// ); + +// const tx = await l1ERC20TokenBridge +// .connect(tokenHolderA.l1Signer) +// .depositERC20To( +// l1Token.address, +// l2Token.address, +// tokenHolderB.address, +// depositAmount, +// 200_000, +// "0x" +// ); + +// await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderB.address, +// depositAmount, +// "0x", +// ]); + +// const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( +// "finalizeDeposit", +// [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderB.address, +// depositAmount, +// "0x", +// ] +// ); + +// const messageNonce = await l1CrossDomainMessenger.messageNonce(); + +// await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ +// l2ERC20TokenBridge.address, +// l1ERC20TokenBridge.address, +// l2DepositCalldata, +// messageNonce, +// 200_000, +// ]); + +// assert.equalBN( +// await l1Token.balanceOf(l1ERC20TokenBridge.address), +// l1ERC20TokenBridgeBalanceBefore.add(depositAmount) +// ); + +// assert.equalBN( +// await l1Token.balanceOf(tokenHolderA.address), +// tokenHolderABalanceBefore.sub(depositAmount) +// ); +// }) + +// .step("Finalize deposit on L2", async (ctx) => { +// const { +// l1Token, +// l1ERC20TokenBridge, +// l2Token, +// l2CrossDomainMessenger, +// l2ERC20TokenBridge, +// } = ctx; +// const { +// accountA: tokenHolderA, +// accountB: tokenHolderB, +// l1CrossDomainMessengerAliased, +// } = ctx.accounts; +// const { depositAmount } = ctx.common; + +// const l2TokenTotalSupplyBefore = await l2Token.totalSupply(); +// const tokenHolderBBalanceBefore = await l2Token.balanceOf( +// tokenHolderB.address +// ); + +// const tx = await l2CrossDomainMessenger +// .connect(l1CrossDomainMessengerAliased) +// .relayMessage( +// 1, +// l1ERC20TokenBridge.address, +// l2ERC20TokenBridge.address, +// 0, +// 300_000, +// l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderB.address, +// depositAmount, +// "0x", +// ]), +// { gasLimit: 5_000_000 } +// ); + +// await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ +// l1Token.address, +// l2Token.address, +// tokenHolderA.address, +// tokenHolderB.address, +// depositAmount, +// "0x", +// ]); + +// assert.equalBN( +// await l2Token.totalSupply(), +// l2TokenTotalSupplyBefore.add(depositAmount) +// ); +// assert.equalBN( +// await l2Token.balanceOf(tokenHolderB.address), +// tokenHolderBBalanceBefore.add(depositAmount) +// ); +// }) + +// .step("L2 -> L1 withdrawal via withdrawTo()", async (ctx) => { +// const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; +// const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; +// const { withdrawalAmount } = ctx.common; + +// const tokenHolderBBalanceBefore = await l2Token.balanceOf( +// tokenHolderB.address +// ); +// const l2TotalSupplyBefore = await l2Token.totalSupply(); + +// const tx = await l2ERC20TokenBridge +// .connect(tokenHolderB.l2Signer) +// .withdrawTo( +// l2Token.address, +// tokenHolderA.address, +// withdrawalAmount, +// 0, +// "0x" +// ); + +// await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ +// l1Token.address, +// l2Token.address, +// tokenHolderB.address, +// tokenHolderA.address, +// withdrawalAmount, +// "0x", +// ]); + +// assert.equalBN( +// await l2Token.balanceOf(tokenHolderB.address), +// tokenHolderBBalanceBefore.sub(withdrawalAmount) +// ); + +// assert.equalBN( +// await l2Token.totalSupply(), +// l2TotalSupplyBefore.sub(withdrawalAmount) +// ); +// }) + +// .step("Finalize withdrawal on L1", async (ctx) => { +// const { +// l1Token, +// l1CrossDomainMessenger, +// l1ERC20TokenBridge, +// l2CrossDomainMessenger, +// l2Token, +// l2ERC20TokenBridge, +// } = ctx; +// const { +// accountA: tokenHolderA, +// accountB: tokenHolderB, +// l1Stranger, +// } = ctx.accounts; +// const { withdrawalAmount } = ctx.common; + +// const tokenHolderABalanceBefore = await l1Token.balanceOf( +// tokenHolderA.address +// ); +// const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( +// l1ERC20TokenBridge.address +// ); + +// await l1CrossDomainMessenger +// .connect(l1Stranger) +// .setXDomainMessageSender(l2ERC20TokenBridge.address); + +// const tx = await l1CrossDomainMessenger +// .connect(l1Stranger) +// .relayMessage( +// l1ERC20TokenBridge.address, +// l2CrossDomainMessenger.address, +// l1ERC20TokenBridge.interface.encodeFunctionData( +// "finalizeERC20Withdrawal", +// [ +// l1Token.address, +// l2Token.address, +// tokenHolderB.address, +// tokenHolderA.address, +// withdrawalAmount, +// "0x", +// ] +// ), +// 0 +// ); + +// await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ +// l1Token.address, +// l2Token.address, +// tokenHolderB.address, +// tokenHolderA.address, +// withdrawalAmount, +// "0x", +// ]); + +// assert.equalBN( +// await l1Token.balanceOf(l1ERC20TokenBridge.address), +// l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) +// ); + +// assert.equalBN( +// await l1Token.balanceOf(tokenHolderA.address), +// tokenHolderABalanceBefore.add(withdrawalAmount) +// ); +// }) + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); + console.log("networkName=",networkName); + + const { + l1Provider, + l2Provider, + l1ERC20TokenBridgeAdmin, + l2ERC20TokenBridgeAdmin, + ...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 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 l1ERC20TokenBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l2ERC20TokenBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + 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), + l1ERC20TokenBridgeAdmin, + l2ERC20TokenBridgeAdmin, + l1CrossDomainMessengerAliased, + }, + common: { + depositAmount, + withdrawalAmount, + }, + snapshot: { + l1: l1Snapshot, + l2: l2Snapshot, + }, + }; +} diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index c3b6aa3a..39ce1f7f 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -2,6 +2,7 @@ import { assert } from "chai"; import { Overrides, Wallet } from "ethers"; import { ERC20Bridged__factory, + ERC20Rebasable__factory, IERC20Metadata__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, @@ -20,6 +21,7 @@ interface OptL1DeployScriptParams { interface OptL2DeployScriptParams extends OptL1DeployScriptParams { l2Token?: { name?: string; symbol?: string }; + l2TokenRebasable?: { name?: string; symbol?: string }; } interface OptDeploymentOptions extends CommonOptions { @@ -35,21 +37,26 @@ export default function deployment( return { async erc20TokenBridgeDeployScript( l1Token: string, + l1TokenRebasable: string, + tokensRateOracleStub: string, l1Params: OptL1DeployScriptParams, - l2Params: OptL2DeployScriptParams + l2Params: OptL2DeployScriptParams, ) { + const [ expectedL1TokenBridgeImplAddress, expectedL1TokenBridgeProxyAddress, ] = await network.predictAddresses(l1Params.deployer, 2); - + const [ expectedL2TokenImplAddress, expectedL2TokenProxyAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, expectedL2TokenBridgeImplAddress, expectedL2TokenBridgeProxyAddress, - ] = await network.predictAddresses(l2Params.deployer, 4); - + ] = await network.predictAddresses(l2Params.deployer, 6); + const l1DeployScript = new DeployScript( l1Params.deployer, options?.logger @@ -60,7 +67,9 @@ export default function deployment( optAddresses.L1CrossDomainMessenger, expectedL2TokenBridgeProxyAddress, l1Token, + l1TokenRebasable, expectedL2TokenProxyAddress, + expectedL2TokenRebasableProxyAddress, options?.overrides, ], afterDeploy: (c) => @@ -86,10 +95,16 @@ export default function deployment( l1Params.deployer ); - const [decimals, l2TokenName, l2TokenSymbol] = await Promise.all([ + 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 DeployScript( @@ -122,13 +137,43 @@ export default function deployment( afterDeploy: (c) => assert.equal(c.address, expectedL2TokenProxyAddress), }) + .addStep({ + factory: ERC20Rebasable__factory, + args: [ + expectedL2TokenProxyAddress, + tokensRateOracleStub, + l2TokenRebasableName, + l2TokenRebasableSymbol, + decimals, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableImplAddress), + }) + .addStep({ + factory: OssifiableProxy__factory, + args: [ + expectedL2TokenRebasableImplAddress, + l2Params.admins.proxy, + ERC20Rebasable__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ factory: L2ERC20TokenBridge__factory, args: [ optAddresses.L2CrossDomainMessenger, expectedL1TokenBridgeProxyAddress, l1Token, + l1TokenRebasable, expectedL2TokenProxyAddress, + expectedL2TokenRebasableProxyAddress, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index febb4de7..bb030fcf 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -1,4 +1,4 @@ -import { Signer } from "ethers"; +import { BigNumber, Signer } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { @@ -9,9 +9,12 @@ import { L2ERC20TokenBridge, ERC20Bridged__factory, ERC20BridgedStub__factory, + ERC20WrapableStub__factory, + TokensRateOracleStub__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, CrossDomainMessengerStub__factory, + ERC20Rebasable__factory, } from "../../typechain"; import addresses from "./addresses"; import contracts from "./contracts"; @@ -157,9 +160,14 @@ async function loadDeployedBridges( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), + l1TokenRebasable: IERC20__factory.connect( + testingUtils.env.OPT_L1_TOKEN(), + l1SignerOrProvider + ), ...connectBridgeContracts( { l2Token: testingUtils.env.OPT_L2_TOKEN(), + l2TokenRebasable: testingUtils.env.OPT_L2_TOKEN(), // fix l1ERC20TokenBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), l2ERC20TokenBridge: testingUtils.env.OPT_L2_ERC20_TOKEN_BRIDGE(), }, @@ -177,15 +185,28 @@ 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 ERC20WrapableStub__factory(ethDeployer).deploy( + l1TokenRebasable.address, "Test Token", "TT" ); + const tokensRateOracleStub = await new TokensRateOracleStub__factory(optDeployer).deploy(); + await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("2000000000000000000")); + await tokensRateOracleStub.setDecimals(18); + await tokensRateOracleStub.setUpdatedAt(100); + const [ethDeployScript, optDeployScript] = await deployment( networkName ).erc20TokenBridgeDeployScript( l1Token.address, + l1TokenRebasable.address, + tokensRateOracleStub.address, { deployer: ethDeployer, admins: { proxy: ethDeployer.address, bridge: ethDeployer.address }, @@ -205,7 +226,7 @@ async function deployTestBridge( ethDeployer ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 3; + const l2ERC20TokenBridgeProxyDeployStepIndex = 5; const l2BridgingManagement = new BridgingManagement( optDeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), optDeployer @@ -225,11 +246,13 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), + l1TokenRebasable: l1TokenRebasable.connect(ethProvider), ...connectBridgeContracts( { l2Token: optDeployScript.getContractAddress(1), + l2TokenRebasable: optDeployScript.getContractAddress(3), l1ERC20TokenBridge: ethDeployScript.getContractAddress(1), - l2ERC20TokenBridge: optDeployScript.getContractAddress(3), + l2ERC20TokenBridge: optDeployScript.getContractAddress(5), }, ethProvider, optProvider @@ -240,6 +263,7 @@ async function deployTestBridge( function connectBridgeContracts( addresses: { l2Token: string; + l2TokenRebasable: string; l1ERC20TokenBridge: string; l2ERC20TokenBridge: string; }, @@ -258,8 +282,13 @@ function connectBridgeContracts( addresses.l2Token, optSignerOrProvider ); + const l2TokenRebasable = ERC20Rebasable__factory.connect( + addresses.l2TokenRebasable, + optSignerOrProvider + ); return { l2Token, + l2TokenRebasable, l1ERC20TokenBridge, l2ERC20TokenBridge, }; From c3bc0db60c2bba7eb6876ca68c425968a61c004a Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 6 Nov 2023 00:40:15 +0100 Subject: [PATCH 06/61] add withdraw flow for rebasable token on optimism --- contracts/BridgeableTokens.sol | 24 +- .../arbitrum/InterchainERC20TokenGateway.sol | 14 +- contracts/arbitrum/L1ERC20TokenGateway.sol | 4 +- contracts/arbitrum/L2ERC20TokenGateway.sol | 17 +- contracts/optimism/L1ERC20TokenBridge.sol | 52 ++-- contracts/optimism/L2ERC20TokenBridge.sol | 48 ++-- .../optimism/interfaces/IL2ERC20Bridge.sol | 1 + contracts/stubs/ERC20WrapableStub.sol | 14 +- .../bridging-rebase.integration.test.ts | 224 +++++++++--------- 9 files changed, 204 insertions(+), 194 deletions(-) diff --git a/contracts/BridgeableTokens.sol b/contracts/BridgeableTokens.sol index 36a636b9..a0e85e50 100644 --- a/contracts/BridgeableTokens.sol +++ b/contracts/BridgeableTokens.sol @@ -6,30 +6,32 @@ pragma solidity 0.8.10; /// @author psirex /// @notice Contains the logic for validation of tokens used in the bridging process contract BridgeableTokens { - /// @notice Address of the bridged token in the L1 chain - address public immutable l1Token; + /// @notice Address of the bridged non rebasable token in the L1 chain + address public immutable l1TokenNonRebasable; /// @notice Address of the bridged rebasable token in the L1 chain address public immutable l1TokenRebasable; - /// @notice Address of the token minted on the L2 chain when token bridged - address public immutable l2Token; + /// @notice Address of the non rebasable token minted on the L2 chain when token bridged + address public immutable l2TokenNonRebasable; /// @notice Address of the rebasable token minted on the L2 chain when token bridged address public immutable l2TokenRebasable; - /// @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 l1Token_, address l1TokenRebasable_, address l2Token_, address l2TokenRebasable_) { - l1Token = l1Token_; + /// @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_) { + l1TokenNonRebasable = l1TokenNonRebasable_; l1TokenRebasable = l1TokenRebasable_; - l2Token = l2Token_; + l2TokenNonRebasable = l2TokenNonRebasable_; l2TokenRebasable = l2TokenRebasable_; } /// @dev Validates that passed l1Token_ is supported by the bridge modifier onlySupportedL1Token(address l1Token_) { - if (l1Token_ != l1Token && l1Token_ != l1TokenRebasable) { + if (l1Token_ != l1TokenNonRebasable && l1Token_ != l1TokenRebasable) { revert ErrorUnsupportedL1Token(); } _; @@ -37,7 +39,7 @@ contract BridgeableTokens { /// @dev Validates that passed l2Token_ is supported by the bridge modifier onlySupportedL2Token(address l2Token_) { - if (l2Token_ != l2Token && l2Token_ != l2TokenRebasable) { + if (l2Token_ != l2TokenNonRebasable && l2Token_ != l2TokenRebasable) { revert ErrorUnsupportedL2Token(); } _; diff --git a/contracts/arbitrum/InterchainERC20TokenGateway.sol b/contracts/arbitrum/InterchainERC20TokenGateway.sol index f75334d1..78f9be68 100644 --- a/contracts/arbitrum/InterchainERC20TokenGateway.sol +++ b/contracts/arbitrum/InterchainERC20TokenGateway.sol @@ -24,18 +24,18 @@ abstract contract InterchainERC20TokenGateway is /// @param router_ Address of the router in the corresponding chain /// @param counterpartGateway_ Address of the counterpart gateway used in the bridging process - /// @param l1Token_ Address of the bridged token in the Ethereum chain + /// @param l1TokenNonRebasable Address of the bridged token in the Ethereum chain /// @param l1TokenRebasable_ Address of the bridged token in the Ethereum chain - /// @param l2Token_ Address of the token minted on the Arbitrum chain when token bridged + /// @param l2TokenNonRebasable_ Address of the token minted on the Arbitrum chain when token bridged /// @param l2TokenRebasable_ Address of the token minted on the Arbitrum chain when token bridged constructor( address router_, address counterpartGateway_, - address l1Token_, + address l1TokenNonRebasable, address l1TokenRebasable_, - address l2Token_, + address l2TokenNonRebasable_, address l2TokenRebasable_ - ) BridgeableTokens(l1Token_, l1TokenRebasable_, l2Token_, l2TokenRebasable_) { + ) BridgeableTokens(l1TokenNonRebasable, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { router = router_; counterpartGateway = counterpartGateway_; } @@ -48,8 +48,8 @@ abstract contract InterchainERC20TokenGateway is view returns (address) { - if (l1Token_ == l1Token) { - return l2Token; + if (l1Token_ == l1TokenRebasable) { + return l2TokenNonRebasable; } return address(0); } diff --git a/contracts/arbitrum/L1ERC20TokenGateway.sol b/contracts/arbitrum/L1ERC20TokenGateway.sol index 7b91b6a2..85161aec 100644 --- a/contracts/arbitrum/L1ERC20TokenGateway.sol +++ b/contracts/arbitrum/L1ERC20TokenGateway.sol @@ -82,7 +82,7 @@ contract L1ERC20TokenGateway is }) ); - emit DepositInitiated(l1Token, from, to_, retryableTicketId, amount_); + emit DepositInitiated(l1TokenNonRebasable, from, to_, retryableTicketId, amount_); return abi.encode(retryableTicketId); } @@ -117,7 +117,7 @@ contract L1ERC20TokenGateway is sendCrossDomainMessage( from_, counterpartGateway, - getOutboundCalldata(l1Token, from_, to_, amount_, ""), + getOutboundCalldata(l1TokenNonRebasable, from_, to_, amount_, ""), messageOptions ); } diff --git a/contracts/arbitrum/L2ERC20TokenGateway.sol b/contracts/arbitrum/L2ERC20TokenGateway.sol index d65fd3ac..0c6df5ac 100644 --- a/contracts/arbitrum/L2ERC20TokenGateway.sol +++ b/contracts/arbitrum/L2ERC20TokenGateway.sol @@ -21,24 +21,25 @@ contract L2ERC20TokenGateway is /// @param arbSys_ Address of the Arbitrum’s ArbSys contract in the L2 chain /// @param router_ Address of the router in the L2 chain /// @param counterpartGateway_ Address of the counterpart L1 gateway - /// @param l1Token_ Address of the bridged token in the L1 chain + /// @param l1TokenNonRebasable_ Address of the bridged token in the L1 chain /// @param l1TokenRebasable_ Address of the bridged token in the L1 chain - /// @param l2Token_ Address of the token minted on the Arbitrum chain when token bridged + /// @param l2TokenNonRebasable_ Address of the token minted on the Arbitrum chain when token bridged + /// @param l2TokenRebasable_ Address of the token minted on the Arbitrum chain when token bridged constructor( address arbSys_, address router_, address counterpartGateway_, - address l1Token_, + address l1TokenNonRebasable_, address l1TokenRebasable_, - address l2Token_, + address l2TokenNonRebasable_, address l2TokenRebasable_ ) InterchainERC20TokenGateway( router_, counterpartGateway_, - l1Token_, + l1TokenNonRebasable_, l1TokenRebasable_, - l2Token_, + l2TokenNonRebasable_, l2TokenRebasable_ ) L2CrossDomainEnabled(arbSys_) @@ -60,7 +61,7 @@ contract L2ERC20TokenGateway is { address from = L2OutboundDataParser.decode(router, data_); - IERC20Bridged(l2Token).bridgeBurn(from, amount_); + IERC20Bridged(l2TokenNonRebasable).bridgeBurn(from, amount_); uint256 id = sendCrossDomainMessage( from, @@ -88,7 +89,7 @@ contract L2ERC20TokenGateway is onlySupportedL1Token(l1Token_) onlyFromCrossDomainAccount(counterpartGateway) { - IERC20Bridged(l2Token).bridgeMint(to_, amount_); + IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); emit DepositFinalized(l1Token_, from_, to_, amount_); } diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index d543a961..93b9535f 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -34,23 +34,24 @@ contract L1ERC20TokenBridge is /// @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 l1TokenNonRebasable_ Address of the bridged token in the L1 chain /// @param l1TokenRebasable_ Address of the bridged token in the L1 chain - /// @param l2Token_ Address of the token minted on the L2 chain when token bridged + /// @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 l1Token_, // wstETH - address l1TokenRebasable_, // stETH - address l2Token_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l1TokenRebasable_, l2Token_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) BridgeableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l2TokenBridge = l2TokenBridge_; } /// @inheritdoc IL1ERC20Bridge function depositERC20( - address l1Token_, // stETH or wstETH + address l1Token_, address l2Token_, uint256 amount_, uint32 l2Gas_, @@ -66,19 +67,14 @@ contract L1ERC20TokenBridge is } if(l1Token_ == l1TokenRebasable) { - bytes memory data = bytes.concat(hex'01', data_); // add rate + bytes memory data = bytes.concat(hex'01', data_); IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); - IERC20(l1TokenRebasable).approve(l1Token, amount_); - uint256 wstETHAmount = IERC20Wrapable(l1Token).wrap(amount_); - console.log("wstETHAmount=",wstETHAmount); - - uint256 balanceOfl1Token = IERC20(l1Token).balanceOf(address(this)); - console.log("balanceOfl1Token=",balanceOfl1Token); - - _initiateERC20Deposit(msg.sender, msg.sender, wstETHAmount, l2Gas_, data); + IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); + uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); + _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, msg.sender, wstETHAmount, l2Gas_, data); } else { - IERC20(l1Token).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(msg.sender, msg.sender, amount_, l2Gas_, data_); + IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); + _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, data_); } } @@ -97,7 +93,7 @@ contract L1ERC20TokenBridge is onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) { - _initiateERC20Deposit(msg.sender, to_, amount_, l2Gas_, data_); + _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, data_); } /// @inheritdoc IL1ERC20Bridge @@ -115,7 +111,12 @@ contract L1ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l2TokenBridge) { - IERC20(l1Token_).safeTransfer(to_, amount_); + if (data_.length > 0 && data_[0] == hex'01') { + uint256 stETHAmount = IERC20Wrapable(l1TokenNonRebasable).unwrap(amount_); + IERC20(l1TokenRebasable).safeTransfer(to_, stETHAmount); + } else { + IERC20(l1Token_).safeTransfer(to_, amount_); + } emit ERC20WithdrawalFinalized( l1Token_, @@ -137,6 +138,8 @@ contract L1ERC20TokenBridge is /// solely as a convenience for external contracts. Aside from enforcing a maximum /// length, these contracts provide no guarantees about its content. function _initiateERC20Deposit( + address l1Token_, + address l2Token_, address from_, address to_, uint256 amount_, @@ -146,20 +149,19 @@ contract L1ERC20TokenBridge is bytes memory message = abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, - l1Token, - l2Token, + l1Token_, + l2Token_, from_, to_, amount_, data_ ); - console.log("SEND l2TokenBridge=%s l1Token=%s l2Token=%s", l2TokenBridge, l1Token, l2Token); sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); emit ERC20DepositInitiated( - l1Token, - l2Token, + l1Token_, + l2Token_, from_, to_, amount_, diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 32c72053..a8f9df01 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -7,6 +7,9 @@ import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; @@ -26,32 +29,45 @@ contract L2ERC20TokenBridge is BridgeableTokens, CrossDomainEnabled { + using SafeERC20 for IERC20; + /// @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 + /// @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 l1Token_, + address l1TokenNonRebasable_, address l1TokenRebasable_, - address l2Token_, + address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1Token_, l1TokenRebasable_, l2Token_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) BridgeableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l1TokenBridge = l1TokenBridge_; } /// @inheritdoc IL2ERC20Bridge function withdraw( + address l1Token_, address l2Token_, uint256 amount_, uint32 l1Gas_, bytes calldata data_ ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _initiateWithdrawal(msg.sender, msg.sender, amount_, l1Gas_, data_); + if(l2Token_ == l2TokenRebasable) { + bytes memory data = bytes.concat(hex'01', data_); + uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); + ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); + _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, shares, l1Gas_, data); + } else { + IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); + _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l1Gas_, data_); + } } /// @inheritdoc IL2ERC20Bridge @@ -62,7 +78,7 @@ contract L2ERC20TokenBridge is uint32 l1Gas_, bytes calldata data_ ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _initiateWithdrawal(msg.sender, to_, amount_, l1Gas_, data_); + _initiateWithdrawal(l1TokenNonRebasable, l2Token_, msg.sender, to_, amount_, l1Gas_, data_); } /// @inheritdoc IL2ERC20Bridge @@ -81,17 +97,10 @@ contract L2ERC20TokenBridge is onlyFromCrossDomainAccount(l1TokenBridge) { if (data_.length > 0 && data_[0] == hex'01') { - console.log("finalizeDeposit1 l2TokenRebasable balanceBefore=",ERC20Rebasable(l2TokenRebasable).balanceOf(to_)); - ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); - - console.log("finalizeDeposit1 l2TokenRebasable balanceafter==",ERC20Rebasable(l2TokenRebasable).balanceOf(to_)); - bytes memory data = data_[1:]; emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data); } else { - console.log("finalizeDeposit2 data=", data_.length); - IERC20Bridged(l2Token_).bridgeMint(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); } @@ -107,18 +116,19 @@ contract L2ERC20TokenBridge is /// solely as a convenience for external contracts. Aside from enforcing a maximum /// length, these contracts provide no guarantees about its content function _initiateWithdrawal( + address l1Token_, + address l2Token_, address from_, address to_, uint256 amount_, uint32 l1Gas_, - bytes calldata data_ + bytes memory data_ ) internal { - IERC20Bridged(l2Token).bridgeBurn(from_, amount_); bytes memory message = abi.encodeWithSelector( IL1ERC20Bridge.finalizeERC20Withdrawal.selector, - l1Token, - l2Token, + l1Token_, + l2Token_, from_, to_, amount_, @@ -127,6 +137,6 @@ contract L2ERC20TokenBridge is sendCrossDomainMessage(l1TokenBridge, l1Gas_, message); - emit WithdrawalInitiated(l1Token, l2Token, from_, to_, amount_, data_); + emit WithdrawalInitiated(l1Token_, l2Token_, from_, to_, amount_, data_); } } diff --git a/contracts/optimism/interfaces/IL2ERC20Bridge.sol b/contracts/optimism/interfaces/IL2ERC20Bridge.sol index 448dfb8b..04c27baf 100644 --- a/contracts/optimism/interfaces/IL2ERC20Bridge.sol +++ b/contracts/optimism/interfaces/IL2ERC20Bridge.sol @@ -46,6 +46,7 @@ interface IL2ERC20Bridge { /// solely as a convenience for external contracts. Aside from enforcing a maximum /// length, these contracts provide no guarantees about its content. function withdraw( + address l1Token_, address l2Token_, uint256 amount_, uint32 l1Gas_, diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index fe49ff83..2edf81d1 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -32,12 +32,6 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { uint256 wstETHAmount = (_stETHAmount * tokensRate) / (10 ** uint256(decimals())); - - console.log("wrap msg.sender=",msg.sender); - console.log("wrap address(this)=",address(this)); - console.log("wrap _stETHAmount=",_stETHAmount); - console.log("wrap wstETHAmount=",wstETHAmount); - _mint(msg.sender, wstETHAmount); stETH.transferFrom(msg.sender, address(this), _stETHAmount); @@ -47,10 +41,10 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { function unwrap(uint256 _wstETHAmount) external returns (uint256) { require(_wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); - // uint256 stETHAmount = (_wstETHAmount * (10 ** uint256(decimals()))) / tokensRate; - // _burn(msg.sender, _wstETHAmount); - // stETH.transfer(msg.sender, stETHAmount); + uint256 stETHAmount = (_wstETHAmount * (10 ** uint256(decimals()))) / tokensRate; + _burn(msg.sender, _wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); - return 0; //stETHAmount; + return stETHAmount; } } diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 5c72b102..71193d4c 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -77,6 +77,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1TokenRebasable, // stETH l1ERC20TokenBridge, l2Token, + l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, } = ctx; @@ -84,12 +85,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { depositAmount: depositAmountInRebasableTokens } = ctx.common; const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); - console.log("depositAmount=",depositAmount); - console.log("depositAmountInRebasableTokens=",depositAmountInRebasableTokens); - - console.log("l1Token=",l1Token.address); - console.log("l1TokenRebasable=",l1TokenRebasable.address); - console.log("l1ERC20TokenBridge=",l1ERC20TokenBridge.address); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -98,27 +93,24 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); - console.log("tokenHolderA=", tokenHolderA.address); - console.log("tokenHolderABalanceBefore=", tokenHolderABalanceBefore); const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( l1ERC20TokenBridge.address ); - console.log("l1ERC20TokenBridgeBalanceBefore=", l1ERC20TokenBridgeBalanceBefore); const tx = await l1ERC20TokenBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1TokenRebasable.address, - l2Token.address, + l2TokenRebasable.address, depositAmountInRebasableTokens, 200_000, "0x" ); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ - l1Token.address, - l2Token.address, + l1TokenRebasable.address, + l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, depositAmount, @@ -128,8 +120,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( "finalizeDeposit", [ - l1Token.address, - l2Token.address, + l1TokenRebasable.address, + l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, depositAmount, @@ -147,16 +139,11 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 200_000, ]); - - assert.equalBN( await l1Token.balanceOf(l1ERC20TokenBridge.address), l1ERC20TokenBridgeBalanceBefore.add(depositAmount) ); - // console.log("qwe=",await l1TokenRebasable.balanceOf(tokenHolderA.address)); - // console.log("asd=",tokenHolderABalanceBefore.sub(depositAmount)); - assert.equalBN( await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH tokenHolderABalanceBefore.sub(depositAmountInRebasableTokens) @@ -172,24 +159,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2CrossDomainMessenger, l2ERC20TokenBridge, } = ctx; - // const { depositAmount } = ctx.common; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = ctx.accounts; - console.log("Finalize1 depositAmount=",depositAmount); const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( tokenHolderA.address ); - console.log("tokenHolderABalanceBefore=",tokenHolderABalanceBefore); const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); - - console.log("test1", l1ERC20TokenBridge.address, l2ERC20TokenBridge.address, l1TokenRebasable.address, l2Token.address); - + const tx = await l2CrossDomainMessenger .connect(l1CrossDomainMessengerAliased) .relayMessage( @@ -219,10 +201,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) depositAmount, "0x", ]); - // console.log(await l1TokenRebasable.balanceOf(tokenHolderA.address)); - - console.log(await l2TokenRebasable.balanceOf(tokenHolderA.address)); - console.log(tokenHolderABalanceBefore.add(depositAmountInRebasableTokens)); assert.equalBN( await l2TokenRebasable.balanceOf(tokenHolderA.address), @@ -234,99 +212,121 @@ 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; + .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { + const { accountA: tokenHolderA } = ctx.accounts; + const { withdrawalAmount: withdrawalAmountInRebasableTokens } = ctx.common; + const { + l1Token, + l2Token, + l1TokenRebasable, + l2TokenRebasable, + l2ERC20TokenBridge + } = ctx; -// const tokenHolderABalanceBefore = await l2Token.balanceOf( -// tokenHolderA.address -// ); -// const l2TotalSupplyBefore = await l2Token.totalSupply(); + const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).mul(2); -// const tx = await l2ERC20TokenBridge -// .connect(tokenHolderA.l2Signer) -// .withdraw(l2Token.address, withdrawalAmount, 0, "0x"); + const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l2TotalSupplyBefore = await l2TokenRebasable.totalSupply(); -// await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderA.address, -// withdrawalAmount, -// "0x", -// ]); -// assert.equalBN( -// await l2Token.balanceOf(tokenHolderA.address), -// tokenHolderABalanceBefore.sub(withdrawalAmount) -// ); -// assert.equalBN( -// await l2Token.totalSupply(), -// l2TotalSupplyBefore.sub(withdrawalAmount) -// ); -// }) + await l2TokenRebasable + .connect(tokenHolderA.l2Signer) + .approve(l2ERC20TokenBridge.address, withdrawalAmountInRebasableTokens); -// .step("Finalize withdrawal on L1", async (ctx) => { -// const { -// l1Token, -// l1CrossDomainMessenger, -// l1ERC20TokenBridge, -// l2CrossDomainMessenger, -// l2Token, -// l2ERC20TokenBridge, -// } = ctx; -// const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; -// const { withdrawalAmount } = ctx.common; + const tx = await l2ERC20TokenBridge + .connect(tokenHolderA.l2Signer) + .withdraw( + l1TokenRebasable.address, + l2TokenRebasable.address, + withdrawalAmountInRebasableTokens, + 0, + "0x" + ); -// const tokenHolderABalanceBefore = await l1Token.balanceOf( -// tokenHolderA.address -// ); -// const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( -// l1ERC20TokenBridge.address -// ); + await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmount, + "0x01", + ]); + -// await l1CrossDomainMessenger -// .connect(l1Stranger) -// .setXDomainMessageSender(l2ERC20TokenBridge.address); + assert.equalBN( + await l2TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.sub(withdrawalAmountInRebasableTokens) + ); + assert.equalBN( + await l2TokenRebasable.totalSupply(), + l2TotalSupplyBefore.sub(withdrawalAmountInRebasableTokens) + ); + }) -// const tx = await l1CrossDomainMessenger -// .connect(l1Stranger) -// .relayMessage( -// l1ERC20TokenBridge.address, -// l2CrossDomainMessenger.address, -// l1ERC20TokenBridge.interface.encodeFunctionData( -// "finalizeERC20Withdrawal", -// [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderA.address, -// withdrawalAmount, -// "0x", -// ] -// ), -// 0 -// ); + .step("Finalize withdrawal on L1", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1CrossDomainMessenger, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2TokenRebasable, + l2ERC20TokenBridge, + } = ctx; + const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; + const { withdrawalAmount: withdrawalAmountInRebasableTokens } = ctx.common; + const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).mul(2); -// await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderA.address, -// withdrawalAmount, -// "0x", -// ]); + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); -// assert.equalBN( -// await l1Token.balanceOf(l1ERC20TokenBridge.address), -// l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) -// ); + await l1CrossDomainMessenger + .connect(l1Stranger) + .setXDomainMessageSender(l2ERC20TokenBridge.address); -// assert.equalBN( -// await l1Token.balanceOf(tokenHolderA.address), -// tokenHolderABalanceBefore.add(withdrawalAmount) -// ); -// }) + const tx = await l1CrossDomainMessenger + .connect(l1Stranger) + .relayMessage( + l1ERC20TokenBridge.address, + l2CrossDomainMessenger.address, + l1ERC20TokenBridge.interface.encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmount, + "0x01", + ] + ), + 0 + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + withdrawalAmount, + "0x01", + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(withdrawalAmountInRebasableTokens) + ); + }) // .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { // const { From eb98d0b976e280797469ec0be1c48a12e695fc50 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 9 Nov 2023 20:54:45 +0100 Subject: [PATCH 07/61] send rate in data and time during deposit flow --- contracts/BridgeableTokens.sol | 8 ++++ contracts/optimism/DepositDataCodec.sol | 38 +++++++++++++++++ contracts/optimism/L1ERC20TokenBridge.sol | 24 +++++++---- contracts/optimism/L2ERC20TokenBridge.sol | 29 ++++++++----- contracts/stubs/ERC20WrapableStub.sol | 4 ++ contracts/stubs/TokensRateOracleStub.sol | 5 +++ contracts/token/ERC20Rebasable.sol | 4 ++ contracts/token/interfaces/IERC20Wrapable.sol | 31 +++----------- .../token/interfaces/ITokensRateOracle.sol | 6 +++ .../bridging-rebase.integration.test.ts | 42 +++++++++++++++---- utils/optimism/deployment.ts | 1 + utils/optimism/testing.ts | 10 ++++- 12 files changed, 148 insertions(+), 54 deletions(-) create mode 100644 contracts/optimism/DepositDataCodec.sol diff --git a/contracts/BridgeableTokens.sol b/contracts/BridgeableTokens.sol index a0e85e50..6416d97d 100644 --- a/contracts/BridgeableTokens.sol +++ b/contracts/BridgeableTokens.sol @@ -53,6 +53,14 @@ contract BridgeableTokens { _; } + function isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + return l1Token_ == l1TokenRebasable && l2Token_ == l2TokenRebasable; + } + + function isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + return l1Token_ == l1TokenNonRebasable && l2Token_ == l2TokenNonRebasable; + } + error ErrorUnsupportedL1Token(); error ErrorUnsupportedL2Token(); error ErrorAccountIsZeroAddress(); diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol new file mode 100644 index 00000000..9da4265b --- /dev/null +++ b/contracts/optimism/DepositDataCodec.sol @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +contract DepositDataCodec { + + struct DepositData { + uint256 rate; + uint256 time; + bytes data; + } + + function encodeDepositData(DepositData memory depositData) internal pure returns (bytes memory) { + bytes memory data = bytes.concat( + abi.encodePacked(depositData.rate), + abi.encodePacked(depositData.time), + abi.encodePacked(depositData.data) + ); + return data; + } + + function decodeDepositData(bytes calldata buffer) internal pure returns (DepositData memory) { + + if (buffer.length < 32 * 2) { + revert ErrorDepositDataLength(); + } + + DepositData memory depositData; + depositData.rate = uint256(bytes32(buffer[0:31])); + depositData.time = uint256(bytes32(buffer[32:63])); + depositData.data = buffer[64:]; + + return depositData; + } + + error ErrorDepositDataLength(); +} \ No newline at end of file diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 93b9535f..96f18a7f 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -13,6 +13,7 @@ import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {DepositDataCodec} from "./DepositDataCodec.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; import "hardhat/console.sol"; @@ -25,7 +26,8 @@ contract L1ERC20TokenBridge is IL1ERC20Bridge, BridgingManager, BridgeableTokens, - CrossDomainEnabled + CrossDomainEnabled, + DepositDataCodec { using SafeERC20 for IERC20; @@ -65,16 +67,22 @@ contract L1ERC20TokenBridge is if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } + + DepositData memory depositData; + depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); + depositData.time = block.timestamp; + depositData.data = data_; - if(l1Token_ == l1TokenRebasable) { - bytes memory data = bytes.concat(hex'01', data_); + bytes memory encodedDepositData = encodeDepositData(depositData); + + if (isRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, msg.sender, wstETHAmount, l2Gas_, data); - } else { + _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, msg.sender, wstETHAmount, l2Gas_, encodedDepositData); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, data_); + _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, msg.sender, amount_, l2Gas_, encodedDepositData); } } @@ -111,10 +119,10 @@ contract L1ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l2TokenBridge) { - if (data_.length > 0 && data_[0] == hex'01') { + if (isRebasableTokenFlow(l1Token_, l2Token_)) { uint256 stETHAmount = IERC20Wrapable(l1TokenNonRebasable).unwrap(amount_); IERC20(l1TokenRebasable).safeTransfer(to_, stETHAmount); - } else { + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(l1Token_).safeTransfer(to_, amount_); } diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index a8f9df01..595d1bf1 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -6,6 +6,7 @@ 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 {ITokensRateOracle} from "../token/interfaces/ITokensRateOracle.sol"; import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; @@ -14,6 +15,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {BridgingManager} from "../BridgingManager.sol"; import {BridgeableTokens} from "../BridgeableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {DepositDataCodec} from "./DepositDataCodec.sol"; import { console } from "hardhat/console.sol"; @@ -27,13 +29,18 @@ contract L2ERC20TokenBridge is IL2ERC20Bridge, BridgingManager, BridgeableTokens, - CrossDomainEnabled + CrossDomainEnabled, + DepositDataCodec { using SafeERC20 for IERC20; /// @inheritdoc IL2ERC20Bridge address public immutable l1TokenBridge; + address public immutable tokensRateOracle; + + + /// @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 @@ -46,9 +53,11 @@ contract L2ERC20TokenBridge is address l1TokenNonRebasable_, address l1TokenRebasable_, address l2TokenNonRebasable_, - address l2TokenRebasable_ + address l2TokenRebasable_, + address tokensRateOracle_ ) CrossDomainEnabled(messenger_) BridgeableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l1TokenBridge = l1TokenBridge_; + tokensRateOracle = tokensRateOracle_; } /// @inheritdoc IL2ERC20Bridge @@ -60,10 +69,9 @@ contract L2ERC20TokenBridge is bytes calldata data_ ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { if(l2Token_ == l2TokenRebasable) { - bytes memory data = bytes.concat(hex'01', data_); uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); - _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, shares, l1Gas_, data); + _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, shares, l1Gas_, data_); } else { IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l1Gas_, data_); @@ -96,14 +104,15 @@ contract L2ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l1TokenBridge) { - if (data_.length > 0 && data_[0] == hex'01') { + DepositData memory depositData = decodeDepositData(data_); + ITokensRateOracle(tokensRateOracle).updateRate(int256(depositData.rate), depositData.time); + + if (isRebasableTokenFlow(l1Token_, l2Token_)) { ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); - bytes memory data = data_[1:]; - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data); - } else { - IERC20Bridged(l2Token_).bridgeMint(to_, amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); } + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } /// @notice Performs the logic for withdrawals by burning the token and informing diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index 2edf81d1..b8e35599 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -47,4 +47,8 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { return stETHAmount; } + + function tokensPerStEth() external view returns (uint256) { + return tokensRate; + } } diff --git a/contracts/stubs/TokensRateOracleStub.sol b/contracts/stubs/TokensRateOracleStub.sol index 6f397515..4c43af26 100644 --- a/contracts/stubs/TokensRateOracleStub.sol +++ b/contracts/stubs/TokensRateOracleStub.sol @@ -44,4 +44,9 @@ contract TokensRateOracleStub is ITokensRateOracle { ) { return (0,latestRoundDataAnswer,0,latestRoundDataUpdatedAt,0); } + + function updateRate(int256 rate, uint256 updatedAt) external { + latestRoundDataAnswer = rate; + latestRoundDataUpdatedAt = updatedAt; + } } \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 04899582..0501dde8 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -75,6 +75,10 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } + function tokensPerStEth() external view returns (uint256) { + return 0; + } + function mintShares(address account_, uint256 amount_) external returns (uint256) { return _mintShares(account_, amount_); } diff --git a/contracts/token/interfaces/IERC20Wrapable.sol b/contracts/token/interfaces/IERC20Wrapable.sol index 15213ccd..de82ee27 100644 --- a/contracts/token/interfaces/IERC20Wrapable.sol +++ b/contracts/token/interfaces/IERC20Wrapable.sol @@ -30,30 +30,9 @@ interface IERC20Wrapable { */ function unwrap(uint256 wrapableTokenAmount_) external returns (uint256); - // TODO: - // /** - // * @notice Get amount of wstETH for a given amount of stETH - // * @param _stETHAmount amount of stETH - // * @return Amount of wstETH for a given stETH amount - // */ - // function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); - - // /** - // * @notice Get amount of stETH for a given amount of wstETH - // * @param _wstETHAmount amount of wstETH - // * @return Amount of stETH for a given wstETH amount - // */ - // function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256); - - // /** - // * @notice Get amount of stETH for a one wstETH - // * @return Amount of stETH for 1 wstETH - // */ - // function stEthPerToken() external view returns (uint256); - - // /** - // * @notice Get amount of wstETH for a one stETH - // * @return Amount of wstETH for a 1 stETH - // */ - // function tokensPerStEth() external view returns (uint256); + /** + * @notice Get amount of wstETH for a one stETH + * @return Amount of wstETH for a 1 stETH + */ + function tokensPerStEth() external view returns (uint256); } \ No newline at end of file diff --git a/contracts/token/interfaces/ITokensRateOracle.sol b/contracts/token/interfaces/ITokensRateOracle.sol index 843be503..c28cabc9 100644 --- a/contracts/token/interfaces/ITokensRateOracle.sol +++ b/contracts/token/interfaces/ITokensRateOracle.sol @@ -7,6 +7,8 @@ pragma solidity 0.8.10; /// @notice Oracle interface for two tokens rate interface ITokensRateOracle { + function updateRate(int256 rate, uint256 updatedAt) external; + /** * @notice represents the number of decimals the oracle responses represent. */ @@ -25,4 +27,8 @@ interface ITokensRateOracle { uint256 updatedAt, uint80 answeredInRound ); +} + +interface ITokensRateOracleUpdatable { + function updateRate(int256 rate, uint256 updatedAt) external; } \ No newline at end of file diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 71193d4c..6b4ab756 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -80,11 +80,15 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, + tokensRateOracle, + l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; - const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); + const tokensPerStEth = await l1Token.tokensPerStEth(); + + await tokensRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -108,13 +112,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); + const blockNumber = await l1Provider.getBlockNumber(); + const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + const dataToSend = tokensPerStEthStr + blockTimestampStr.slice(2); + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x01", + dataToSend, ]); const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( @@ -125,7 +135,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x01", + dataToSend, ] ); @@ -152,15 +162,27 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Finalize deposit on L2", async (ctx) => { const { + l1Token, l1TokenRebasable, l2Token, l2TokenRebasable, l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, + tokensRateOracle, + l2Provider } = ctx; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); + const tokensPerStEth = await l1Token.tokensPerStEth(); + + + const blockNumber = await l2Provider.getBlockNumber(); + const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + const dataToReceive = tokensPerStEthStr + blockTimestampStr.slice(2); + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = ctx.accounts; @@ -186,12 +208,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x01", + dataToReceive, ]), { gasLimit: 5_000_000 } ); - console.log("test2"); + + const [,tokensRate,,,] = await tokensRateOracle.latestRoundData(); + console.log("tokensPerStEth=",tokensPerStEth); + console.log("tokensRate=",tokensRate); + assert.equalBN(tokensPerStEth, tokensRate); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, @@ -250,7 +276,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, withdrawalAmount, - "0x01", + "0x", ]); @@ -302,7 +328,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, withdrawalAmount, - "0x01", + "0x", ] ), 0 @@ -314,7 +340,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, withdrawalAmount, - "0x01", + "0x", ]); assert.equalBN( diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index 39ce1f7f..4a3625a3 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -174,6 +174,7 @@ export default function deployment( l1TokenRebasable, expectedL2TokenProxyAddress, expectedL2TokenRebasableProxyAddress, + tokensRateOracleStub, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index bb030fcf..721ed54b 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -156,7 +156,7 @@ async function loadDeployedBridges( l2SignerOrProvider: SignerOrProvider ) { return { - l1Token: IERC20__factory.connect( + l1Token: ERC20WrapableStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), @@ -164,6 +164,11 @@ async function loadDeployedBridges( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), + tokensRateOracle: TokensRateOracleStub__factory.connect( + testingUtils.env.OPT_L1_TOKEN(), + l1SignerOrProvider + ), + ...connectBridgeContracts( { l2Token: testingUtils.env.OPT_L2_TOKEN(), @@ -197,7 +202,7 @@ async function deployTestBridge( ); const tokensRateOracleStub = await new TokensRateOracleStub__factory(optDeployer).deploy(); - await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("2000000000000000000")); + await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); await tokensRateOracleStub.setDecimals(18); await tokensRateOracleStub.setUpdatedAt(100); @@ -247,6 +252,7 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), l1TokenRebasable: l1TokenRebasable.connect(ethProvider), + tokensRateOracle: tokensRateOracleStub, ...connectBridgeContracts( { l2Token: optDeployScript.getContractAddress(1), From 800e33fde9fe77c8d83e80eb025319c67a554669 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 10 Nov 2023 00:05:31 +0100 Subject: [PATCH 08/61] add possibility to depost 0 tokens --- contracts/optimism/DepositDataCodec.sol | 4 +- contracts/optimism/L1ERC20TokenBridge.sol | 53 +++-- contracts/optimism/L2ERC20TokenBridge.sol | 37 ++-- .../optimism/interfaces/IL2ERC20Bridge.sol | 1 - contracts/stubs/ERC20WrapableStub.sol | 3 +- .../bridging-rebase.integration.test.ts | 187 ++++++++++++++++-- 6 files changed, 232 insertions(+), 53 deletions(-) diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol index 9da4265b..a7bc919a 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/optimism/DepositDataCodec.sol @@ -27,8 +27,8 @@ contract DepositDataCodec { } DepositData memory depositData; - depositData.rate = uint256(bytes32(buffer[0:31])); - depositData.time = uint256(bytes32(buffer[32:63])); + depositData.rate = uint256(bytes32(buffer[0:32])); + depositData.time = uint256(bytes32(buffer[32:64])); depositData.data = buffer[64:]; return depositData; diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 96f18a7f..1353325f 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -68,22 +68,7 @@ contract L1ERC20TokenBridge is revert ErrorSenderNotEOA(); } - DepositData memory depositData; - depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); - depositData.time = block.timestamp; - depositData.data = data_; - - bytes memory encodedDepositData = encodeDepositData(depositData); - - if (isRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); - IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); - uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); - _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, msg.sender, wstETHAmount, l2Gas_, encodedDepositData); - } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, msg.sender, amount_, l2Gas_, encodedDepositData); - } + _depositERC20To(l1Token_, l2Token_, msg.sender, amount_, l2Gas_, data_); } /// @inheritdoc IL1ERC20Bridge @@ -101,7 +86,7 @@ contract L1ERC20TokenBridge is onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) { - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, data_); + _depositERC20To(l1Token_, l2Token_, to_, amount_, l2Gas_, data_); } /// @inheritdoc IL1ERC20Bridge @@ -123,7 +108,7 @@ contract L1ERC20TokenBridge is uint256 stETHAmount = IERC20Wrapable(l1TokenNonRebasable).unwrap(amount_); IERC20(l1TokenRebasable).safeTransfer(to_, stETHAmount); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1Token_).safeTransfer(to_, amount_); + IERC20(l1TokenNonRebasable).safeTransfer(to_, amount_); } emit ERC20WithdrawalFinalized( @@ -136,6 +121,38 @@ contract L1ERC20TokenBridge is ); } + function _depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) internal { + + DepositData memory depositData; + depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); + depositData.time = block.timestamp; + depositData.data = data_; + + bytes memory encodedDepositData = encodeDepositData(depositData); + + if (amount_ == 0) { + _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + return; + } + + if (isRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); + IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); + uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); + _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); + _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + } + } + /// @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 diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 595d1bf1..68f2bceb 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -39,8 +39,6 @@ contract L2ERC20TokenBridge is address public immutable tokensRateOracle; - - /// @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 @@ -62,20 +60,12 @@ contract L2ERC20TokenBridge is /// @inheritdoc IL2ERC20Bridge function withdraw( - address l1Token_, address l2Token_, uint256 amount_, uint32 l1Gas_, bytes calldata data_ ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - if(l2Token_ == l2TokenRebasable) { - uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); - ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); - _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, shares, l1Gas_, data_); - } else { - IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); - _initiateWithdrawal(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l1Gas_, data_); - } + _withdrawTo(l2Token_, msg.sender, amount_, l1Gas_, data_); } /// @inheritdoc IL2ERC20Bridge @@ -85,8 +75,28 @@ contract L2ERC20TokenBridge is uint256 amount_, uint32 l1Gas_, bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _initiateWithdrawal(l1TokenNonRebasable, l2Token_, msg.sender, to_, amount_, l1Gas_, data_); + ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { + _withdrawTo(l2Token_, to_, amount_, l1Gas_, data_); + } + + function _withdrawTo( + address l2Token_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) internal { + if (l2Token_ == l2TokenRebasable) { + + uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); + ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); + _initiateWithdrawal(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, shares, l1Gas_, data_); + + } else if (l2Token_ == l2TokenNonRebasable) { + + IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); + _initiateWithdrawal(l2TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l1Gas_, data_); + } } /// @inheritdoc IL2ERC20Bridge @@ -112,6 +122,7 @@ contract L2ERC20TokenBridge is } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); } + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } diff --git a/contracts/optimism/interfaces/IL2ERC20Bridge.sol b/contracts/optimism/interfaces/IL2ERC20Bridge.sol index 04c27baf..448dfb8b 100644 --- a/contracts/optimism/interfaces/IL2ERC20Bridge.sol +++ b/contracts/optimism/interfaces/IL2ERC20Bridge.sol @@ -46,7 +46,6 @@ interface IL2ERC20Bridge { /// solely as a convenience for external contracts. Aside from enforcing a maximum /// length, these contracts provide no guarantees about its content. function withdraw( - address l1Token_, address l2Token_, uint256 amount_, uint32 l1Gas_, diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index b8e35599..f3c7f33a 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -15,13 +15,12 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { IERC20 public stETH; address public bridge; - uint256 public tokensRate; /// wst/st + uint256 public tokensRate; constructor(IERC20 stETH_, string memory name_, string memory symbol_) ERC20(name_, symbol_) { stETH = stETH_; - console.log("constructor wrap stETH=",address(stETH)); tokensRate = 2 * 10 **18; _mint(msg.sender, 1000000 * 10**18); diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 6b4ab756..e0822643 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -4,8 +4,8 @@ import env from "../../utils/env"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; import testing, { scenario } from "../../utils/testing"; -import hre, { ethers } from "hardhat"; -import { BigNumber, FixedNumber } from "ethers"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; scenario("Optimism :: Bridging integration test", ctxFactory) .after(async (ctx) => { @@ -71,12 +71,169 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); }) + .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1ERC20TokenBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + tokensRateOracle, + l1Provider + } = ctx; + + const { accountA: tokenHolderA } = ctx.accounts; + const tokensPerStEth = await l1Token.tokensPerStEth(); + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1ERC20TokenBridge.address, 0); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1ERC20TokenBridge.address + ); + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + 0, + 200_000, + "0x" + ); + + const blockNumber = await l1Provider.getBlockNumber(); + const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore + ); + }) + + .step("Finalize deposit zero tokens on L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l2TokenRebasable, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + tokensRateOracle, + l2Provider + } = ctx; + + const tokensPerStEth = await l1Token.tokensPerStEth(); + const blockNumber = await l2Provider.getBlockNumber(); + const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; + const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + + 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, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderA.address, + 0, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + + + const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); + assert.equalBN(tokensPerStEth, tokensRate); + assert.equalBN(blockTimestamp, updatedAt); + + await assert.emits(l2ERC20TokenBridge, 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, // wstETH - l1TokenRebasable, // stETH + l1Token, + l1TokenRebasable, l1ERC20TokenBridge, - l2Token, l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -116,7 +273,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToSend = tokensPerStEthStr + blockTimestampStr.slice(2); + const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, @@ -164,7 +322,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l2Token, l2TokenRebasable, l1ERC20TokenBridge, l2CrossDomainMessenger, @@ -181,7 +338,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToReceive = tokensPerStEthStr + blockTimestampStr.slice(2); + const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = @@ -214,10 +371,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); - const [,tokensRate,,,] = await tokensRateOracle.latestRoundData(); - console.log("tokensPerStEth=",tokensPerStEth); - console.log("tokensRate=",tokensRate); + const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); assert.equalBN(tokensPerStEth, tokensRate); + assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, @@ -241,9 +397,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { const { accountA: tokenHolderA } = ctx.accounts; const { withdrawalAmount: withdrawalAmountInRebasableTokens } = ctx.common; - const { - l1Token, - l2Token, + const { l1TokenRebasable, l2TokenRebasable, l2ERC20TokenBridge @@ -263,7 +417,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tx = await l2ERC20TokenBridge .connect(tokenHolderA.l2Signer) .withdraw( - l1TokenRebasable.address, l2TokenRebasable.address, withdrawalAmountInRebasableTokens, 0, @@ -278,7 +431,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) withdrawalAmount, "0x", ]); - assert.equalBN( await l2TokenRebasable.balanceOf(tokenHolderA.address), @@ -292,7 +444,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Finalize withdrawal on L1", async (ctx) => { const { - l1Token, + l1Token, l1TokenRebasable, l1CrossDomainMessenger, l1ERC20TokenBridge, @@ -354,6 +506,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); }) + // .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { // const { // l1Token, From d9ac556d663b6a79e50688cc58737ae6bde17cb9 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 10 Nov 2023 00:14:30 +0100 Subject: [PATCH 09/61] init structs --- contracts/optimism/DepositDataCodec.sol | 9 +++++---- contracts/optimism/L1ERC20TokenBridge.sol | 11 ++++++----- contracts/token/ERC20Rebasable.sol | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol index a7bc919a..91b8c574 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/optimism/DepositDataCodec.sol @@ -26,10 +26,11 @@ contract DepositDataCodec { revert ErrorDepositDataLength(); } - DepositData memory depositData; - depositData.rate = uint256(bytes32(buffer[0:32])); - depositData.time = uint256(bytes32(buffer[32:64])); - depositData.data = buffer[64:]; + DepositData memory depositData = DepositData({ + rate: uint256(bytes32(buffer[0:32])), + time: uint256(bytes32(buffer[32:64])), + data: buffer[64:] + }); return depositData; } diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 1353325f..e1aa72be 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -129,11 +129,12 @@ contract L1ERC20TokenBridge is uint32 l2Gas_, bytes calldata data_ ) internal { - - DepositData memory depositData; - depositData.rate = IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(); - depositData.time = block.timestamp; - depositData.data = data_; + + DepositData memory depositData = DepositData({ + rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), + time: block.timestamp, + data: data_ + }); bytes memory encodedDepositData = encodeDepositData(depositData); diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 0501dde8..d55c1542 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -75,7 +75,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } - function tokensPerStEth() external view returns (uint256) { + function tokensPerStEth() external pure returns (uint256) { return 0; } From ddb9ca342fb6342c6c2126a0e5670b88ce2a671b Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 11 Dec 2023 19:27:52 +0100 Subject: [PATCH 10/61] add comments --- contracts/optimism/L1ERC20TokenBridge.sol | 51 +++++++++++++++-------- contracts/optimism/L2ERC20TokenBridge.sol | 6 +-- contracts/stubs/TokensRateOracleStub.sol | 1 + contracts/token/ERC20Rebasable.sol | 7 +--- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index e1aa72be..7af0573e 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -18,7 +18,11 @@ import {DepositDataCodec} from "./DepositDataCodec.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; import "hardhat/console.sol"; -/// @author psirex +// Check if Optimism changed API for bridges. They could depricate methods. +// Optimise gas usage with data transfer. Maybe cache rate and see if it changed. + + +/// @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 @@ -51,6 +55,11 @@ contract L1ERC20TokenBridge is l2TokenBridge = l2TokenBridge_; } + function pushTokenRate(address to_, uint32 l2Gas_) external { + bytes memory empty = new bytes(0); + _depositERC20To(l1TokenRebasable, l2TokenRebasable, to_, 0, l2Gas_, empty); + } + /// @inheritdoc IL1ERC20Bridge function depositERC20( address l1Token_, @@ -127,30 +136,36 @@ contract L1ERC20TokenBridge is address to_, uint256 amount_, uint32 l2Gas_, - bytes calldata data_ + bytes memory data_ ) internal { - DepositData memory depositData = DepositData({ - rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), - time: block.timestamp, - data: data_ - }); - - bytes memory encodedDepositData = encodeDepositData(depositData); - - if (amount_ == 0) { - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); - return; - } - if (isRebasableTokenFlow(l1Token_, l2Token_)) { + console.log("isRebasableTokenFlow"); + DepositData memory depositData = DepositData({ + rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), // replace by stETHPerToken + time: block.timestamp, + data: data_ + }); + + bytes memory encodedDepositData = encodeDepositData(depositData); + + // probably need to add a new method for amount zero + if (amount_ == 0) { + _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + return; + } + + // maybe loosing 1 wei for stETH. Check another method IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); + // when 1 wei wasnt't transfer, can this wrap be failed? uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + console.log("isNonRebasableTokenFlow"); + + // IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); + _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l2Gas_, data_); } } @@ -182,7 +197,7 @@ contract L1ERC20TokenBridge is amount_, data_ ); - + console.logBytes(data_); sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); emit ERC20DepositInitiated( diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 68f2bceb..de6e467a 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -87,15 +87,13 @@ contract L2ERC20TokenBridge is bytes calldata data_ ) internal { if (l2Token_ == l2TokenRebasable) { - + // maybe loosing 1 wei her as well uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); _initiateWithdrawal(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, shares, l1Gas_, data_); - } else if (l2Token_ == l2TokenNonRebasable) { - IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); - _initiateWithdrawal(l2TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l1Gas_, data_); + _initiateWithdrawal(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l1Gas_, data_); } } diff --git a/contracts/stubs/TokensRateOracleStub.sol b/contracts/stubs/TokensRateOracleStub.sol index 4c43af26..65ed5642 100644 --- a/contracts/stubs/TokensRateOracleStub.sol +++ b/contracts/stubs/TokensRateOracleStub.sol @@ -46,6 +46,7 @@ contract TokensRateOracleStub is ITokensRateOracle { } function updateRate(int256 rate, uint256 updatedAt) external { + // check timestamp not late as current one. latestRoundDataAnswer = rate; latestRoundDataUpdatedAt = updatedAt; } diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index d55c1542..ab5658bc 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -79,10 +79,12 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return 0; } + // allow call only bridge function mintShares(address account_, uint256 amount_) external returns (uint256) { return _mintShares(account_, amount_); } + // allow call only bridge function burnShares(address account_, uint256 amount_) external { _burnShares(account_, amount_); } @@ -248,12 +250,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { } function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { - console.log("_getTokensByShares"); (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); - console.log("sharesAmount_=",sharesAmount_); - console.log("decimals=",decimals); - console.log("tokensRate=",tokensRate); - return (sharesAmount_ * (10 ** decimals)) / tokensRate; } From a73bcf4229715ab05c74b5deec299ae9fd31e787 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 11 Dec 2023 19:30:40 +0100 Subject: [PATCH 11/61] add gas test --- test/optimism/deposit-gas-estimation.test.ts | 272 +++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 test/optimism/deposit-gas-estimation.test.ts diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts new file mode 100644 index 00000000..25e7252a --- /dev/null +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -0,0 +1,272 @@ +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 { BigNumber } from "ethers"; + +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 { l1ERC20TokenBridge } = ctx; + const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l1ERC20TokenBridge + .connect(l1ERC20TokenBridgeAdmin) + .enableDeposits(); + } else { + console.log("L1 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l1ERC20TokenBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l1ERC20TokenBridge + .connect(l1ERC20TokenBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L1 withdrawals already enabled"); + } + + assert.isTrue(await l1ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l1ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("Activate bridging on L2", async (ctx) => { + const { l2ERC20TokenBridge } = ctx; + const { l2ERC20TokenBridgeAdmin } = ctx.accounts; + + const isDepositsEnabled = await l2ERC20TokenBridge.isDepositsEnabled(); + + if (!isDepositsEnabled) { + await l2ERC20TokenBridge + .connect(l2ERC20TokenBridgeAdmin) + .enableDeposits(); + } else { + console.log("L2 deposits already enabled"); + } + + const isWithdrawalsEnabled = + await l2ERC20TokenBridge.isWithdrawalsEnabled(); + + if (!isWithdrawalsEnabled) { + await l2ERC20TokenBridge + .connect(l2ERC20TokenBridgeAdmin) + .enableWithdrawals(); + } else { + console.log("L2 withdrawals already enabled"); + } + + assert.isTrue(await l2ERC20TokenBridge.isDepositsEnabled()); + assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); + }) + + .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { + const { + l1Token, + l2Token, + l1TokenRebasable, + l1ERC20TokenBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + tokensRateOracle, + l1Provider + } = ctx; + + const { accountA: tokenHolderA } = ctx.accounts; + const tokensPerStEth = await l1Token.tokensPerStEth(); + + await l1TokenRebasable + .connect(tokenHolderA.l1Signer) + .approve(l1ERC20TokenBridge.address, 0); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + + const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1ERC20TokenBridge.address + ); + + for(var x = 0; x< 2; ++x) { + const tx0 = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1Token.address, + l2Token.address, + 0, + 200_000, + "0x" + ); + + const receipt0 = await tx0.wait(); + + console.log("l1Token gasUsed=",receipt0.gasUsed); + } + + for(var x = 0; x< 2; ++x) { + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1TokenRebasable.address, + l2TokenRebasable.address, + 0, + 200_000, + "0x" + ); + + const receipt1 = await tx.wait(); + + console.log("l1TokenRebasable gasUsed=",receipt1.gasUsed); + } + //const gasDifference = receipt1.gasUsed.sub(receipt0.gasUsed); + + //console.log("gasUsed difference=", gasDifference); + + + // const blockNumber = await l1Provider.getBlockNumber(); + // const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; + // const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) + // const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) + // const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + + + // await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + // l1TokenRebasable.address, + // l2TokenRebasable.address, + // tokenHolderA.address, + // tokenHolderA.address, + // 0, + // dataToSend, + // ]); + + // const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + // "finalizeDeposit", + // [ + // l1TokenRebasable.address, + // l2TokenRebasable.address, + // tokenHolderA.address, + // tokenHolderA.address, + // 0, + // dataToSend, + // ] + // ); + + // const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + // await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + // l2ERC20TokenBridge.address, + // l1ERC20TokenBridge.address, + // l2DepositCalldata, + // messageNonce, + // 200_000, + // ]); + + // assert.equalBN( + // await l1Token.balanceOf(l1ERC20TokenBridge.address), + // l1ERC20TokenBridgeBalanceBefore + // ); + + // assert.equalBN( + // await l1TokenRebasable.balanceOf(tokenHolderA.address), + // tokenHolderABalanceBefore + // ); + }) + + + + .run(); + +async function ctxFactory() { + const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); + console.log("networkName=",networkName); + + const { + l1Provider, + l2Provider, + l1ERC20TokenBridgeAdmin, + l2ERC20TokenBridgeAdmin, + ...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 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 l1ERC20TokenBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l1Provider + ); + + await testing.setBalance( + await l2ERC20TokenBridgeAdmin.getAddress(), + wei.toBigNumber(wei`1 ether`), + l2Provider + ); + + 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), + l1ERC20TokenBridgeAdmin, + l2ERC20TokenBridgeAdmin, + l1CrossDomainMessengerAliased, + }, + common: { + depositAmount, + withdrawalAmount, + }, + snapshot: { + l1: l1Snapshot, + l2: l2Snapshot, + }, + }; +} From 6d37edce5b15c4ce88de98c51505ef771de4d9f4 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 12 Dec 2023 09:29:28 +0100 Subject: [PATCH 12/61] update gas test --- test/optimism/deposit-gas-estimation.test.ts | 94 ++++---------------- 1 file changed, 16 insertions(+), 78 deletions(-) diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts index 25e7252a..79ee4185 100644 --- a/test/optimism/deposit-gas-estimation.test.ts +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -4,8 +4,6 @@ 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 { BigNumber } from "ethers"; scenario("Optimism :: Bridging integration test", ctxFactory) .after(async (ctx) => { @@ -77,11 +75,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2Token, l1TokenRebasable, l1ERC20TokenBridge, - l2TokenRebasable, - l1CrossDomainMessenger, - l2ERC20TokenBridge, - tokensRateOracle, - l1Provider + l2TokenRebasable } = ctx; const { accountA: tokenHolderA } = ctx.accounts; @@ -99,25 +93,20 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge.address ); - for(var x = 0; x< 2; ++x) { - const tx0 = await l1ERC20TokenBridge - .connect(tokenHolderA.l1Signer) - .depositERC20( - l1Token.address, - l2Token.address, - 0, - 200_000, - "0x" - ); - - const receipt0 = await tx0.wait(); - - console.log("l1Token gasUsed=",receipt0.gasUsed); - } + const tx0 = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20( + l1Token.address, + l2Token.address, + 0, + 200_000, + "0x" + ); - for(var x = 0; x< 2; ++x) { + const receipt0 = await tx0.wait(); + console.log("l1Token gasUsed=",receipt0.gasUsed); - const tx = await l1ERC20TokenBridge + const tx1 = await l1ERC20TokenBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1TokenRebasable.address, @@ -127,62 +116,11 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - const receipt1 = await tx.wait(); - + const receipt1 = await tx1.wait(); console.log("l1TokenRebasable gasUsed=",receipt1.gasUsed); - } - //const gasDifference = receipt1.gasUsed.sub(receipt0.gasUsed); - - //console.log("gasUsed difference=", gasDifference); - - // const blockNumber = await l1Provider.getBlockNumber(); - // const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; - // const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - // const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - // const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); - - - // await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ - // l1TokenRebasable.address, - // l2TokenRebasable.address, - // tokenHolderA.address, - // tokenHolderA.address, - // 0, - // dataToSend, - // ]); - - // const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( - // "finalizeDeposit", - // [ - // l1TokenRebasable.address, - // l2TokenRebasable.address, - // tokenHolderA.address, - // tokenHolderA.address, - // 0, - // dataToSend, - // ] - // ); - - // const messageNonce = await l1CrossDomainMessenger.messageNonce(); - - // await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - // l2ERC20TokenBridge.address, - // l1ERC20TokenBridge.address, - // l2DepositCalldata, - // messageNonce, - // 200_000, - // ]); - - // assert.equalBN( - // await l1Token.balanceOf(l1ERC20TokenBridge.address), - // l1ERC20TokenBridgeBalanceBefore - // ); - - // assert.equalBN( - // await l1TokenRebasable.balanceOf(tokenHolderA.address), - // tokenHolderABalanceBefore - // ); + const gasDifference = receipt1.gasUsed.sub(receipt0.gasUsed); + console.log("gasUsed difference=", gasDifference); }) From 8098fe17dbc63a7e4603eaf9c01eabe62d3979c9 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Sat, 16 Dec 2023 13:34:34 +0100 Subject: [PATCH 13/61] fix tests --- contracts/BridgeableTokens.sol | 40 +++-------- .../arbitrum/InterchainERC20TokenGateway.sol | 18 ++--- contracts/arbitrum/L1ERC20TokenGateway.sol | 12 ++-- contracts/arbitrum/L2ERC20TokenGateway.sol | 22 +++--- .../optimism/BridgeableTokensOptimism.sol | 67 +++++++++++++++++++ contracts/optimism/L1ERC20TokenBridge.sol | 18 +++-- contracts/optimism/L2ERC20TokenBridge.sol | 15 ++--- test/optimism/L1ERC20TokenBridge.unit.test.ts | 8 ++- 8 files changed, 118 insertions(+), 82 deletions(-) create mode 100644 contracts/optimism/BridgeableTokensOptimism.sol diff --git a/contracts/BridgeableTokens.sol b/contracts/BridgeableTokens.sol index 6416d97d..52ec31af 100644 --- a/contracts/BridgeableTokens.sol +++ b/contracts/BridgeableTokens.sol @@ -6,32 +6,22 @@ pragma solidity 0.8.10; /// @author psirex /// @notice Contains the logic for validation of tokens used in the bridging process contract BridgeableTokens { - /// @notice Address of the bridged non rebasable token in the L1 chain - address public immutable l1TokenNonRebasable; + /// @notice Address of the bridged token in the L1 chain + address public immutable l1Token; - /// @notice Address of the bridged rebasable token in the L1 chain - address public immutable l1TokenRebasable; + /// @notice Address of the token minted on the L2 chain when token bridged + address public immutable l2Token; - /// @notice Address of the non rebasable token minted on the L2 chain when token bridged - address public immutable l2TokenNonRebasable; - - /// @notice Address of the rebasable token minted on the L2 chain when token bridged - address public immutable l2TokenRebasable; - - /// @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_) { - l1TokenNonRebasable = l1TokenNonRebasable_; - l1TokenRebasable = l1TokenRebasable_; - l2TokenNonRebasable = l2TokenNonRebasable_; - l2TokenRebasable = l2TokenRebasable_; + /// @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 l1Token_, address l2Token_) { + l1Token = l1Token_; + l2Token = l2Token_; } /// @dev Validates that passed l1Token_ is supported by the bridge modifier onlySupportedL1Token(address l1Token_) { - if (l1Token_ != l1TokenNonRebasable && l1Token_ != l1TokenRebasable) { + if (l1Token_ != l1Token) { revert ErrorUnsupportedL1Token(); } _; @@ -39,7 +29,7 @@ contract BridgeableTokens { /// @dev Validates that passed l2Token_ is supported by the bridge modifier onlySupportedL2Token(address l2Token_) { - if (l2Token_ != l2TokenNonRebasable && l2Token_ != l2TokenRebasable) { + if (l2Token_ != l2Token) { revert ErrorUnsupportedL2Token(); } _; @@ -53,14 +43,6 @@ contract BridgeableTokens { _; } - function isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { - return l1Token_ == l1TokenRebasable && l2Token_ == l2TokenRebasable; - } - - function isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { - return l1Token_ == l1TokenNonRebasable && l2Token_ == l2TokenNonRebasable; - } - error ErrorUnsupportedL1Token(); error ErrorUnsupportedL2Token(); error ErrorAccountIsZeroAddress(); diff --git a/contracts/arbitrum/InterchainERC20TokenGateway.sol b/contracts/arbitrum/InterchainERC20TokenGateway.sol index 78f9be68..329f4c87 100644 --- a/contracts/arbitrum/InterchainERC20TokenGateway.sol +++ b/contracts/arbitrum/InterchainERC20TokenGateway.sol @@ -24,18 +24,14 @@ abstract contract InterchainERC20TokenGateway is /// @param router_ Address of the router in the corresponding chain /// @param counterpartGateway_ Address of the counterpart gateway used in the bridging process - /// @param l1TokenNonRebasable Address of the bridged token in the Ethereum chain - /// @param l1TokenRebasable_ Address of the bridged token in the Ethereum chain - /// @param l2TokenNonRebasable_ Address of the token minted on the Arbitrum chain when token bridged - /// @param l2TokenRebasable_ Address of the token minted on the Arbitrum chain when token bridged + /// @param l1Token_ Address of the bridged token in the Ethereum chain + /// @param l2Token_ Address of the token minted on the Arbitrum chain when token bridged constructor( address router_, address counterpartGateway_, - address l1TokenNonRebasable, - address l1TokenRebasable_, - address l2TokenNonRebasable_, - address l2TokenRebasable_ - ) BridgeableTokens(l1TokenNonRebasable, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + address l1Token_, + address l2Token_ + ) BridgeableTokens(l1Token_, l2Token_) { router = router_; counterpartGateway = counterpartGateway_; } @@ -48,8 +44,8 @@ abstract contract InterchainERC20TokenGateway is view returns (address) { - if (l1Token_ == l1TokenRebasable) { - return l2TokenNonRebasable; + if (l1Token_ == l1Token) { + return l2Token; } return address(0); } diff --git a/contracts/arbitrum/L1ERC20TokenGateway.sol b/contracts/arbitrum/L1ERC20TokenGateway.sol index 85161aec..1be951aa 100644 --- a/contracts/arbitrum/L1ERC20TokenGateway.sol +++ b/contracts/arbitrum/L1ERC20TokenGateway.sol @@ -32,17 +32,13 @@ contract L1ERC20TokenGateway is address router_, address counterpartGateway_, address l1Token_, - address l1TokenRebasable_, - address l2Token_, - address l2TokenRebasable_ + address l2Token_ ) InterchainERC20TokenGateway( router_, counterpartGateway_, l1Token_, - l1TokenRebasable_, - l2Token_, - l2TokenRebasable_ + l2Token_ ) L1CrossDomainEnabled(inbox_) {} @@ -82,7 +78,7 @@ contract L1ERC20TokenGateway is }) ); - emit DepositInitiated(l1TokenNonRebasable, from, to_, retryableTicketId, amount_); + emit DepositInitiated(l1Token, from, to_, retryableTicketId, amount_); return abi.encode(retryableTicketId); } @@ -117,7 +113,7 @@ contract L1ERC20TokenGateway is sendCrossDomainMessage( from_, counterpartGateway, - getOutboundCalldata(l1TokenNonRebasable, from_, to_, amount_, ""), + getOutboundCalldata(l1Token, from_, to_, amount_, ""), messageOptions ); } diff --git a/contracts/arbitrum/L2ERC20TokenGateway.sol b/contracts/arbitrum/L2ERC20TokenGateway.sol index 0c6df5ac..5853d0ac 100644 --- a/contracts/arbitrum/L2ERC20TokenGateway.sol +++ b/contracts/arbitrum/L2ERC20TokenGateway.sol @@ -21,26 +21,20 @@ contract L2ERC20TokenGateway is /// @param arbSys_ Address of the Arbitrum’s ArbSys contract in the L2 chain /// @param router_ Address of the router in the L2 chain /// @param counterpartGateway_ Address of the counterpart L1 gateway - /// @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 Arbitrum chain when token bridged - /// @param l2TokenRebasable_ Address of the token minted on the Arbitrum chain when token bridged + /// @param l1Token_ Address of the bridged token in the L1 chain + /// @param l2Token_ Address of the token minted on the Arbitrum chain when token bridged constructor( address arbSys_, address router_, address counterpartGateway_, - address l1TokenNonRebasable_, - address l1TokenRebasable_, - address l2TokenNonRebasable_, - address l2TokenRebasable_ + address l1Token_, + address l2Token_ ) InterchainERC20TokenGateway( router_, counterpartGateway_, - l1TokenNonRebasable_, - l1TokenRebasable_, - l2TokenNonRebasable_, - l2TokenRebasable_ + l1Token_, + l2Token_ ) L2CrossDomainEnabled(arbSys_) {} @@ -61,7 +55,7 @@ contract L2ERC20TokenGateway is { address from = L2OutboundDataParser.decode(router, data_); - IERC20Bridged(l2TokenNonRebasable).bridgeBurn(from, amount_); + IERC20Bridged(l2Token).bridgeBurn(from, amount_); uint256 id = sendCrossDomainMessage( from, @@ -89,7 +83,7 @@ contract L2ERC20TokenGateway is onlySupportedL1Token(l1Token_) onlyFromCrossDomainAccount(counterpartGateway) { - IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); + IERC20Bridged(l2Token).bridgeMint(to_, amount_); emit DepositFinalized(l1Token_, from_, to_, amount_); } diff --git a/contracts/optimism/BridgeableTokensOptimism.sol b/contracts/optimism/BridgeableTokensOptimism.sol new file mode 100644 index 00000000..087c845d --- /dev/null +++ b/contracts/optimism/BridgeableTokensOptimism.sol @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author psirex +/// @notice Contains the logic for validation of tokens used in the bridging process +contract BridgeableTokensOptimism { + /// @notice Address of the bridged non rebasable token in the L1 chain + address public immutable l1TokenNonRebasable; + + /// @notice Address of the bridged rebasable token in the L1 chain + address public immutable l1TokenRebasable; + + /// @notice Address of the non rebasable token minted on the L2 chain when token bridged + address public immutable l2TokenNonRebasable; + + /// @notice Address of the rebasable token minted on the L2 chain when token bridged + address public immutable l2TokenRebasable; + + /// @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_) { + l1TokenNonRebasable = l1TokenNonRebasable_; + l1TokenRebasable = l1TokenRebasable_; + l2TokenNonRebasable = l2TokenNonRebasable_; + l2TokenRebasable = l2TokenRebasable_; + } + + /// @dev Validates that passed l1Token_ is supported by the bridge + modifier onlySupportedL1Token(address l1Token_) { + if (l1Token_ != l1TokenNonRebasable && l1Token_ != l1TokenRebasable) { + revert ErrorUnsupportedL1Token(); + } + _; + } + + /// @dev Validates that passed l2Token_ is supported by the bridge + modifier onlySupportedL2Token(address l2Token_) { + if (l2Token_ != l2TokenNonRebasable && l2Token_ != l2TokenRebasable) { + revert ErrorUnsupportedL2Token(); + } + _; + } + + /// @dev validates that account_ is not zero address + modifier onlyNonZeroAccount(address account_) { + if (account_ == address(0)) { + revert ErrorAccountIsZeroAddress(); + } + _; + } + + function isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + return l1Token_ == l1TokenRebasable && l2Token_ == l2TokenRebasable; + } + + function isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + return l1Token_ == l1TokenNonRebasable && l2Token_ == l2TokenNonRebasable; + } + + error ErrorUnsupportedL1Token(); + error ErrorUnsupportedL2Token(); + error ErrorAccountIsZeroAddress(); +} diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 7af0573e..67009c86 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -11,7 +11,7 @@ import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {BridgingManager} from "../BridgingManager.sol"; -import {BridgeableTokens} from "../BridgeableTokens.sol"; +import {BridgeableTokensOptimism} from "./BridgeableTokensOptimism.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; @@ -21,7 +21,6 @@ import "hardhat/console.sol"; // Check if Optimism changed API for bridges. They could depricate methods. // Optimise gas usage with data transfer. Maybe cache rate and see if it changed. - /// @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 @@ -29,7 +28,7 @@ import "hardhat/console.sol"; contract L1ERC20TokenBridge is IL1ERC20Bridge, BridgingManager, - BridgeableTokens, + BridgeableTokensOptimism, CrossDomainEnabled, DepositDataCodec { @@ -51,7 +50,7 @@ contract L1ERC20TokenBridge is address l1TokenRebasable_, address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l2TokenBridge = l2TokenBridge_; } @@ -138,9 +137,8 @@ contract L1ERC20TokenBridge is uint32 l2Gas_, bytes memory data_ ) internal { - if (isRebasableTokenFlow(l1Token_, l2Token_)) { - console.log("isRebasableTokenFlow"); + DepositData memory depositData = DepositData({ rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), // replace by stETHPerToken time: block.timestamp, @@ -158,13 +156,13 @@ contract L1ERC20TokenBridge is // maybe loosing 1 wei for stETH. Check another method IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); + // when 1 wei wasnt't transfer, can this wrap be failed? uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); - } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - console.log("isNonRebasableTokenFlow"); - // IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); + } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l2Gas_, data_); } } @@ -197,7 +195,7 @@ contract L1ERC20TokenBridge is amount_, data_ ); - console.logBytes(data_); + sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); emit ERC20DepositInitiated( diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index de6e467a..323e2969 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -13,7 +13,7 @@ import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {BridgingManager} from "../BridgingManager.sol"; -import {BridgeableTokens} from "../BridgeableTokens.sol"; +import {BridgeableTokensOptimism} from "./BridgeableTokensOptimism.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; @@ -28,7 +28,7 @@ import { console } from "hardhat/console.sol"; contract L2ERC20TokenBridge is IL2ERC20Bridge, BridgingManager, - BridgeableTokens, + BridgeableTokensOptimism, CrossDomainEnabled, DepositDataCodec { @@ -53,7 +53,7 @@ contract L2ERC20TokenBridge is address l2TokenNonRebasable_, address l2TokenRebasable_, address tokensRateOracle_ - ) CrossDomainEnabled(messenger_) BridgeableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l1TokenBridge = l1TokenBridge_; tokensRateOracle = tokensRateOracle_; } @@ -112,16 +112,15 @@ contract L2ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(l1TokenBridge) { - DepositData memory depositData = decodeDepositData(data_); - ITokensRateOracle(tokensRateOracle).updateRate(int256(depositData.rate), depositData.time); - if (isRebasableTokenFlow(l1Token_, l2Token_)) { + DepositData memory depositData = decodeDepositData(data_); + ITokensRateOracle(tokensRateOracle).updateRate(int256(depositData.rate), depositData.time); ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); + emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); } - - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } /// @notice Performs the logic for withdrawals by burning the token and informing diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts index 09aeefa2..774811c2 100644 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L1ERC20TokenBridge.unit.test.ts @@ -456,7 +456,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) .run(); async function ctxFactory() { - const [deployer, l2TokenBridgeEOA, stranger, recipient] = + const [deployer, l2TokenBridgeEOA, stranger, recipient, rebasableToken] = await hre.ethers.getSigners(); const l1MessengerStub = await new CrossDomainMessengerStub__factory( @@ -488,7 +488,9 @@ async function ctxFactory() { l1MessengerStub.address, l2TokenBridgeEOA.address, l1TokenStub.address, - l2TokenStub.address + rebasableToken.address, + l2TokenStub.address, + rebasableToken.address ); const l1TokenBridgeProxy = await new OssifiableProxy__factory( @@ -534,6 +536,8 @@ async function ctxFactory() { stubs: { l1Token: l1TokenStub, l2Token: l2TokenStub, + l1TokenRebasable: l1TokenStub, + l2TokenRebasable: l2TokenStub, l1Messenger: l1MessengerStub, }, l1TokenBridge, From 3335339b8fca4baa5920a48b39d74005737fb882 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 20 Dec 2023 16:33:21 +0100 Subject: [PATCH 14/61] add token rate oracle --- contracts/optimism/L2ERC20TokenBridge.sol | 13 +- contracts/optimism/TokenRateOracle.sol | 158 ++++++++++++++++++ ...OracleStub.sol => TokenRateOracleStub.sol} | 8 +- contracts/token/ERC20Rebasable.sol | 12 +- .../token/interfaces/ITokenRateOracle.sol | 30 ++++ .../token/interfaces/ITokensRateOracle.sol | 34 ---- .../optimism.integration.test.ts | 4 +- test/optimism/L2ERC20TokenBridge.unit.test.ts | 6 +- test/optimism/TokenRateOracle.unit.test.ts | 95 +++++++++++ test/optimism/deposit-gas-estimation.test.ts | 2 - test/token/ERC20Rebasable.unit.test.ts | 4 +- 11 files changed, 308 insertions(+), 58 deletions(-) create mode 100644 contracts/optimism/TokenRateOracle.sol rename contracts/stubs/{TokensRateOracleStub.sol => TokenRateOracleStub.sol} (83%) create mode 100644 contracts/token/interfaces/ITokenRateOracle.sol delete mode 100644 contracts/token/interfaces/ITokensRateOracle.sol create mode 100644 test/optimism/TokenRateOracle.unit.test.ts diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 323e2969..724c2db9 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -6,10 +6,10 @@ 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 {ITokensRateOracle} from "../token/interfaces/ITokensRateOracle.sol"; +import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {BridgingManager} from "../BridgingManager.sol"; @@ -37,8 +37,6 @@ contract L2ERC20TokenBridge is /// @inheritdoc IL2ERC20Bridge address public immutable l1TokenBridge; - address public immutable tokensRateOracle; - /// @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 @@ -51,11 +49,9 @@ contract L2ERC20TokenBridge is address l1TokenNonRebasable_, address l1TokenRebasable_, address l2TokenNonRebasable_, - address l2TokenRebasable_, - address tokensRateOracle_ + address l2TokenRebasable_ ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { l1TokenBridge = l1TokenBridge_; - tokensRateOracle = tokensRateOracle_; } /// @inheritdoc IL2ERC20Bridge @@ -114,7 +110,8 @@ contract L2ERC20TokenBridge is { if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); - ITokensRateOracle(tokensRateOracle).updateRate(int256(depositData.rate), depositData.time); + ITokenRateOracle tokensRateOracle = ERC20Rebasable(l2TokenRebasable).tokensRateOracle(); + tokensRateOracle.updateRate(int256(depositData.rate), depositData.time); ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol new file mode 100644 index 00000000..0f8cd320 --- /dev/null +++ b/contracts/optimism/TokenRateOracle.sol @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; +// import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; + +contract TokenRateOracle is ITokenRateOracle { + + /// Chain specification + uint256 private immutable slotsPerEpoch; + uint256 private immutable secondsPerSlot; + uint256 private immutable genesisTime; + uint256 private immutable initialEpoch; + uint256 private immutable epochsPerFrame; + + error InvalidChainConfig(); + error InitialEpochRefSlotCannotBeEarlierThanProcessingSlot(); + error InitialEpochIsYetToArrive(); + + int256 private tokenRate; + uint8 private decimalsInAnswer; + uint256 private rateL1Timestamp; + uint80 private answeredInRound; + + constructor( + uint256 slotsPerEpoch_, + uint256 secondsPerSlot_, + uint256 genesisTime_, + uint256 initialEpoch_, + uint256 epochsPerFrame_ + ) { + if (slotsPerEpoch_ == 0) revert InvalidChainConfig(); + if (secondsPerSlot_ == 0) revert InvalidChainConfig(); + + // Should I use toUint64(); + slotsPerEpoch = slotsPerEpoch_; + secondsPerSlot = secondsPerSlot_; + genesisTime = genesisTime_; + initialEpoch = initialEpoch_; + epochsPerFrame = epochsPerFrame_; + } + + /// @inheritdoc ITokenRateOracle + /// @return roundId_ is reference slot of HashConsensus + /// @return answer_ is wstETH/stETH token rate. + /// @return startedAt_ is HashConsensus frame start. + /// @return updatedAt_ is L2 timestamp of token rate update. + /// @return answeredInRound_ is the round ID of the round in which the answer was computed + function latestRoundData() external view returns ( + uint80 roundId_, + int256 answer_, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ + ) { + uint256 refSlot = _getRefSlot(initialEpoch, epochsPerFrame); + uint80 roundId = uint80(refSlot); + uint256 startedAt = _computeTimestampAtSlot(refSlot); + + return ( + roundId, + tokenRate, + startedAt, + rateL1Timestamp, + answeredInRound + ); + } + + /// @inheritdoc ITokenRateOracle + function latestAnswer() external view returns (int256) { + return tokenRate; + } + + /// @inheritdoc ITokenRateOracle + function decimals() external view returns (uint8) { + return decimalsInAnswer; + } + + /// @inheritdoc ITokenRateOracle + function updateRate(int256 rate, uint256 rateL1Timestamp_) external { + // check timestamp not late as current one. + if (rateL1Timestamp_ < _getTime()) { + return; + } + tokenRate = rate; + rateL1Timestamp = rateL1Timestamp_; + answeredInRound = 666; + decimalsInAnswer = 10; + } + + /// Frame utilities + + function _getTime() internal virtual view returns (uint256) { + return block.timestamp; // solhint-disable-line not-rely-on-time + } + + function _getRefSlot(uint256 initialEpoch_, uint256 epochsPerFrame_) internal view returns (uint256) { + return _getRefSlotAtTimestamp(_getTime(), initialEpoch_, epochsPerFrame_); + } + + function _getRefSlotAtTimestamp(uint256 timestamp_, uint256 initialEpoch_, uint256 epochsPerFrame_) + internal view returns (uint256) + { + return _getRefSlotAtIndex(_computeFrameIndex(timestamp_, initialEpoch_, epochsPerFrame_), initialEpoch_, epochsPerFrame_); + } + + function _getRefSlotAtIndex(uint256 frameIndex_, uint256 initialEpoch_, uint256 epochsPerFrame_) + internal view returns (uint256) + { + uint256 frameStartEpoch = _computeStartEpochOfFrameWithIndex(frameIndex_, initialEpoch_, epochsPerFrame_); + uint256 frameStartSlot = _computeStartSlotAtEpoch(frameStartEpoch); + return uint64(frameStartSlot - 1); + } + + function _computeStartSlotAtEpoch(uint256 epoch_) internal view returns (uint256) { + // See: github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch + return epoch_ * slotsPerEpoch; + } + + function _computeStartEpochOfFrameWithIndex(uint256 frameIndex_, uint256 initialEpoch_, uint256 epochsPerFrame_) + internal pure returns (uint256) + { + return initialEpoch_ + frameIndex_ * epochsPerFrame_; + } + + function _computeFrameIndex( + uint256 timestamp_, + uint256 initialEpoch_, + uint256 epochsPerFrame_ + ) internal view returns (uint256) + { + uint256 epoch = _computeEpochAtTimestamp(timestamp_); + if (epoch < initialEpoch_) { + revert InitialEpochIsYetToArrive(); + } + return (epoch - initialEpoch_) / epochsPerFrame_; + } + + function _computeEpochAtTimestamp(uint256 timestamp_) internal view returns (uint256) { + return _computeEpochAtSlot(_computeSlotAtTimestamp(timestamp_)); + } + + function _computeEpochAtSlot(uint256 slot_) internal view returns (uint256) { + // See: github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_epoch_at_slot + return slot_ / slotsPerEpoch; + } + + function _computeSlotAtTimestamp(uint256 timestamp_) internal view returns (uint256) { + return (timestamp_ - genesisTime) / secondsPerSlot; + } + + function _computeTimestampAtSlot(uint256 slot_) internal view returns (uint256) { + // See: github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#compute_timestamp_at_slot + return genesisTime + slot_ * secondsPerSlot; + } +} \ No newline at end of file diff --git a/contracts/stubs/TokensRateOracleStub.sol b/contracts/stubs/TokenRateOracleStub.sol similarity index 83% rename from contracts/stubs/TokensRateOracleStub.sol rename to contracts/stubs/TokenRateOracleStub.sol index 65ed5642..4be61315 100644 --- a/contracts/stubs/TokensRateOracleStub.sol +++ b/contracts/stubs/TokenRateOracleStub.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.10; -import {ITokensRateOracle} from "../token/interfaces/ITokensRateOracle.sol"; +import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; -contract TokensRateOracleStub is ITokensRateOracle { +contract TokenRateOracleStub is ITokenRateOracle { uint8 public _decimals; @@ -45,6 +45,10 @@ contract TokensRateOracleStub is ITokensRateOracle { return (0,latestRoundDataAnswer,0,latestRoundDataUpdatedAt,0); } + function latestAnswer() external view returns (int256) { + return latestRoundDataAnswer; + } + function updateRate(int256 rate, uint256 updatedAt) external { // check timestamp not late as current one. latestRoundDataAnswer = rate; diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index ab5658bc..c975689a 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; -import {ITokensRateOracle} from "./interfaces/ITokensRateOracle.sol"; +import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; import { console } from "hardhat/console.sol"; @@ -25,7 +25,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { error ErrorDecreasedAllowanceBelowZero(); IERC20 public immutable wrappedToken; - ITokensRateOracle public immutable tokensRateOracle; + ITokenRateOracle public immutable tokensRateOracle; /// @param wrappedToken_ address of the ERC20 token to wrap /// @param tokensRateOracle_ address of oracle that returns tokens rate @@ -33,14 +33,14 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { /// @param symbol_ The symbol of the token /// @param decimals_ The decimals places of the token constructor( - IERC20 wrappedToken_, - ITokensRateOracle tokensRateOracle_, + address wrappedToken_, + address tokensRateOracle_, string memory name_, string memory symbol_, uint8 decimals_ ) ERC20Metadata(name_, symbol_, decimals_) { - wrappedToken = wrappedToken_; - tokensRateOracle = tokensRateOracle_; + wrappedToken = IERC20(wrappedToken_); + tokensRateOracle = ITokenRateOracle(tokensRateOracle_); } /// @notice Sets the name and the symbol of the tokens if they both are empty diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/token/interfaces/ITokenRateOracle.sol new file mode 100644 index 00000000..648b346d --- /dev/null +++ b/contracts/token/interfaces/ITokenRateOracle.sol @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice Oracle interface for two tokens rate. A subset of Chainlink data feed interface. +interface ITokenRateOracle { + + /// @notice get data about the latest round. + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + /// @notice get answer about the latest round. + function latestAnswer() external view returns (int256); + + /// @notice represents the number of decimals the oracle responses represent. + function decimals() external view returns (uint8); + + /// @notice Updates token rate. + function updateRate(int256 rate, uint256 rateL1Timestamp) external; +} \ No newline at end of file diff --git a/contracts/token/interfaces/ITokensRateOracle.sol b/contracts/token/interfaces/ITokensRateOracle.sol deleted file mode 100644 index c28cabc9..00000000 --- a/contracts/token/interfaces/ITokensRateOracle.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @notice Oracle interface for two tokens rate -interface ITokensRateOracle { - - function updateRate(int256 rate, uint256 updatedAt) external; - - /** - * @notice represents the number of decimals the oracle responses represent. - */ - function decimals() external view returns (uint8); - - /** - * @notice get data about the latest round. - */ - function latestRoundData() - external - view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ); -} - -interface ITokensRateOracleUpdatable { - function updateRate(int256 rate, uint256 updatedAt) external; -} \ No newline at end of file diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index de7f5902..2a8949f4 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -6,7 +6,7 @@ import { OptimismBridgeExecutor__factory, ERC20Bridged__factory, ERC20Rebasable__factory, - TokensRateOracleStub__factory, + TokenRateOracle__factory, } from "../../typechain"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; @@ -219,7 +219,7 @@ async function ctxFactory() { "TTR" ); - const tokensRateOracleStub = await new TokensRateOracleStub__factory(l2Deployer).deploy(); + const tokensRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy(); const optAddresses = optimism.addresses(networkName); diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index 97e40afa..9c5eae8f 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -375,7 +375,7 @@ unit("Optimism:: L2ERC20TokenBridge", ctxFactory) .run(); async function ctxFactory() { - const [deployer, stranger, recipient, l1TokenBridgeEOA] = + const [deployer, stranger, recipient, l1TokenBridgeEOA, token2] = await hre.ethers.getSigners(); const l2Messenger = await new CrossDomainMessengerStub__factory( @@ -405,7 +405,9 @@ async function ctxFactory() { l2Messenger.address, l1TokenBridgeEOA.address, l1Token.address, - l2Token.address + token2.address, + l2Token.address, + token2.address ); const l2TokenBridgeProxy = await new OssifiableProxy__factory( diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts new file mode 100644 index 00000000..3858ed2e --- /dev/null +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -0,0 +1,95 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { unit } from "../../utils/testing"; +import { TokenRateOracle__factory } from "../../typechain"; +import { ethers } from "ethers"; + +unit("TokenRateOracle", ctxFactory) + + .test("init zero slotsPerEpoch", async (ctx) => { + const [deployer] = await hre.ethers.getSigners(); + await assert.revertsWith(new TokenRateOracle__factory(deployer).deploy( + 0, + 10, + 1000, + 100, + 50 + ), "InvalidChainConfig()"); + }) + + .test("init zero secondsPerSlot", async (ctx) => { + const [deployer] = await hre.ethers.getSigners(); + await assert.revertsWith(new TokenRateOracle__factory(deployer).deploy( + 41, + 0, + 1000, + 100, + 50 + ), "InvalidChainConfig()"); + }) + + .test("state after init", async (ctx) => { + const { tokensRateOracle } = ctx.contracts; + + assert.equalBN(await tokensRateOracle.latestAnswer(), 0); + + const { + roundId_, + answer_, + startedAt_, + updatedAt_, + answeredInRound_ + } = await tokensRateOracle.latestRoundData(); + + assert.equalBN(roundId_, 170307199); + assert.equalBN(answer_, 0); + assert.equalBN(startedAt_, 1703072990); + assert.equalBN(updatedAt_, 0); + assert.equalBN(answeredInRound_, 0); + + assert.equalBN(await tokensRateOracle.decimals(), 0); + }) + + .test("state after update token rate", async (ctx) => { + const { tokensRateOracle } = ctx.contracts; + + await tokensRateOracle.updateRate(2, ethers.constants.MaxInt256 ); + + assert.equalBN(await tokensRateOracle.latestAnswer(), 2); + + const { + roundId_, + answer_, + startedAt_, + updatedAt_, + answeredInRound_ + } = await tokensRateOracle.latestRoundData(); + + assert.equalBN(roundId_, 170307199); + assert.equalBN(answer_, 2); + assert.equalBN(startedAt_, 1703072990); + assert.equalBN(updatedAt_, ethers.constants.MaxInt256); + assert.equalBN(answeredInRound_, 666); + + assert.equalBN(await tokensRateOracle.decimals(), 10); + }) + + .run(); + +async function ctxFactory() { + + const [deployer] = await hre.ethers.getSigners(); + + const tokensRateOracle = await new TokenRateOracle__factory(deployer).deploy( + 32, + 10, + 1000, + 100, + 50 + ); + + return { + accounts: { deployer }, + contracts: { tokensRateOracle } + }; +} diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts index 79ee4185..c6fc8db7 100644 --- a/test/optimism/deposit-gas-estimation.test.ts +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -142,8 +142,6 @@ async function ctxFactory() { 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); diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 849155a7..2d49abc6 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -3,7 +3,7 @@ import { assert } from "chai"; import { unit } from "../../utils/testing"; import { wei } from "../../utils/wei"; -import { ERC20Stub__factory, ERC20Rebasable__factory, TokensRateOracleStub__factory, OssifiableProxy__factory } from "../../typechain"; +import { ERC20Stub__factory, ERC20Rebasable__factory, TokenRateOracleStub__factory, OssifiableProxy__factory } from "../../typechain"; import { BigNumber } from "ethers"; @@ -241,7 +241,7 @@ async function ctxFactory() { const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokensRateOracleStub = await new TokensRateOracleStub__factory(deployer).deploy(); + const tokensRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( wrappedTokenStub.address, From 86037dcebc4b61ddf579ea32c76851b1a0e61b7f Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Sun, 24 Dec 2023 16:44:33 +0100 Subject: [PATCH 15/61] add tests for new token, renaming --- contracts/optimism/L1ERC20TokenBridge.sol | 4 +- contracts/optimism/L2ERC20TokenBridge.sol | 4 +- contracts/stubs/ERC20WrapableStub.sol | 2 +- contracts/stubs/TokenRateOracleStub.sol | 6 +- contracts/token/ERC20Rebasable.sol | 99 +- contracts/token/interfaces/IERC20Wrapable.sol | 2 +- .../token/interfaces/ITokenRateOracle.sol | 2 +- .../optimism.integration.test.ts | 4 +- .../bridging-rebase.integration.test.ts | 46 +- test/optimism/deposit-gas-estimation.test.ts | 2 +- test/token/ERC20Rebasable.unit.test.ts | 931 +++++++++++++++--- utils/optimism/deployment.ts | 5 +- utils/optimism/testing.ts | 16 +- 13 files changed, 921 insertions(+), 202 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 67009c86..1c39797e 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -71,7 +71,7 @@ contract L1ERC20TokenBridge is whenDepositsEnabled onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) - { + { if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } @@ -140,7 +140,7 @@ contract L1ERC20TokenBridge is if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ - rate: IERC20Wrapable(l1TokenNonRebasable).tokensPerStEth(), // replace by stETHPerToken + rate: IERC20Wrapable(l1TokenNonRebasable).stETHPerToken(), time: block.timestamp, data: data_ }); diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 724c2db9..36ae5c0b 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -110,8 +110,8 @@ contract L2ERC20TokenBridge is { if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); - ITokenRateOracle tokensRateOracle = ERC20Rebasable(l2TokenRebasable).tokensRateOracle(); - tokensRateOracle.updateRate(int256(depositData.rate), depositData.time); + ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2TokenRebasable).tokenRateOracle(); + tokenRateOracle.updateRate(int256(depositData.rate), depositData.time, 0); ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index f3c7f33a..9f48b24f 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -47,7 +47,7 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { return stETHAmount; } - function tokensPerStEth() external view returns (uint256) { + function stETHPerToken() external view returns (uint256) { return tokensRate; } } diff --git a/contracts/stubs/TokenRateOracleStub.sol b/contracts/stubs/TokenRateOracleStub.sol index 4be61315..40463af7 100644 --- a/contracts/stubs/TokenRateOracleStub.sol +++ b/contracts/stubs/TokenRateOracleStub.sol @@ -49,9 +49,9 @@ contract TokenRateOracleStub is ITokenRateOracle { return latestRoundDataAnswer; } - function updateRate(int256 rate, uint256 updatedAt) external { + function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external { // check timestamp not late as current one. - latestRoundDataAnswer = rate; - latestRoundDataUpdatedAt = updatedAt; + latestRoundDataAnswer = tokenRate_; + latestRoundDataUpdatedAt = rateL1Timestamp_; } } \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index c975689a..b7a32798 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -7,7 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; -import { console } from "hardhat/console.sol"; +// import { console } from "hardhat/console.sol"; /// @author kovalgek /// @notice Extends the ERC20Shared functionality @@ -23,24 +23,43 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { error ErrorNotEnoughAllowance(); error ErrorAccountIsZeroAddress(); error ErrorDecreasedAllowanceBelowZero(); + error ErrorNotBridge(); + /// @notice Bridge which can mint and burn tokens on L2. + address public immutable bridge; + + /// @notice Contract of non-rebasable token to wrap. IERC20 public immutable wrappedToken; - ITokenRateOracle public immutable tokensRateOracle; - /// @param wrappedToken_ address of the ERC20 token to wrap - /// @param tokensRateOracle_ address of oracle that returns tokens rate + /// @notice Oracle contract used to get token rate for wrapping/unwrapping tokens. + ITokenRateOracle public immutable tokenRateOracle; + + /// @inheritdoc IERC20 + mapping(address => mapping(address => uint256)) public allowance; + + /// @notice Basic unit representing the token holder's share in the total amount of ether controlled by the protocol. + mapping (address => uint256) private shares; + + /// @notice The total amount of shares in existence. + uint256 private totalShares; + /// @param name_ The name of the token /// @param symbol_ The symbol of the token /// @param decimals_ The decimals places of the token + /// @param wrappedToken_ 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( - address wrappedToken_, - address tokensRateOracle_, string memory name_, string memory symbol_, - uint8 decimals_ + uint8 decimals_, + address wrappedToken_, + address tokenRateOracle_, + address bridge_ ) ERC20Metadata(name_, symbol_, decimals_) { wrappedToken = IERC20(wrappedToken_); - tokensRateOracle = ITokenRateOracle(tokensRateOracle_); + tokenRateOracle = ITokenRateOracle(tokenRateOracle_); + bridge = bridge_; } /// @notice Sets the name and the symbol of the tokens if they both are empty @@ -51,8 +70,6 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { _setERC20MetadataSymbol(symbol_); } - /// ------------IERC20Wrapable------------ - /// @inheritdoc IERC20Wrapable function wrap(uint256 sharesAmount_) external returns (uint256) { if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); @@ -75,25 +92,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return sharesAmount; } - function tokensPerStEth() external pure returns (uint256) { - return 0; + /// @inheritdoc IERC20Wrapable + function stETHPerToken() external view returns (uint256) { + return uint256(tokenRateOracle.latestAnswer()); } // allow call only bridge - function mintShares(address account_, uint256 amount_) external returns (uint256) { + function mintShares(address account_, uint256 amount_) external onlyBridge returns (uint256) { return _mintShares(account_, amount_); } // allow call only bridge - function burnShares(address account_, uint256 amount_) external { + function burnShares(address account_, uint256 amount_) external onlyBridge { _burnShares(account_, amount_); } - - /// ------------ERC20------------ - - /// @inheritdoc IERC20 - mapping(address => mapping(address => uint256)) public allowance; + /// @dev Validates that sender of the transaction is the bridge + modifier onlyBridge() { + if (msg.sender != bridge) { + revert ErrorNotBridge(); + } + _; + } /// @inheritdoc IERC20 function totalSupply() external view returns (uint256) { @@ -212,35 +232,32 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { emit Approval(owner_, spender_, amount_); } - - /// ------------Shares------------ - // API - function sharesOf(address _account) external view returns (uint256) { - return _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_); } - function getTokensRateAndDecimal() external view returns (uint256, uint256) { - return _getTokensRateAndDecimal(); - } - - // private/internal - - mapping (address => uint256) private shares; - - uint256 private totalShares; - function _sharesOf(address account_) internal view returns (uint256) { return shares[account_]; } @@ -251,32 +268,28 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); - return (sharesAmount_ * (10 ** decimals)) / tokensRate; + return (sharesAmount_ * tokensRate) / (10 ** decimals); } function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) { (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); - return (tokenAmount_ * tokensRate) / (10 ** decimals); + return (tokenAmount_ * (10 ** decimals)) / tokensRate; } function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { - uint8 rateDecimals = tokensRateOracle.decimals(); - console.log("_getTokensRateAndDecimal1"); + uint8 rateDecimals = tokenRateOracle.decimals(); if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); - console.log("_getTokensRateAndDecimal2"); (, int256 answer , , uint256 updatedAt - ,) = tokensRateOracle.latestRoundData(); - console.log("_getTokensRateAndDecimal3"); + ,) = tokenRateOracle.latestRoundData(); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); if (answer <= 0) revert ErrorOracleAnswerIsNegative(); - console.log("_getTokensRateAndDecimal4"); return (uint256(answer), uint256(rateDecimals)); } @@ -290,6 +303,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { ) internal onlyNonZeroAccount(recipient_) returns (uint256) { totalShares = totalShares + amount_; shares[recipient_] = shares[recipient_] + amount_; + emit Transfer(address(0), recipient_, amount_); return totalShares; } @@ -304,6 +318,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { if (accountShares < amount_) revert ErrorNotEnoughBalance(); totalShares = totalShares - amount_; shares[account_] = accountShares - amount_; + emit Transfer(account_, address(0), amount_); return totalShares; } diff --git a/contracts/token/interfaces/IERC20Wrapable.sol b/contracts/token/interfaces/IERC20Wrapable.sol index de82ee27..0fb2d2dc 100644 --- a/contracts/token/interfaces/IERC20Wrapable.sol +++ b/contracts/token/interfaces/IERC20Wrapable.sol @@ -34,5 +34,5 @@ interface IERC20Wrapable { * @notice Get amount of wstETH for a one stETH * @return Amount of wstETH for a 1 stETH */ - function tokensPerStEth() external view returns (uint256); + function stETHPerToken() external view returns (uint256); } \ No newline at end of file diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/token/interfaces/ITokenRateOracle.sol index 648b346d..eb9aa8aa 100644 --- a/contracts/token/interfaces/ITokenRateOracle.sol +++ b/contracts/token/interfaces/ITokenRateOracle.sol @@ -26,5 +26,5 @@ interface ITokenRateOracle { function decimals() external view returns (uint8); /// @notice Updates token rate. - function updateRate(int256 rate, uint256 rateL1Timestamp) external; + function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external; } \ No newline at end of file diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 2a8949f4..6cd18db8 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -219,7 +219,7 @@ async function ctxFactory() { "TTR" ); - const tokensRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy(); const optAddresses = optimism.addresses(networkName); @@ -243,7 +243,7 @@ async function ctxFactory() { .erc20TokenBridgeDeployScript( l1Token.address, l1TokenRebasable.address, - tokensRateOracleStub.address, + tokenRateOracleStub.address, { deployer: l1Deployer, admins: { diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index e0822643..cda397c2 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -79,12 +79,12 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -111,8 +111,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockNumber = await l1Provider.getBlockNumber(); const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToSend = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ @@ -165,16 +165,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l2Provider } = ctx; - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); const blockNumber = await l2Provider.getBlockNumber(); const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToReceive = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = ctx.accounts; @@ -206,8 +206,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); - const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); - assert.equalBN(tokensPerStEth, tokensRate); + const [,tokensRate,,updatedAt,] = await tokenRateOracle.latestRoundData(); + assert.equalBN(stETHPerToken, tokensRate); assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ @@ -237,15 +237,15 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); - await tokensRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); + await tokenRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -272,8 +272,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const blockNumber = await l1Provider.getBlockNumber(); const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToSend = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToSend = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ @@ -326,19 +326,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, - tokensRateOracle, + tokenRateOracle, l2Provider } = ctx; const { depositAmount: depositAmountInRebasableTokens } = ctx.common; - const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); - const tokensPerStEth = await l1Token.tokensPerStEth(); + const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).div(2); + const stETHPerToken = await l1Token.stETHPerToken(); const blockNumber = await l2Provider.getBlockNumber(); const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const tokensPerStEthStr = ethers.utils.hexZeroPad(tokensPerStEth.toHexString(), 32) - const dataToReceive = ethers.utils.hexConcat([tokensPerStEthStr, blockTimestampStr]); + const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) + const dataToReceive = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = @@ -371,8 +371,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); - const [,tokensRate,,updatedAt,] = await tokensRateOracle.latestRoundData(); - assert.equalBN(tokensPerStEth, tokensRate); + const [,tokensRate,,updatedAt,] = await tokenRateOracle.latestRoundData(); + assert.equalBN(stETHPerToken, tokensRate); assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ @@ -403,7 +403,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2ERC20TokenBridge } = ctx; - const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).mul(2); + const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).div(2); const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( tokenHolderA.address diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts index c6fc8db7..5a14cda6 100644 --- a/test/optimism/deposit-gas-estimation.test.ts +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -79,7 +79,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const tokensPerStEth = await l1Token.tokensPerStEth(); + const stETHPerToken = await l1Token.stETHPerToken(); await l1TokenRebasable .connect(tokenHolderA.l1Signer) diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 2d49abc6..ac87c9d5 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -6,7 +6,6 @@ import { wei } from "../../utils/wei"; import { ERC20Stub__factory, ERC20Rebasable__factory, TokenRateOracleStub__factory, OssifiableProxy__factory } from "../../typechain"; import { BigNumber } from "ethers"; - unit("ERC20Rebasable", ctxFactory) .test("wrappedToken", async (ctx) => { @@ -14,9 +13,9 @@ unit("ERC20Rebasable", ctxFactory) assert.equal(await rebasableProxied.wrappedToken(), wrappedTokenStub.address) }) - .test("tokensRateOracle", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; - assert.equal(await rebasableProxied.tokensRateOracle(), tokensRateOracleStub.address) + .test("tokenRateOracle", async (ctx) => { + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + assert.equal(await rebasableProxied.tokenRateOracle(), tokenRateOracleStub.address) }) .test("name()", async (ctx) => @@ -27,235 +26,935 @@ unit("ERC20Rebasable", ctxFactory) assert.equal(await ctx.contracts.rebasableProxied.symbol(), ctx.constants.symbol) ) + .test("initialize() :: name already set", async (ctx) => { + const { deployer, owner } = ctx.accounts; + + // deploy new implementation + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + "name", + "", + 10, + wrappedTokenStub.address, + tokenRateOracleStub.address, + owner.address + ); + await assert.revertsWith( + rebasableTokenImpl.initialize("New Name", ""), + "ErrorNameAlreadySet()" + ); + }) + + .test("initialize() :: symbol already set", async (ctx) => { + const { deployer, owner } = ctx.accounts; + + // deploy new implementation + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + "", + "symbol", + 10, + wrappedTokenStub.address, + tokenRateOracleStub.address, + owner.address + ); + await assert.revertsWith( + rebasableTokenImpl.initialize("", "New Symbol"), + "ErrorSymbolAlreadySet()" + ); + }) + .test("decimals", async (ctx) => - assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimals) + assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimalsToSet) ) + .test("totalShares", async (ctx) => { + const { premintShares } = ctx.constants; + assert.equalBN(await ctx.contracts.rebasableProxied.getTotalShares(), premintShares); + }) + .test("wrap(0)", async (ctx) => { const { rebasableProxied } = ctx.contracts; - await assert.revertsWith(rebasableProxied.wrap(0), "ErrorZeroSharesWrap()"); + const { user1 } = ctx.accounts; + await assert.revertsWith(rebasableProxied.connect(user1).wrap(0), "ErrorZeroSharesWrap()"); }) .test("unwrap(0)", async (ctx) => { const { rebasableProxied } = ctx.contracts; - await assert.revertsWith(rebasableProxied.unwrap(0), "ErrorZeroTokensUnwrap()"); + const { user1 } = ctx.accounts; + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(0), "ErrorZeroTokensUnwrap()"); }) .test("wrap() positive scenario", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub, wrappedTokenStub } = ctx.contracts; + + const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; + const { rate, decimals, premintShares } = ctx.constants; + + const totalSupply = rate.mul(premintShares).div(decimals); - await tokensRateOracleStub.setDecimals(5); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); // user1 - assert.equalBN(await rebasableProxied.callStatic.wrap(100), 83); - const tx = await rebasableProxied.wrap(100); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); - assert.equalBN(await rebasableProxied.getTotalShares(), 100); - assert.equalBN(await rebasableProxied.sharesOf(user1.address), 100); + const user1Shares = wei`100 ether`; + const user1Tokens = rate.mul(user1Shares).div(decimals); + + assert.equalBN(await rebasableProxied.connect(user1).callStatic.wrap(user1Shares), user1Tokens); + const tx = await rebasableProxied.connect(user1).wrap(user1Shares); + + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); assert.equal(await wrappedTokenStub.transferFromAddress(), user1.address); assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); - assert.equalBN(await wrappedTokenStub.transferFromAmount(), 100); + assert.equalBN(await wrappedTokenStub.transferFromAmount(), 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.connect(user2).callStatic.wrap(50), 41); - const tx2 = await rebasableProxied.connect(user2).wrap(50); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - assert.equalBN(await rebasableProxied.getTotalShares(), 150); - assert.equalBN(await rebasableProxied.sharesOf(user2.address), 50); + const user2Shares = wei`50 ether`; + const user2Tokens = rate.mul(user2Shares).div(decimals); + assert.equalBN(await rebasableProxied.connect(user2).callStatic.wrap(user2Shares), user2Tokens); + const tx2 = await rebasableProxied.connect(user2).wrap(user2Shares); + + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); assert.equal(await wrappedTokenStub.transferFromAddress(), user2.address); assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); - assert.equalBN(await wrappedTokenStub.transferFromAmount(), 50); + assert.equalBN(await wrappedTokenStub.transferFromAmount(), user2Shares); // common state changes - assert.equalBN(await rebasableProxied.totalSupply(), 125); + assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares).add(user2Shares)); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); }) .test("wrap() with wrong oracle decimals", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; - - await tokensRateOracleStub.setDecimals(0); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.wrap(23), "ErrorInvalidRateDecimals(0)"); + await tokenRateOracleStub.setDecimals(0); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorInvalidRateDecimals(0)"); - await tokensRateOracleStub.setDecimals(19); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); - - await assert.revertsWith(rebasableProxied.wrap(23), "ErrorInvalidRateDecimals(19)"); + await tokenRateOracleStub.setDecimals(19); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorInvalidRateDecimals(19)"); }) .test("wrap() with wrong oracle update time", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; - - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(0); + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.wrap(5), "ErrorWrongOracleUpdateTime()"); + await tokenRateOracleStub.setUpdatedAt(0); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(5), "ErrorWrongOracleUpdateTime()"); }) .test("wrap() with wrong oracle answer", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(0); - await tokensRateOracleStub.setUpdatedAt(10); - - await assert.revertsWith(rebasableProxied.wrap(21), "ErrorOracleAnswerIsNegative()"); + await tokenRateOracleStub.setLatestRoundDataAnswer(0); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNegative()"); }) - .test("unwrap() positive scenario", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub, wrappedTokenStub } = ctx.contracts; + const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; + const { rate, decimals, premintShares } = ctx.constants; - await tokensRateOracleStub.setDecimals(7); - await tokensRateOracleStub.setLatestRoundDataAnswer(14000000); - await tokensRateOracleStub.setUpdatedAt(14000); + const totalSupply = BigNumber.from(rate).mul(premintShares).div(decimals); + + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); // user1 - const tx0 = await rebasableProxied.wrap(4500); + 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); - assert.equalBN(await rebasableProxied.callStatic.unwrap(59), 82); - const tx = await rebasableProxied.unwrap(59); + const user1Shares = BigNumber.from(user1SharesToWrap).sub(user1SharesToUnwrap); + const user1Tokens = BigNumber.from(rate).mul(user1Shares).div(decimals); - assert.equalBN(await rebasableProxied.getTotalShares(), 4418); - assert.equalBN(await rebasableProxied.sharesOf(user1.address), 4418); + 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.equal(await wrappedTokenStub.transferTo(), user1.address); - assert.equalBN(await wrappedTokenStub.transferAmount(), 82); + assert.equalBN(await wrappedTokenStub.transferAmount(), user1SharesToUnwrap); - // // user2 - await rebasableProxied.connect(user2).wrap(200); + // 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); - assert.equalBN(await rebasableProxied.connect(user2).callStatic.unwrap(50), 70); - const tx2 = await rebasableProxied.connect(user2).unwrap(50); + const user2Shares = BigNumber.from(user2SharesToWrap).sub(user2SharesToUnwrap); + const user2Tokens = BigNumber.from(rate).mul(user2Shares).div(decimals); - assert.equalBN(await rebasableProxied.getTotalShares(), 4548); - assert.equalBN(await rebasableProxied.sharesOf(user2.address), 130); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); + 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.equal(await wrappedTokenStub.transferTo(), user2.address); - assert.equalBN(await wrappedTokenStub.transferAmount(), 70); + assert.equalBN(await wrappedTokenStub.transferAmount(), user2SharesToUnwrap); // common state changes - assert.equalBN(await rebasableProxied.totalSupply(), 3248); + 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 decimals", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + await rebasableProxied.connect(user1).wrap(wei`2 ether`); + + await tokenRateOracleStub.setDecimals(0); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorInvalidRateDecimals(0)"); + + await tokenRateOracleStub.setDecimals(19); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorInvalidRateDecimals(19)"); + }) + + .test("unwrap() with wrong oracle update time", async (ctx) => { - await rebasableProxied.wrap(100); - await tokensRateOracleStub.setDecimals(0); + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.unwrap(23), "ErrorInvalidRateDecimals(0)"); + await rebasableProxied.connect(user1).wrap(wei`6 ether`); + await tokenRateOracleStub.setUpdatedAt(0); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`1 ether`), "ErrorWrongOracleUpdateTime()"); + }) - await tokensRateOracleStub.setDecimals(19); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(1000); + .test("unwrap() when no balance", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.unwrap(23), "ErrorInvalidRateDecimals(19)"); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`4 ether`), "ErrorNotEnoughBalance()"); }) - .test("unwrap() with wrong oracle update time", async (ctx) => { + .test("mintShares() positive scenario", 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`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); + + assert.equalBN(await rebasableProxied.connect(owner).callStatic.mintShares(user1.address, user1SharesToMint), premintShares.add(user1SharesToMint)); + const tx0 = await rebasableProxied.connect(owner).mintShares(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); - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - await tokensRateOracleStub.setDecimals(10); - await tokensRateOracleStub.setLatestRoundDataAnswer(120000); - await tokensRateOracleStub.setUpdatedAt(300); + assert.equalBN( + await rebasableProxied.connect(owner).callStatic.mintShares(user2.address, user2SharesToMint), + premintShares.add(user1SharesToMint).add(user2SharesToMint) + ); + const tx1 = await rebasableProxied.connect(owner).mintShares(user2.address, user2SharesToMint); - await rebasableProxied.wrap(100); - await tokensRateOracleStub.setUpdatedAt(0); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); - await assert.revertsWith(rebasableProxied.unwrap(5), "ErrorWrongOracleUpdateTime()"); + // common state changes + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1SharesToMint).add(user2SharesToMint)); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted).add(user2TokensMinted)); }) - .test("unwrap() when no balance", async (ctx) => { - const { rebasableProxied, tokensRateOracleStub } = ctx.contracts; + .test("burnShares() positive scenario", async (ctx) => { + + const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const {user1, user2, owner } = ctx.accounts; + const { rate, decimals, premintShares, premintTokens } = ctx.constants; - await tokensRateOracleStub.setDecimals(8); - await tokensRateOracleStub.setLatestRoundDataAnswer(12000000); - await tokensRateOracleStub.setUpdatedAt(1000); + assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); + assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); - await assert.revertsWith(rebasableProxied.unwrap(10), "ErrorNotEnoughBalance()"); + // 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).mintShares(user1.address, user1SharesToMint); + assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1TokensMinted); + + await rebasableProxied.connect(owner).burnShares(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).mintShares(user2.address, user2SharesToMint); + assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); + assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); + await rebasableProxied.connect(owner).burnShares(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()", async (ctx) => { const { rebasableProxied } = ctx.contracts; - const { user1, user2 } = ctx.accounts; + const { holder, spender } = ctx.accounts; // validate initially allowance is zero assert.equalBN( - await rebasableProxied.allowance(user1.address, user2.address), + await rebasableProxied.allowance(holder.address, spender.address), "0" ); - const amount = 3; + const amount = wei`1 ether`; // validate return value of the method assert.isTrue( - await rebasableProxied.callStatic.approve(user2.address, amount) + await rebasableProxied.callStatic.approve(spender.address, amount) ); // approve tokens - const tx = await rebasableProxied.approve(user2.address, amount); + const tx = await rebasableProxied.approve(spender.address, amount); // validate Approval event was emitted await assert.emits(rebasableProxied, tx, "Approval", [ - user1.address, - user2.address, + holder.address, + spender.address, amount, ]); // validate allowance was set assert.equalBN( - await rebasableProxied.allowance(user1.address, user2.address), + 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()", 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`; + + // 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, + ]); + + // 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()", 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("increaseAllowance() :: initial allowance is zero", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + "0" + ); + + const allowanceIncrease = wei`1 ether`; + + // increase allowance + const tx = await rebasableProxied.increaseAllowance( + spender.address, + allowanceIncrease + ); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + allowanceIncrease, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + allowanceIncrease + ); + }) + + .test("increaseAllowance() :: initial allowance is not zero", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // set initial allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + const allowanceIncrease = wei`1 ether`; + + // increase allowance + const tx = await rebasableProxied.increaseAllowance( + spender.address, + allowanceIncrease + ); + + const expectedAllowance = wei + .toBigNumber(initialAllowance) + .add(allowanceIncrease); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + expectedAllowance, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + expectedAllowance + ); + }) + + .test("increaseAllowance() :: the increase is not zero", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // set initial allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // increase allowance + const tx = await rebasableProxied.increaseAllowance(spender.address, "0"); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + initialAllowance, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + }) + + .test( + "decreaseAllowance() :: decrease is greater than current allowance", + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + "0" + ); + + const allowanceDecrease = wei`1 ether`; + + // decrease allowance + await assert.revertsWith( + rebasableProxied.decreaseAllowance(spender.address, allowanceDecrease), + "ErrorDecreasedAllowanceBelowZero()" + ); + } + ) + + .group([wei`1 ether`, "0"], (allowanceDecrease) => [ + `decreaseAllowance() :: the decrease is ${allowanceDecrease} wei`, + async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { holder, spender } = ctx.accounts; + + const initialAllowance = wei`2 ether`; + + // set initial allowance + await rebasableProxied.approve(spender.address, initialAllowance); + + // validate allowance before increasing + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + initialAllowance + ); + + // decrease allowance + const tx = await rebasableProxied.decreaseAllowance( + spender.address, + allowanceDecrease + ); + + const expectedAllowance = wei + .toBigNumber(initialAllowance) + .sub(allowanceDecrease); + + // validate Approval event was emitted + await assert.emits(rebasableProxied, tx, "Approval", [ + holder.address, + spender.address, + expectedAllowance, + ]); + + // validate allowance was updated correctly + assert.equalBN( + await rebasableProxied.allowance(holder.address, spender.address), + expectedAllowance + ); + }, + ]) + + .test("bridgeMint() :: not owner", async (ctx) => { + const { rebasableProxied } = ctx.contracts; + const { stranger } = ctx.accounts; + + await assert.revertsWith( + rebasableProxied + .connect(stranger) + .mintShares(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 } = 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) + .mintShares(recipient.address, mintAmount); + + // validate Transfer event was emitted + await assert.emits(rebasableProxied, tx, "Transfer", [ + hre.ethers.constants.AddressZero, + 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).burnShares(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).burnShares(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) + .burnShares(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 decimals = 18; - const [deployer, user1, user2] = await hre.ethers.getSigners(); + const decimalsToSet = 16; + 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(); const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - - const tokensRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); - + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( - wrappedTokenStub.address, - tokensRateOracleStub.address, name, symbol, - decimals + decimalsToSet, + wrappedTokenStub.address, + tokenRateOracleStub.address, + owner.address ); - rebasableTokenImpl.wrap + await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [hre.ethers.constants.AddressZero], }); - + + const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( rebasableTokenImpl.address, deployer.address, @@ -267,12 +966,18 @@ async function ctxFactory() { const rebasableProxied = ERC20Rebasable__factory.connect( l2TokensProxy.address, - user1 + holder ); - + + await tokenRateOracleStub.setDecimals(decimalsToSet); + await tokenRateOracleStub.setLatestRoundDataAnswer(rate); + await tokenRateOracleStub.setUpdatedAt(1000); + + await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); + return { - accounts: { deployer, user1, user2 }, - constants: { name, symbol, decimals }, - contracts: { rebasableProxied, wrappedTokenStub, tokensRateOracleStub } + accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, + constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate }, + contracts: { rebasableProxied, wrappedTokenStub, tokenRateOracleStub } }; } diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index 4a3625a3..33b3fdb0 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -38,7 +38,7 @@ export default function deployment( async erc20TokenBridgeDeployScript( l1Token: string, l1TokenRebasable: string, - tokensRateOracleStub: string, + tokenRateOracleStub: string, l1Params: OptL1DeployScriptParams, l2Params: OptL2DeployScriptParams, ) { @@ -141,7 +141,7 @@ export default function deployment( factory: ERC20Rebasable__factory, args: [ expectedL2TokenProxyAddress, - tokensRateOracleStub, + tokenRateOracleStub, l2TokenRebasableName, l2TokenRebasableSymbol, decimals, @@ -174,7 +174,6 @@ export default function deployment( l1TokenRebasable, expectedL2TokenProxyAddress, expectedL2TokenRebasableProxyAddress, - tokensRateOracleStub, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 721ed54b..4042a351 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -10,7 +10,7 @@ import { ERC20Bridged__factory, ERC20BridgedStub__factory, ERC20WrapableStub__factory, - TokensRateOracleStub__factory, + TokenRateOracleStub__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, CrossDomainMessengerStub__factory, @@ -164,7 +164,7 @@ async function loadDeployedBridges( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), - tokensRateOracle: TokensRateOracleStub__factory.connect( + tokenRateOracle: TokenRateOracleStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), @@ -201,17 +201,17 @@ async function deployTestBridge( "TT" ); - const tokensRateOracleStub = await new TokensRateOracleStub__factory(optDeployer).deploy(); - await tokensRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); - await tokensRateOracleStub.setDecimals(18); - await tokensRateOracleStub.setUpdatedAt(100); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(optDeployer).deploy(); + await tokenRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); + await tokenRateOracleStub.setDecimals(18); + await tokenRateOracleStub.setUpdatedAt(100); const [ethDeployScript, optDeployScript] = await deployment( networkName ).erc20TokenBridgeDeployScript( l1Token.address, l1TokenRebasable.address, - tokensRateOracleStub.address, + tokenRateOracleStub.address, { deployer: ethDeployer, admins: { proxy: ethDeployer.address, bridge: ethDeployer.address }, @@ -252,7 +252,7 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), l1TokenRebasable: l1TokenRebasable.connect(ethProvider), - tokensRateOracle: tokensRateOracleStub, + tokenRateOracle: tokenRateOracleStub, ...connectBridgeContracts( { l2Token: optDeployScript.getContractAddress(1), From 86788b201d8f8689f7a3359dac528039b19146d2 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 27 Dec 2023 23:26:12 +0100 Subject: [PATCH 16/61] simplify oracle, remove warnings --- .solhint.json | 3 +- contracts/optimism/DepositDataCodec.sol | 12 +- contracts/optimism/L1ERC20TokenBridge.sol | 11 +- contracts/optimism/L2ERC20TokenBridge.sol | 4 +- contracts/optimism/TokenRateOracle.sol | 147 +++++------------- contracts/stubs/ERC20Stub.sol | 1 - contracts/stubs/ERC20WrapableStub.sol | 2 - contracts/stubs/TokenRateOracleStub.sol | 16 +- contracts/token/ERC20Core.sol | 2 - contracts/token/ERC20Rebasable.sol | 10 +- .../token/interfaces/IERC20BridgedShares.sol | 23 +++ .../token/interfaces/ITokenRateOracle.sol | 23 +-- 12 files changed, 102 insertions(+), 152 deletions(-) create mode 100644 contracts/token/interfaces/IERC20BridgedShares.sol diff --git a/.solhint.json b/.solhint.json index ff8b9e54..83b993c5 100644 --- a/.solhint.json +++ b/.solhint.json @@ -14,6 +14,7 @@ "ignoreConstructors": true } ], - "lido/fixed-compiler-version": "error" + "lido/fixed-compiler-version": "error", + "const-name-snakecase": false } } diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol index 91b8c574..55dd9ea8 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/optimism/DepositDataCodec.sol @@ -6,8 +6,8 @@ pragma solidity 0.8.10; contract DepositDataCodec { struct DepositData { - uint256 rate; - uint256 time; + uint96 rate; + uint40 time; bytes data; } @@ -22,14 +22,14 @@ contract DepositDataCodec { function decodeDepositData(bytes calldata buffer) internal pure returns (DepositData memory) { - if (buffer.length < 32 * 2) { + if (buffer.length < 12 + 5) { revert ErrorDepositDataLength(); } DepositData memory depositData = DepositData({ - rate: uint256(bytes32(buffer[0:32])), - time: uint256(bytes32(buffer[32:64])), - data: buffer[64:] + rate: uint96(bytes12(buffer[0:12])), + time: uint40(bytes5(buffer[12:17])), + data: buffer[17:] }); return depositData; diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 1c39797e..a475594b 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -16,9 +16,8 @@ import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; -import "hardhat/console.sol"; -// Check if Optimism changed API for bridges. They could depricate methods. +// Check if Optimism changed API for bridges. They could deprecate methods. // Optimise gas usage with data transfer. Maybe cache rate and see if it changed. /// @author psirex, kovalgek @@ -54,9 +53,9 @@ contract L1ERC20TokenBridge is l2TokenBridge = l2TokenBridge_; } - function pushTokenRate(address to_, uint32 l2Gas_) external { + function pushTokenRate(uint32 l2Gas_) external { bytes memory empty = new bytes(0); - _depositERC20To(l1TokenRebasable, l2TokenRebasable, to_, 0, l2Gas_, empty); + _depositERC20To(l1TokenRebasable, l2TokenRebasable, l2TokenBridge, 0, l2Gas_, empty); } /// @inheritdoc IL1ERC20Bridge @@ -140,8 +139,8 @@ contract L1ERC20TokenBridge is if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ - rate: IERC20Wrapable(l1TokenNonRebasable).stETHPerToken(), - time: block.timestamp, + rate: uint96(IERC20Wrapable(l1TokenNonRebasable).stETHPerToken()), + time: uint40(block.timestamp), data: data_ }); diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 36ae5c0b..3e52b948 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -17,8 +17,6 @@ import {BridgeableTokensOptimism} from "./BridgeableTokensOptimism.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; -import { console } from "hardhat/console.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 @@ -111,7 +109,7 @@ contract L2ERC20TokenBridge is if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2TokenRebasable).tokenRateOracle(); - tokenRateOracle.updateRate(int256(depositData.rate), depositData.time, 0); + tokenRateOracle.updateRate(depositData.rate, depositData.time); ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index 0f8cd320..6190ee90 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -4,50 +4,34 @@ pragma solidity 0.8.10; import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; -// import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; +/// @author kovalgek +/// @notice Oracle for storing token rate. contract TokenRateOracle is ITokenRateOracle { - /// Chain specification - uint256 private immutable slotsPerEpoch; - uint256 private immutable secondsPerSlot; - uint256 private immutable genesisTime; - uint256 private immutable initialEpoch; - uint256 private immutable epochsPerFrame; + error NotAnOwner(address caller); + error IncorrectRateTimestamp(); - error InvalidChainConfig(); - error InitialEpochRefSlotCannotBeEarlierThanProcessingSlot(); - error InitialEpochIsYetToArrive(); + /// @notice wstETH/stETH token rate. + uint256 private tokenRate; - int256 private tokenRate; - uint8 private decimalsInAnswer; + /// @notice L1 time when token rate was pushed. uint256 private rateL1Timestamp; - uint80 private answeredInRound; - constructor( - uint256 slotsPerEpoch_, - uint256 secondsPerSlot_, - uint256 genesisTime_, - uint256 initialEpoch_, - uint256 epochsPerFrame_ - ) { - if (slotsPerEpoch_ == 0) revert InvalidChainConfig(); - if (secondsPerSlot_ == 0) revert InvalidChainConfig(); + /// @notice A bridge which can update oracle. + address public immutable bridge; + + /// @notice An updater which can update oracle. + address public immutable tokenRateUpdater; - // Should I use toUint64(); - slotsPerEpoch = slotsPerEpoch_; - secondsPerSlot = secondsPerSlot_; - genesisTime = genesisTime_; - initialEpoch = initialEpoch_; - epochsPerFrame = epochsPerFrame_; + /// @param bridge_ the bridge address that has a right to updates oracle. + /// @param tokenRateUpdater_ address of oracle updater that has a right to updates oracle. + constructor(address bridge_, address tokenRateUpdater_) { + bridge = bridge_; + tokenRateUpdater = tokenRateUpdater_; } - + /// @inheritdoc ITokenRateOracle - /// @return roundId_ is reference slot of HashConsensus - /// @return answer_ is wstETH/stETH token rate. - /// @return startedAt_ is HashConsensus frame start. - /// @return updatedAt_ is L2 timestamp of token rate update. - /// @return answeredInRound_ is the round ID of the round in which the answer was computed function latestRoundData() external view returns ( uint80 roundId_, int256 answer_, @@ -55,14 +39,13 @@ contract TokenRateOracle is ITokenRateOracle { uint256 updatedAt_, uint80 answeredInRound_ ) { - uint256 refSlot = _getRefSlot(initialEpoch, epochsPerFrame); - uint80 roundId = uint80(refSlot); - uint256 startedAt = _computeTimestampAtSlot(refSlot); + uint80 roundId = uint80(rateL1Timestamp); // TODO: add solt + uint80 answeredInRound = roundId; return ( roundId, - tokenRate, - startedAt, + int256(tokenRate), + rateL1Timestamp, rateL1Timestamp, answeredInRound ); @@ -70,89 +53,29 @@ contract TokenRateOracle is ITokenRateOracle { /// @inheritdoc ITokenRateOracle function latestAnswer() external view returns (int256) { - return tokenRate; + return int256(tokenRate); } /// @inheritdoc ITokenRateOracle - function decimals() external view returns (uint8) { - return decimalsInAnswer; + function decimals() external pure returns (uint8) { + return 18; } /// @inheritdoc ITokenRateOracle - function updateRate(int256 rate, uint256 rateL1Timestamp_) external { - // check timestamp not late as current one. - if (rateL1Timestamp_ < _getTime()) { - return; + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external onlyOwner { + // reject rates from the future + if (rateL1Timestamp_ < rateL1Timestamp) { + revert IncorrectRateTimestamp(); } - tokenRate = rate; + tokenRate = tokenRate_; rateL1Timestamp = rateL1Timestamp_; - answeredInRound = 666; - decimalsInAnswer = 10; - } - - /// Frame utilities - - function _getTime() internal virtual view returns (uint256) { - return block.timestamp; // solhint-disable-line not-rely-on-time - } - - function _getRefSlot(uint256 initialEpoch_, uint256 epochsPerFrame_) internal view returns (uint256) { - return _getRefSlotAtTimestamp(_getTime(), initialEpoch_, epochsPerFrame_); - } - - function _getRefSlotAtTimestamp(uint256 timestamp_, uint256 initialEpoch_, uint256 epochsPerFrame_) - internal view returns (uint256) - { - return _getRefSlotAtIndex(_computeFrameIndex(timestamp_, initialEpoch_, epochsPerFrame_), initialEpoch_, epochsPerFrame_); - } - - function _getRefSlotAtIndex(uint256 frameIndex_, uint256 initialEpoch_, uint256 epochsPerFrame_) - internal view returns (uint256) - { - uint256 frameStartEpoch = _computeStartEpochOfFrameWithIndex(frameIndex_, initialEpoch_, epochsPerFrame_); - uint256 frameStartSlot = _computeStartSlotAtEpoch(frameStartEpoch); - return uint64(frameStartSlot - 1); } - function _computeStartSlotAtEpoch(uint256 epoch_) internal view returns (uint256) { - // See: github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch - return epoch_ * slotsPerEpoch; - } - - function _computeStartEpochOfFrameWithIndex(uint256 frameIndex_, uint256 initialEpoch_, uint256 epochsPerFrame_) - internal pure returns (uint256) - { - return initialEpoch_ + frameIndex_ * epochsPerFrame_; - } - - function _computeFrameIndex( - uint256 timestamp_, - uint256 initialEpoch_, - uint256 epochsPerFrame_ - ) internal view returns (uint256) - { - uint256 epoch = _computeEpochAtTimestamp(timestamp_); - if (epoch < initialEpoch_) { - revert InitialEpochIsYetToArrive(); + /// @dev validates that method called by one of the owners + modifier onlyOwner() { + if (msg.sender != bridge || msg.sender != tokenRateUpdater) { + revert NotAnOwner(msg.sender); } - return (epoch - initialEpoch_) / epochsPerFrame_; - } - - function _computeEpochAtTimestamp(uint256 timestamp_) internal view returns (uint256) { - return _computeEpochAtSlot(_computeSlotAtTimestamp(timestamp_)); - } - - function _computeEpochAtSlot(uint256 slot_) internal view returns (uint256) { - // See: github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_epoch_at_slot - return slot_ / slotsPerEpoch; - } - - function _computeSlotAtTimestamp(uint256 timestamp_) internal view returns (uint256) { - return (timestamp_ - genesisTime) / secondsPerSlot; - } - - function _computeTimestampAtSlot(uint256 slot_) internal view returns (uint256) { - // See: github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#compute_timestamp_at_slot - return genesisTime + slot_ * secondsPerSlot; + _; } } \ No newline at end of file diff --git a/contracts/stubs/ERC20Stub.sol b/contracts/stubs/ERC20Stub.sol index 686ea516..b8ef5902 100644 --- a/contracts/stubs/ERC20Stub.sol +++ b/contracts/stubs/ERC20Stub.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { console } from "hardhat/console.sol"; contract ERC20Stub is IERC20 { diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index 9f48b24f..de8f244b 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -7,8 +7,6 @@ import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; -// import {ERC20Core} from "../token/ERC20Core.sol"; -import { console } from "hardhat/console.sol"; // represents wstETH on L1 contract ERC20WrapableStub is IERC20Wrapable, ERC20 { diff --git a/contracts/stubs/TokenRateOracleStub.sol b/contracts/stubs/TokenRateOracleStub.sol index 40463af7..6581e512 100644 --- a/contracts/stubs/TokenRateOracleStub.sol +++ b/contracts/stubs/TokenRateOracleStub.sol @@ -17,9 +17,9 @@ contract TokenRateOracleStub is ITokenRateOracle { return _decimals; } - int256 public latestRoundDataAnswer; + uint256 public latestRoundDataAnswer; - function setLatestRoundDataAnswer(int256 answer_) external { + function setLatestRoundDataAnswer(uint256 answer_) external { latestRoundDataAnswer = answer_; } @@ -42,14 +42,20 @@ contract TokenRateOracleStub is ITokenRateOracle { uint256 updatedAt, uint80 answeredInRound ) { - return (0,latestRoundDataAnswer,0,latestRoundDataUpdatedAt,0); + return ( + 0, + int256(latestRoundDataAnswer), + 0, + latestRoundDataUpdatedAt, + 0 + ); } function latestAnswer() external view returns (int256) { - return latestRoundDataAnswer; + return int256(latestRoundDataAnswer); } - function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external { + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { // check timestamp not late as current one. latestRoundDataAnswer = tokenRate_; latestRoundDataUpdatedAt = rateL1Timestamp_; diff --git a/contracts/token/ERC20Core.sol b/contracts/token/ERC20Core.sol index 9fb618cb..bf4e67db 100644 --- a/contracts/token/ERC20Core.sol +++ b/contracts/token/ERC20Core.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { console } from "hardhat/console.sol"; /// @author psirex /// @notice Contains the required logic of the ERC20 standard as defined in the EIP. Additionally @@ -122,7 +121,6 @@ contract ERC20Core is IERC20 { address spender_, uint256 amount_ ) internal virtual onlyNonZeroAccount(owner_) onlyNonZeroAccount(spender_) { - console.log("_approve %@ %@ %@", msg.sender, owner_, spender_); allowance[owner_][spender_] = amount_; emit Approval(owner_, spender_, amount_); } diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index b7a32798..a46fa2da 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -5,13 +5,13 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; +import {IERC20BridgedShares} from "./interfaces/IERC20BridgedShares.sol"; import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; -// import { console } from "hardhat/console.sol"; /// @author kovalgek /// @notice Extends the ERC20Shared functionality -contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { +contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Metadata { error ErrorZeroSharesWrap(); error ErrorZeroTokensUnwrap(); @@ -25,7 +25,7 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { error ErrorDecreasedAllowanceBelowZero(); error ErrorNotBridge(); - /// @notice Bridge which can mint and burn tokens on L2. + /// @inheritdoc IERC20BridgedShares address public immutable bridge; /// @notice Contract of non-rebasable token to wrap. @@ -97,12 +97,12 @@ contract ERC20Rebasable is IERC20Wrapable, IERC20, ERC20Metadata { return uint256(tokenRateOracle.latestAnswer()); } - // allow call only bridge + /// @inheritdoc IERC20BridgedShares function mintShares(address account_, uint256 amount_) external onlyBridge returns (uint256) { return _mintShares(account_, amount_); } - // allow call only bridge + /// @inheritdoc IERC20BridgedShares function burnShares(address account_, uint256 amount_) external onlyBridge { _burnShares(account_, amount_); } diff --git a/contracts/token/interfaces/IERC20BridgedShares.sol b/contracts/token/interfaces/IERC20BridgedShares.sol new file mode 100644 index 00000000..11f3ba78 --- /dev/null +++ b/contracts/token/interfaces/IERC20BridgedShares.sol @@ -0,0 +1,23 @@ +// 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 kovalgek +/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens +interface IERC20BridgedShares 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 mintShares(address account_, uint256 amount_) external returns (uint256); + + /// @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 burnShares(address account_, uint256 amount_) external; +} diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/token/interfaces/ITokenRateOracle.sol index eb9aa8aa..1c33fc21 100644 --- a/contracts/token/interfaces/ITokenRateOracle.sol +++ b/contracts/token/interfaces/ITokenRateOracle.sol @@ -4,27 +4,32 @@ pragma solidity 0.8.10; /// @author kovalgek -/// @notice Oracle interface for two tokens rate. A subset of Chainlink data feed interface. +/// @notice Oracle interface for token rate. A subset of Chainlink data feed interface. interface ITokenRateOracle { - /// @notice get data about the latest round. + /// @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. + /// @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 + uint80 roundId_, + int256 answer_, + uint256 startedAt_, + uint256 updatedAt_, + uint80 answeredInRound_ ); - /// @notice get answer about the latest round. + /// @notice get the lastest token rate. function latestAnswer() external view returns (int256); /// @notice represents the number of decimals the oracle responses represent. function decimals() external view returns (uint8); /// @notice Updates token rate. - function updateRate(int256 tokenRate_, uint256 rateL1Timestamp_, uint256 lastProcessingRefSlot_) external; + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external; } \ No newline at end of file From ddffe1ffee7b756a71b82f54c0acaec4fb819f35 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 28 Dec 2023 10:43:11 +0100 Subject: [PATCH 17/61] update unit tests for token rate oracle --- contracts/optimism/TokenRateOracle.sol | 2 +- test/optimism/TokenRateOracle.unit.test.ts | 96 +++++++++++----------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index 6190ee90..ef5e8dc5 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -73,7 +73,7 @@ contract TokenRateOracle is ITokenRateOracle { /// @dev validates that method called by one of the owners modifier onlyOwner() { - if (msg.sender != bridge || msg.sender != tokenRateUpdater) { + if (msg.sender != bridge && msg.sender != tokenRateUpdater) { revert NotAnOwner(msg.sender); } _; diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index 3858ed2e..f5981628 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -2,36 +2,17 @@ import hre from "hardhat"; import { assert } from "chai"; import { unit } from "../../utils/testing"; import { TokenRateOracle__factory } from "../../typechain"; -import { ethers } from "ethers"; unit("TokenRateOracle", ctxFactory) - .test("init zero slotsPerEpoch", async (ctx) => { - const [deployer] = await hre.ethers.getSigners(); - await assert.revertsWith(new TokenRateOracle__factory(deployer).deploy( - 0, - 10, - 1000, - 100, - 50 - ), "InvalidChainConfig()"); - }) - - .test("init zero secondsPerSlot", async (ctx) => { - const [deployer] = await hre.ethers.getSigners(); - await assert.revertsWith(new TokenRateOracle__factory(deployer).deploy( - 41, - 0, - 1000, - 100, - 50 - ), "InvalidChainConfig()"); - }) - .test("state after init", async (ctx) => { - const { tokensRateOracle } = ctx.contracts; + const { tokenRateOracle } = ctx.contracts; + const { bridge, updater } = ctx.accounts; + + assert.equal(await tokenRateOracle.bridge(), bridge.address); + assert.equal(await tokenRateOracle.tokenRateUpdater(), updater.address); - assert.equalBN(await tokensRateOracle.latestAnswer(), 0); + assert.equalBN(await tokenRateOracle.latestAnswer(), 0); const { roundId_, @@ -39,23 +20,42 @@ unit("TokenRateOracle", ctxFactory) startedAt_, updatedAt_, answeredInRound_ - } = await tokensRateOracle.latestRoundData(); + } = await tokenRateOracle.latestRoundData(); - assert.equalBN(roundId_, 170307199); + assert.equalBN(roundId_, 0); assert.equalBN(answer_, 0); - assert.equalBN(startedAt_, 1703072990); + assert.equalBN(startedAt_, 0); assert.equalBN(updatedAt_, 0); assert.equalBN(answeredInRound_, 0); + assert.equalBN(await tokenRateOracle.decimals(), 18); + }) - assert.equalBN(await tokensRateOracle.decimals(), 0); + .test("wrong owner", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge, updater, stranger } = ctx.accounts; + tokenRateOracle.connect(bridge).updateRate(10, 20); + tokenRateOracle.connect(updater).updateRate(10, 23); + await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "NotAnOwner(\""+stranger.address+"\")"); + }) + + .test("incorrect time", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + + tokenRateOracle.connect(bridge).updateRate(10, 1000); + await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "IncorrectRateTimestamp()"); }) .test("state after update token rate", async (ctx) => { - const { tokensRateOracle } = ctx.contracts; + const { tokenRateOracle } = ctx.contracts; + const { updater } = ctx.accounts; - await tokensRateOracle.updateRate(2, ethers.constants.MaxInt256 ); + const currentTime = Date.now(); + const tokenRate = 123; - assert.equalBN(await tokensRateOracle.latestAnswer(), 2); + await tokenRateOracle.connect(updater).updateRate(tokenRate, currentTime ); + + assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRate); const { roundId_, @@ -63,33 +63,29 @@ unit("TokenRateOracle", ctxFactory) startedAt_, updatedAt_, answeredInRound_ - } = await tokensRateOracle.latestRoundData(); - - assert.equalBN(roundId_, 170307199); - assert.equalBN(answer_, 2); - assert.equalBN(startedAt_, 1703072990); - assert.equalBN(updatedAt_, ethers.constants.MaxInt256); - assert.equalBN(answeredInRound_, 666); - - assert.equalBN(await tokensRateOracle.decimals(), 10); + } = await tokenRateOracle.latestRoundData(); + + assert.equalBN(roundId_, currentTime); + assert.equalBN(answer_, tokenRate); + assert.equalBN(startedAt_, currentTime); + assert.equalBN(updatedAt_, currentTime); + assert.equalBN(answeredInRound_, currentTime); + assert.equalBN(await tokenRateOracle.decimals(), 18); }) .run(); async function ctxFactory() { - const [deployer] = await hre.ethers.getSigners(); + const [deployer, bridge, updater, stranger] = await hre.ethers.getSigners(); - const tokensRateOracle = await new TokenRateOracle__factory(deployer).deploy( - 32, - 10, - 1000, - 100, - 50 + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + bridge.address, + updater.address ); return { - accounts: { deployer }, - contracts: { tokensRateOracle } + accounts: { deployer, bridge, updater, stranger }, + contracts: { tokenRateOracle } }; } From 7c79d2d7fb76679a7627b88f3ed5bb7bfa2644ea Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 9 Jan 2024 09:23:37 +0100 Subject: [PATCH 18/61] fix integration tests for rebasable token --- contracts/optimism/L2ERC20TokenBridge.sol | 4 +- contracts/stubs/ERC20WrapableStub.sol | 5 +- scripts/optimism/deploy-bridge.ts | 1 + .../optimism.integration.test.ts | 1 - .../bridging-rebase.integration.test.ts | 681 +++++++++--------- test/token/ERC20Rebasable.unit.test.ts | 6 +- utils/optimism/deployment.ts | 24 +- utils/optimism/testing.ts | 32 +- 8 files changed, 396 insertions(+), 358 deletions(-) diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 3e52b948..873e0380 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -85,9 +85,11 @@ contract L2ERC20TokenBridge is uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); _initiateWithdrawal(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, shares, l1Gas_, data_); + emit WithdrawalInitiated(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, amount_, data_); } else if (l2Token_ == l2TokenNonRebasable) { IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); _initiateWithdrawal(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l1Gas_, data_); + emit WithdrawalInitiated(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, data_); } } @@ -148,7 +150,5 @@ contract L2ERC20TokenBridge is ); sendCrossDomainMessage(l1TokenBridge, l1Gas_, message); - - emit WithdrawalInitiated(l1Token_, l2Token_, from_, to_, amount_, data_); } } diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrapableStub.sol index de8f244b..3f77b88c 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrapableStub.sol @@ -27,7 +27,7 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { function wrap(uint256 _stETHAmount) external returns (uint256) { require(_stETHAmount > 0, "wstETH: can't wrap zero stETH"); - uint256 wstETHAmount = (_stETHAmount * tokensRate) / (10 ** uint256(decimals())); + uint256 wstETHAmount = (_stETHAmount * (10 ** uint256(decimals()))) / tokensRate; _mint(msg.sender, wstETHAmount); stETH.transferFrom(msg.sender, address(this), _stETHAmount); @@ -38,7 +38,8 @@ contract ERC20WrapableStub is IERC20Wrapable, ERC20 { function unwrap(uint256 _wstETHAmount) external returns (uint256) { require(_wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); - uint256 stETHAmount = (_wstETHAmount * (10 ** uint256(decimals()))) / tokensRate; + uint256 stETHAmount = (_wstETHAmount * tokensRate) / (10 ** uint256(decimals())); + _burn(msg.sender, _wstETHAmount); stETH.transfer(msg.sender, stETHAmount); diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 77d3ecb7..633edb29 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -25,6 +25,7 @@ async function main() { .deployment(networkName, { logger: console }) .erc20TokenBridgeDeployScript( deploymentConfig.token, + deploymentConfig.token, // FIX { deployer: ethDeployer, admins: { diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 6cd18db8..ab28543e 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -243,7 +243,6 @@ async function ctxFactory() { .erc20TokenBridgeDeployScript( l1Token.address, l1TokenRebasable.address, - tokenRateOracleStub.address, { deployer: l1Deployer, admins: { diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index cda397c2..2bc623cb 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -6,6 +6,8 @@ import optimism from "../../utils/optimism"; import testing, { scenario } from "../../utils/testing"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ERC20WrapableStub } from "../../typechain"; scenario("Optimism :: Bridging integration test", ctxFactory) .after(async (ctx) => { @@ -71,6 +73,42 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.isTrue(await l2ERC20TokenBridge.isWithdrawalsEnabled()); }) + .step("Set up Token Rate Oracle by pushing first rate", async (ctx) => { + + const { + l1Token, + l1TokenRebasable, + l2TokenRebasable, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + l2Provider + } = ctx; + + const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = + ctx.accounts; + const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); + + const tx = await l2CrossDomainMessenger + .connect(l1CrossDomainMessengerAliased) + .relayMessage( + 1, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.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, @@ -79,12 +117,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, - tokenRateOracle, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const stETHPerToken = await l1Token.stETHPerToken(); await l1TokenRebasable .connect(tokenHolderA.l1Signer) @@ -108,12 +144,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - const blockNumber = await l1Provider.getBlockNumber(); - const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; - const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) - const dataToSend = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); - + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, @@ -165,21 +196,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, - tokenRateOracle, l2Provider } = ctx; - const stETHPerToken = await l1Token.stETHPerToken(); - const blockNumber = await l2Provider.getBlockNumber(); - const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; - const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) - const dataToReceive = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); + const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = ctx.accounts; - const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( tokenHolderA.address ); @@ -204,11 +228,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]), { gasLimit: 5_000_000 } ); - - - const [,tokensRate,,updatedAt,] = await tokenRateOracle.latestRoundData(); - assert.equalBN(stETHPerToken, tokensRate); - assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, @@ -237,19 +256,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, - tokenRateOracle, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const { depositAmount: depositAmountInRebasableTokens } = ctx.common; - const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).mul(2); - const stETHPerToken = await l1Token.stETHPerToken(); + const { depositAmountNonRebasable, depositAmountRebasable } = ctx.common; - await tokenRateOracle.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); - await l1TokenRebasable .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, depositAmountInRebasableTokens); + .approve(l1ERC20TokenBridge.address, depositAmountRebasable); const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address @@ -264,24 +278,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .depositERC20( l1TokenRebasable.address, l2TokenRebasable.address, - depositAmountInRebasableTokens, + depositAmountRebasable, 200_000, "0x" ); - const blockNumber = await l1Provider.getBlockNumber(); - const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; - const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) - const dataToSend = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); - + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - depositAmount, + depositAmountNonRebasable, dataToSend, ]); @@ -292,7 +301,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - depositAmount, + depositAmountNonRebasable, dataToSend, ] ); @@ -309,12 +318,12 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1ERC20TokenBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) ); assert.equalBN( await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH - tokenHolderABalanceBefore.sub(depositAmountInRebasableTokens) + tokenHolderABalanceBefore.sub(depositAmountRebasable) ); }) @@ -326,31 +335,20 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1ERC20TokenBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, - tokenRateOracle, l2Provider } = ctx; - const { depositAmount: depositAmountInRebasableTokens } = ctx.common; - const depositAmount = wei.toBigNumber(depositAmountInRebasableTokens).div(2); - const stETHPerToken = await l1Token.stETHPerToken(); - - - const blockNumber = await l2Provider.getBlockNumber(); - const blockTimestamp = (await l2Provider.getBlock(blockNumber)).timestamp; - const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 32) - const stETHPerTokenStr = ethers.utils.hexZeroPad(stETHPerToken.toHexString(), 32) - const dataToReceive = ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); - + 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( @@ -364,61 +362,50 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - depositAmount, + depositAmountNonRebasable, dataToReceive, ]), { gasLimit: 5_000_000 } ); - - - const [,tokensRate,,updatedAt,] = await tokenRateOracle.latestRoundData(); - assert.equalBN(stETHPerToken, tokensRate); - assert.equalBN(blockTimestamp, updatedAt); await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - depositAmount, + depositAmountNonRebasable, "0x", ]); assert.equalBN( await l2TokenRebasable.balanceOf(tokenHolderA.address), - tokenHolderABalanceBefore.add(depositAmountInRebasableTokens) + tokenHolderABalanceBefore.add(depositAmountRebasable) ); assert.equalBN( await l2TokenRebasable.totalSupply(), - l2TokenRebasableTotalSupplyBefore.add(depositAmountInRebasableTokens) + l2TokenRebasableTotalSupplyBefore.add(depositAmountRebasable) ); }) .step("L2 -> L1 withdrawal via withdraw()", async (ctx) => { const { accountA: tokenHolderA } = ctx.accounts; - const { withdrawalAmount: withdrawalAmountInRebasableTokens } = ctx.common; + const { withdrawalAmountRebasable } = ctx.common; const { l1TokenRebasable, l2TokenRebasable, l2ERC20TokenBridge } = ctx; - const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).div(2); - const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( tokenHolderA.address ); const l2TotalSupplyBefore = await l2TokenRebasable.totalSupply(); - await l2TokenRebasable - .connect(tokenHolderA.l2Signer) - .approve(l2ERC20TokenBridge.address, withdrawalAmountInRebasableTokens); - const tx = await l2ERC20TokenBridge .connect(tokenHolderA.l2Signer) .withdraw( l2TokenRebasable.address, - withdrawalAmountInRebasableTokens, + withdrawalAmountRebasable, 0, "0x" ); @@ -428,17 +415,17 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - withdrawalAmount, + withdrawalAmountRebasable, "0x", ]); assert.equalBN( await l2TokenRebasable.balanceOf(tokenHolderA.address), - tokenHolderABalanceBefore.sub(withdrawalAmountInRebasableTokens) + tokenHolderABalanceBefore.sub(withdrawalAmountRebasable) ); assert.equalBN( await l2TokenRebasable.totalSupply(), - l2TotalSupplyBefore.sub(withdrawalAmountInRebasableTokens) + l2TotalSupplyBefore.sub(withdrawalAmountRebasable) ); }) @@ -453,8 +440,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2ERC20TokenBridge, } = ctx; const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; - const { withdrawalAmount: withdrawalAmountInRebasableTokens } = ctx.common; - const withdrawalAmount = wei.toBigNumber(withdrawalAmountInRebasableTokens).mul(2); + const { withdrawalAmountNonRebasable, withdrawalAmountRebasable } = ctx.common; const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address @@ -479,7 +465,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - withdrawalAmount, + withdrawalAmountNonRebasable, "0x", ] ), @@ -491,268 +477,292 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - withdrawalAmount, + withdrawalAmountNonRebasable, "0x", ]); assert.equalBN( await l1Token.balanceOf(l1ERC20TokenBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) ); assert.equalBN( await l1TokenRebasable.balanceOf(tokenHolderA.address), - tokenHolderABalanceBefore.add(withdrawalAmountInRebasableTokens) + tokenHolderABalanceBefore.add(withdrawalAmountRebasable) + ); + }) + + + .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { + + const { + l1Token, + l1TokenRebasable, + l1ERC20TokenBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + 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(l1ERC20TokenBridge.address, depositAmountRebasable); + + const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + tokenHolderA.address + ); + const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); + + const tx = await l1ERC20TokenBridge + .connect(tokenHolderA.l1Signer) + .depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + depositAmountRebasable, + 200_000, + "0x" + ); + + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountNonRebasable, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountNonRebasable, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH + tokenHolderABalanceBefore.sub(depositAmountRebasable) + ); + }) + + .step("Finalize deposit on L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1ERC20TokenBridge, + l2TokenRebasable, + l2CrossDomainMessenger, + l2ERC20TokenBridge, + 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, + l1ERC20TokenBridge.address, + l2ERC20TokenBridge.address, + 0, + 300_000, + l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountNonRebasable, + dataToReceive, + ]), + { gasLimit: 5_000_000 } + ); + + await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderA.address, + tokenHolderB.address, + depositAmountNonRebasable, + "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, l2ERC20TokenBridge } = 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 l2ERC20TokenBridge + .connect(tokenHolderB.l2Signer) + .withdrawTo( + l2TokenRebasable.address, + tokenHolderA.address, + withdrawalAmountRebasable, + 0, + "0x" + ); + + await assert.emits(l2ERC20TokenBridge, 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, + l1ERC20TokenBridge, + l2CrossDomainMessenger, + l2TokenRebasable, + l2ERC20TokenBridge, + } = 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 l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + l1ERC20TokenBridge.address + ); + + await l1CrossDomainMessenger + .connect(l1Stranger) + .setXDomainMessageSender(l2ERC20TokenBridge.address); + + const tx = await l1CrossDomainMessenger + .connect(l1Stranger) + .relayMessage( + l1ERC20TokenBridge.address, + l2CrossDomainMessenger.address, + l1ERC20TokenBridge.interface.encodeFunctionData( + "finalizeERC20Withdrawal", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmountNonRebasable, + "0x", + ] + ), + 0 + ); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + tokenHolderB.address, + tokenHolderA.address, + withdrawalAmountNonRebasable, + "0x", + ]); -// .step("L1 -> L2 deposit via depositERC20To()", async (ctx) => { -// const { -// l1Token, -// l2Token, -// l1ERC20TokenBridge, -// l2ERC20TokenBridge, -// l1CrossDomainMessenger, -// } = ctx; -// const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; -// const { depositAmount } = ctx.common; - -// assert.notEqual(tokenHolderA.address, tokenHolderB.address); - -// await l1Token -// .connect(tokenHolderA.l1Signer) -// .approve(l1ERC20TokenBridge.address, depositAmount); - -// const tokenHolderABalanceBefore = await l1Token.balanceOf( -// tokenHolderA.address -// ); -// const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( -// l1ERC20TokenBridge.address -// ); - -// const tx = await l1ERC20TokenBridge -// .connect(tokenHolderA.l1Signer) -// .depositERC20To( -// l1Token.address, -// l2Token.address, -// tokenHolderB.address, -// depositAmount, -// 200_000, -// "0x" -// ); - -// await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderB.address, -// depositAmount, -// "0x", -// ]); - -// const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( -// "finalizeDeposit", -// [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderB.address, -// depositAmount, -// "0x", -// ] -// ); - -// const messageNonce = await l1CrossDomainMessenger.messageNonce(); - -// await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ -// l2ERC20TokenBridge.address, -// l1ERC20TokenBridge.address, -// l2DepositCalldata, -// messageNonce, -// 200_000, -// ]); - -// assert.equalBN( -// await l1Token.balanceOf(l1ERC20TokenBridge.address), -// l1ERC20TokenBridgeBalanceBefore.add(depositAmount) -// ); - -// assert.equalBN( -// await l1Token.balanceOf(tokenHolderA.address), -// tokenHolderABalanceBefore.sub(depositAmount) -// ); -// }) - -// .step("Finalize deposit on L2", async (ctx) => { -// const { -// l1Token, -// l1ERC20TokenBridge, -// l2Token, -// l2CrossDomainMessenger, -// l2ERC20TokenBridge, -// } = ctx; -// const { -// accountA: tokenHolderA, -// accountB: tokenHolderB, -// l1CrossDomainMessengerAliased, -// } = ctx.accounts; -// const { depositAmount } = ctx.common; - -// const l2TokenTotalSupplyBefore = await l2Token.totalSupply(); -// const tokenHolderBBalanceBefore = await l2Token.balanceOf( -// tokenHolderB.address -// ); - -// const tx = await l2CrossDomainMessenger -// .connect(l1CrossDomainMessengerAliased) -// .relayMessage( -// 1, -// l1ERC20TokenBridge.address, -// l2ERC20TokenBridge.address, -// 0, -// 300_000, -// l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderB.address, -// depositAmount, -// "0x", -// ]), -// { gasLimit: 5_000_000 } -// ); - -// await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ -// l1Token.address, -// l2Token.address, -// tokenHolderA.address, -// tokenHolderB.address, -// depositAmount, -// "0x", -// ]); - -// assert.equalBN( -// await l2Token.totalSupply(), -// l2TokenTotalSupplyBefore.add(depositAmount) -// ); -// assert.equalBN( -// await l2Token.balanceOf(tokenHolderB.address), -// tokenHolderBBalanceBefore.add(depositAmount) -// ); -// }) - -// .step("L2 -> L1 withdrawal via withdrawTo()", async (ctx) => { -// const { l1Token, l2Token, l2ERC20TokenBridge } = ctx; -// const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; -// const { withdrawalAmount } = ctx.common; - -// const tokenHolderBBalanceBefore = await l2Token.balanceOf( -// tokenHolderB.address -// ); -// const l2TotalSupplyBefore = await l2Token.totalSupply(); - -// const tx = await l2ERC20TokenBridge -// .connect(tokenHolderB.l2Signer) -// .withdrawTo( -// l2Token.address, -// tokenHolderA.address, -// withdrawalAmount, -// 0, -// "0x" -// ); - -// await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ -// l1Token.address, -// l2Token.address, -// tokenHolderB.address, -// tokenHolderA.address, -// withdrawalAmount, -// "0x", -// ]); - -// assert.equalBN( -// await l2Token.balanceOf(tokenHolderB.address), -// tokenHolderBBalanceBefore.sub(withdrawalAmount) -// ); - -// assert.equalBN( -// await l2Token.totalSupply(), -// l2TotalSupplyBefore.sub(withdrawalAmount) -// ); -// }) - -// .step("Finalize withdrawal on L1", async (ctx) => { -// const { -// l1Token, -// l1CrossDomainMessenger, -// l1ERC20TokenBridge, -// l2CrossDomainMessenger, -// l2Token, -// l2ERC20TokenBridge, -// } = ctx; -// const { -// accountA: tokenHolderA, -// accountB: tokenHolderB, -// l1Stranger, -// } = ctx.accounts; -// const { withdrawalAmount } = ctx.common; - -// const tokenHolderABalanceBefore = await l1Token.balanceOf( -// tokenHolderA.address -// ); -// const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( -// l1ERC20TokenBridge.address -// ); - -// await l1CrossDomainMessenger -// .connect(l1Stranger) -// .setXDomainMessageSender(l2ERC20TokenBridge.address); - -// const tx = await l1CrossDomainMessenger -// .connect(l1Stranger) -// .relayMessage( -// l1ERC20TokenBridge.address, -// l2CrossDomainMessenger.address, -// l1ERC20TokenBridge.interface.encodeFunctionData( -// "finalizeERC20Withdrawal", -// [ -// l1Token.address, -// l2Token.address, -// tokenHolderB.address, -// tokenHolderA.address, -// withdrawalAmount, -// "0x", -// ] -// ), -// 0 -// ); - -// await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ -// l1Token.address, -// l2Token.address, -// tokenHolderB.address, -// tokenHolderA.address, -// withdrawalAmount, -// "0x", -// ]); - -// assert.equalBN( -// await l1Token.balanceOf(l1ERC20TokenBridge.address), -// l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) -// ); - -// assert.equalBN( -// await l1Token.balanceOf(tokenHolderA.address), -// tokenHolderABalanceBefore.add(withdrawalAmount) -// ); -// }) + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(tokenHolderA.address), + tokenHolderABalanceBefore.add(withdrawalAmountRebasable) + ); + }) .run(); async function ctxFactory() { const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); - console.log("networkName=",networkName); const { l1Provider, @@ -770,8 +780,12 @@ async function ctxFactory() { 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`; + 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(), @@ -793,22 +807,21 @@ async function ctxFactory() { await contracts.l1TokenRebasable .connect(contracts.l1TokensHolder) - .transfer(accountA.l1Signer.address, wei.toBigNumber(depositAmount).mul(2)); + .transfer(accountA.l1Signer.address, depositAmountRebasable); 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 ); + await contracts.l1ERC20TokenBridge.connect(l1ERC20TokenBridgeAdmin).pushTokenRate(1000000); + return { l1Provider, l2Provider, @@ -822,8 +835,11 @@ async function ctxFactory() { l1CrossDomainMessengerAliased, }, common: { - depositAmount, - withdrawalAmount, + depositAmountNonRebasable, + depositAmountRebasable, + withdrawalAmountNonRebasable, + withdrawalAmountRebasable, + exchangeRate, }, snapshot: { l1: l1Snapshot, @@ -831,3 +847,12 @@ async function ctxFactory() { }, }; } + +async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapableStub) { + 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]); +} \ No newline at end of file diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index ac87c9d5..f429ebbd 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -41,8 +41,8 @@ unit("ERC20Rebasable", ctxFactory) owner.address ); await assert.revertsWith( - rebasableTokenImpl.initialize("New Name", ""), - "ErrorNameAlreadySet()" + rebasableTokenImpl.initialize("New Name", ""), + "ErrorNameAlreadySet()" ); }) @@ -61,7 +61,7 @@ unit("ERC20Rebasable", ctxFactory) owner.address ); await assert.revertsWith( - rebasableTokenImpl.initialize("", "New Symbol"), + rebasableTokenImpl.initialize("", "New Symbol"), "ErrorSymbolAlreadySet()" ); }) diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index 33b3fdb0..a73a1ed4 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -7,6 +7,8 @@ import { L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, OssifiableProxy__factory, + TokenRateOracle, + TokenRateOracle__factory, } from "../../typechain"; import addresses from "./addresses"; @@ -38,7 +40,6 @@ export default function deployment( async erc20TokenBridgeDeployScript( l1Token: string, l1TokenRebasable: string, - tokenRateOracleStub: string, l1Params: OptL1DeployScriptParams, l2Params: OptL2DeployScriptParams, ) { @@ -49,14 +50,15 @@ export default function deployment( ] = await network.predictAddresses(l1Params.deployer, 2); const [ + expectedL2TokenRateOracleImplAddress, expectedL2TokenImplAddress, expectedL2TokenProxyAddress, expectedL2TokenRebasableImplAddress, expectedL2TokenRebasableProxyAddress, expectedL2TokenBridgeImplAddress, expectedL2TokenBridgeProxyAddress, - ] = await network.predictAddresses(l2Params.deployer, 6); - + ] = await network.predictAddresses(l2Params.deployer, 7); + const l1DeployScript = new DeployScript( l1Params.deployer, options?.logger @@ -111,6 +113,17 @@ export default function deployment( l2Params.deployer, options?.logger ) + .addStep({ + factory: TokenRateOracle__factory, + args: [ + expectedL2TokenBridgeProxyAddress, + expectedL2TokenBridgeProxyAddress, + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRateOracleImplAddress), + }) + .addStep({ factory: ERC20Bridged__factory, args: [ @@ -140,11 +153,12 @@ export default function deployment( .addStep({ factory: ERC20Rebasable__factory, args: [ - expectedL2TokenProxyAddress, - tokenRateOracleStub, l2TokenRebasableName, l2TokenRebasableSymbol, decimals, + expectedL2TokenProxyAddress, + expectedL2TokenRateOracleImplAddress, + expectedL2TokenBridgeProxyAddress, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 4042a351..1da87bb5 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -10,7 +10,7 @@ import { ERC20Bridged__factory, ERC20BridgedStub__factory, ERC20WrapableStub__factory, - TokenRateOracleStub__factory, + TokenRateOracle__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, CrossDomainMessengerStub__factory, @@ -164,13 +164,10 @@ async function loadDeployedBridges( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), - tokenRateOracle: TokenRateOracleStub__factory.connect( - testingUtils.env.OPT_L1_TOKEN(), - l1SignerOrProvider - ), ...connectBridgeContracts( { + tokenRateOracle: testingUtils.env.OPT_L2_TOKEN(), // fix l2Token: testingUtils.env.OPT_L2_TOKEN(), l2TokenRebasable: testingUtils.env.OPT_L2_TOKEN(), // fix l1ERC20TokenBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), @@ -201,17 +198,11 @@ async function deployTestBridge( "TT" ); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(optDeployer).deploy(); - await tokenRateOracleStub.setLatestRoundDataAnswer(BigNumber.from("1000000000000000000")); - await tokenRateOracleStub.setDecimals(18); - await tokenRateOracleStub.setUpdatedAt(100); - const [ethDeployScript, optDeployScript] = await deployment( networkName ).erc20TokenBridgeDeployScript( l1Token.address, l1TokenRebasable.address, - tokenRateOracleStub.address, { deployer: ethDeployer, admins: { proxy: ethDeployer.address, bridge: ethDeployer.address }, @@ -231,7 +222,7 @@ async function deployTestBridge( ethDeployer ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 5; + const l2ERC20TokenBridgeProxyDeployStepIndex = 6; const l2BridgingManagement = new BridgingManagement( optDeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), optDeployer @@ -252,13 +243,13 @@ async function deployTestBridge( return { l1Token: l1Token.connect(ethProvider), l1TokenRebasable: l1TokenRebasable.connect(ethProvider), - tokenRateOracle: tokenRateOracleStub, ...connectBridgeContracts( { - l2Token: optDeployScript.getContractAddress(1), - l2TokenRebasable: optDeployScript.getContractAddress(3), + tokenRateOracle: optDeployScript.getContractAddress(0), + l2Token: optDeployScript.getContractAddress(2), + l2TokenRebasable: optDeployScript.getContractAddress(4), l1ERC20TokenBridge: ethDeployScript.getContractAddress(1), - l2ERC20TokenBridge: optDeployScript.getContractAddress(5), + l2ERC20TokenBridge: optDeployScript.getContractAddress(6) }, ethProvider, optProvider @@ -268,6 +259,7 @@ async function deployTestBridge( function connectBridgeContracts( addresses: { + tokenRateOracle: string; l2Token: string; l2TokenRebasable: string; l1ERC20TokenBridge: string; @@ -276,6 +268,7 @@ function connectBridgeContracts( ethSignerOrProvider: SignerOrProvider, optSignerOrProvider: SignerOrProvider ) { + const l1ERC20TokenBridge = L1ERC20TokenBridge__factory.connect( addresses.l1ERC20TokenBridge, ethSignerOrProvider @@ -292,11 +285,16 @@ function connectBridgeContracts( addresses.l2TokenRebasable, optSignerOrProvider ); + const tokenRateOracle = TokenRateOracle__factory.connect( + addresses.tokenRateOracle, + optSignerOrProvider + ); return { + tokenRateOracle, l2Token, l2TokenRebasable, l1ERC20TokenBridge, - l2ERC20TokenBridge, + l2ERC20TokenBridge }; } From f0b891a0e9f943d2a62cde439932e0cf1cc60d8b Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 9 Jan 2024 10:06:20 +0100 Subject: [PATCH 19/61] add test for push token rate method --- contracts/optimism/L1ERC20TokenBridge.sol | 10 ++- .../bridging-rebase.integration.test.ts | 69 +++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index a475594b..210a11e2 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -53,9 +53,13 @@ contract L1ERC20TokenBridge is l2TokenBridge = l2TokenBridge_; } - function pushTokenRate(uint32 l2Gas_) external { - bytes memory empty = new bytes(0); - _depositERC20To(l1TokenRebasable, l2TokenRebasable, l2TokenBridge, 0, l2Gas_, empty); + /// @notice Pushes token rate to L2 by depositing zero tokens. + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + function pushTokenRate(uint32 l2Gas_) + external + whenDepositsEnabled + { + _depositERC20To(l1TokenRebasable, l2TokenRebasable, l2TokenBridge, 0, l2Gas_, ""); } /// @inheritdoc IL1ERC20Bridge diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 2bc623cb..d5664bc3 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -109,6 +109,75 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); }) + .step("Push token rate to L2", async (ctx) => { + const { + l1Token, + l1TokenRebasable, + l1ERC20TokenBridge, + l2TokenRebasable, + l1CrossDomainMessenger, + l2ERC20TokenBridge, + l1Provider + } = ctx; + + const { l1Stranger } = ctx.accounts; + + const tokenHolderStrangerBalanceBefore = await l1TokenRebasable.balanceOf( + l1Stranger.address + ); + + const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + l1ERC20TokenBridge.address + ); + + const tx = await l1ERC20TokenBridge + .connect(l1Stranger) + .pushTokenRate(200_000); + + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); + + await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + l1TokenRebasable.address, + l2TokenRebasable.address, + l1Stranger.address, + l2ERC20TokenBridge.address, + 0, + dataToSend, + ]); + + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + "finalizeDeposit", + [ + l1TokenRebasable.address, + l2TokenRebasable.address, + l1Stranger.address, + l2ERC20TokenBridge.address, + 0, + dataToSend, + ] + ); + + const messageNonce = await l1CrossDomainMessenger.messageNonce(); + + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ + l2ERC20TokenBridge.address, + l1ERC20TokenBridge.address, + l2DepositCalldata, + messageNonce, + 200_000, + ]); + + assert.equalBN( + await l1Token.balanceOf(l1ERC20TokenBridge.address), + l1ERC20TokenBridgeBalanceBefore + ); + + assert.equalBN( + await l1TokenRebasable.balanceOf(l1Stranger.address), + tokenHolderStrangerBalanceBefore + ); + }) + .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { const { l1Token, From e314bb1695c92d18edba6f4f74d4c20d12f6cbfa Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 9 Jan 2024 14:04:17 +0100 Subject: [PATCH 20/61] unused return values fix --- contracts/optimism/L1ERC20TokenBridge.sol | 3 ++- contracts/optimism/L2ERC20TokenBridge.sol | 1 + contracts/optimism/TokenRateOracle.sol | 3 +-- contracts/token/ERC20Rebasable.sol | 9 +++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 210a11e2..d54f5ea5 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -158,7 +158,7 @@ contract L1ERC20TokenBridge is // maybe loosing 1 wei for stETH. Check another method IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); - IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_); + if(!IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_)) revert ErrorRebasableTokenApprove(); // when 1 wei wasnt't transfer, can this wrap be failed? uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); @@ -212,4 +212,5 @@ contract L1ERC20TokenBridge is } error ErrorSenderNotEOA(); + error ErrorRebasableTokenApprove(); } diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 873e0380..3bf6063e 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -112,6 +112,7 @@ contract L2ERC20TokenBridge is DepositData memory depositData = decodeDepositData(data_); ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2TokenRebasable).tokenRateOracle(); tokenRateOracle.updateRate(depositData.rate, depositData.time); + //slither-disable-next-line unused-return ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index ef5e8dc5..18d42fbc 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -40,14 +40,13 @@ contract TokenRateOracle is ITokenRateOracle { uint80 answeredInRound_ ) { uint80 roundId = uint80(rateL1Timestamp); // TODO: add solt - uint80 answeredInRound = roundId; return ( roundId, int256(tokenRate), rateL1Timestamp, rateL1Timestamp, - answeredInRound + roundId ); } diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index a46fa2da..cdf47609 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -24,6 +24,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met error ErrorAccountIsZeroAddress(); error ErrorDecreasedAllowanceBelowZero(); error ErrorNotBridge(); + error ErrorERC20Transfer(); /// @inheritdoc IERC20BridgedShares address public immutable bridge; @@ -73,9 +74,9 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met /// @inheritdoc IERC20Wrapable function wrap(uint256 sharesAmount_) external returns (uint256) { if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); - + _mintShares(msg.sender, sharesAmount_); - wrappedToken.transferFrom(msg.sender, address(this), sharesAmount_); + if(!wrappedToken.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); return _getTokensByShares(sharesAmount_); } @@ -85,9 +86,8 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met if (tokenAmount_ == 0) revert ErrorZeroTokensUnwrap(); uint256 sharesAmount = _getSharesByTokens(tokenAmount_); - _burnShares(msg.sender, sharesAmount); - wrappedToken.transfer(msg.sender, sharesAmount); + if(!wrappedToken.transfer(msg.sender, sharesAmount)) revert ErrorERC20Transfer(); return sharesAmount; } @@ -281,6 +281,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); + //slither-disable-next-line unused-return (, int256 answer , From 4b56ccf140e9204ac1f6bacb08ad24bcfd880313 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 9 Jan 2024 17:20:22 +0100 Subject: [PATCH 21/61] fix deployment scripts for new oracle and token --- .env.example | 3 +++ .env.wsteth.opt_mainnet | 3 +++ README.md | 9 +++++++-- scripts/optimism/deploy-bridge.ts | 8 ++++---- utils/deployment.ts | 6 ++++-- utils/testing/scenario.ts | 4 ++-- utils/testing/unit.ts | 4 ++-- 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 2f00c733..767859ae 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,9 @@ ETHERSCAN_API_KEY_OPT= # Address of the token to deploy the bridge/gateway for TOKEN= +# Address of the rebasable token to deploy the bridge/gateway for +STETH_TOKEN= + # Name of the network environments used by deployment scripts. # Might be one of: "mainnet", "goerli". NETWORK=mainnet diff --git a/.env.wsteth.opt_mainnet b/.env.wsteth.opt_mainnet index 6bf3ae55..ccb7f7d6 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 +STETH_TOKEN= + # Name of the network environments used by deployment scripts. # Might be one of: "mainnet", "goerli". NETWORK=mainnet diff --git a/README.md b/README.md index 5f9ca478..97b1a883 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ 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. +- [`TOKEN`](#TOKEN) - address of the non-rebasable token to deploy a new bridge on the Ethereum chain. +- [`STETH_TOKEN`] (#STETH_TOKEN) - address of the rebasable token to deploy new bridge on the Ethereum chain. - [`NETWORK`](#NETWORK) - name of the network environments used by deployment scripts. Allowed values: `mainnet`, `goerli`. - [`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. @@ -314,7 +315,11 @@ 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 non-rebasable token to deploy a new bridge on the Ethereum chain. + +#### `STETH_TOKEN` + +Address of the rebasable token to deploy new bridge on the Ethereum chain. #### `NETWORK` diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 633edb29..7680befb 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -25,7 +25,7 @@ async function main() { .deployment(networkName, { logger: console }) .erc20TokenBridgeDeployScript( deploymentConfig.token, - deploymentConfig.token, // FIX + deploymentConfig.stETHToken, { deployer: ethDeployer, admins: { @@ -63,15 +63,15 @@ async function main() { { logger: console } ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 3; + const l2ERC20TokenBridgeProxyDeployStepIndex = 6; const l2BridgingManagement = new BridgingManagement( l2DeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), 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/utils/deployment.ts b/utils/deployment.ts index 617ca5b9..f18aa609 100644 --- a/utils/deployment.ts +++ b/utils/deployment.ts @@ -11,6 +11,7 @@ interface ChainDeploymentConfig extends BridgingManagerSetupConfig { interface MultiChainDeploymentConfig { token: string; + stETHToken: string; l1: ChainDeploymentConfig; l2: ChainDeploymentConfig; } @@ -18,6 +19,7 @@ interface MultiChainDeploymentConfig { export function loadMultiChainDeploymentConfig(): MultiChainDeploymentConfig { return { token: env.address("TOKEN"), + stETHToken: env.address("STETH_TOKEN"), l1: { proxyAdmin: env.address("L1_PROXY_ADMIN"), bridgeAdmin: env.address("L1_BRIDGE_ADMIN"), @@ -49,8 +51,8 @@ export async function printMultiChainDeploymentConfig( l1DeployScript: DeployScript, l2DeployScript: DeployScript ) { - const { token, l1, l2 } = deploymentParams; - console.log(chalk.bold(`${title} :: ${chalk.underline(token)}\n`)); + const { token, stETHToken, l1, l2 } = deploymentParams; + console.log(chalk.bold(`${title} :: ${chalk.underline(token)} :: ${chalk.underline(stETHToken)}\n`)); console.log(chalk.bold(" · L1 Deployment Params:")); await printChainDeploymentConfig(l1Deployer, l1); console.log(); diff --git a/utils/testing/scenario.ts b/utils/testing/scenario.ts index a11ba74f..66998a17 100644 --- a/utils/testing/scenario.ts +++ b/utils/testing/scenario.ts @@ -1,6 +1,6 @@ import { CtxFactory, StepTest, CtxFn } from "./types"; -class ScenarioTest { +class ScenarioTest { private afterFn?: CtxFn; private beforeFn?: CtxFn; @@ -68,6 +68,6 @@ class ScenarioTest { } } -export function scenario(title: string, ctxFactory: CtxFactory) { +export function scenario(title: string, ctxFactory: CtxFactory) { return new ScenarioTest(title, ctxFactory); } diff --git a/utils/testing/unit.ts b/utils/testing/unit.ts index 246a847e..a282e77e 100644 --- a/utils/testing/unit.ts +++ b/utils/testing/unit.ts @@ -1,11 +1,11 @@ import hre from "hardhat"; import { CtxFactory, StepTest, CtxFn } from "./types"; -export function unit(title: string, ctxFactory: CtxFactory) { +export function unit(title: string, ctxFactory: CtxFactory) { return new UnitTest(title, ctxFactory); } -class UnitTest { +class UnitTest { public readonly title: string; private readonly ctxFactory: CtxFactory; From 3b82da7668c72a07c822d7abb28bf5c3a005ff52 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 10 Jan 2024 12:08:51 +0100 Subject: [PATCH 22/61] add hearbeat to oracle --- contracts/optimism/L1ERC20TokenBridge.sol | 5 +---- contracts/optimism/TokenRateOracle.sol | 12 +++++++++++- contracts/token/interfaces/ITokenRateOracle.sol | 4 ++++ test/optimism/TokenRateOracle.unit.test.ts | 3 ++- utils/optimism/deployment.ts | 1 + 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index d54f5ea5..8baa3b56 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -55,10 +55,7 @@ contract L1ERC20TokenBridge is /// @notice Pushes token rate to L2 by depositing zero tokens. /// @param l2Gas_ Gas limit required to complete the deposit on L2. - function pushTokenRate(uint32 l2Gas_) - external - whenDepositsEnabled - { + function pushTokenRate(uint32 l2Gas_) external { _depositERC20To(l1TokenRebasable, l2TokenRebasable, l2TokenBridge, 0, l2Gas_, ""); } diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index 18d42fbc..812e4bac 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -24,11 +24,16 @@ contract TokenRateOracle is ITokenRateOracle { /// @notice An updater which can update oracle. address public immutable tokenRateUpdater; + /// @notice A time period when token rate can be considered outdated. + uint256 public immutable heartbeatPeriodTime; + /// @param bridge_ the bridge address that has a right to updates oracle. /// @param tokenRateUpdater_ address of oracle updater that has a right to updates oracle. - constructor(address bridge_, address tokenRateUpdater_) { + /// @param heartbeatPeriodTime_ time period when token rate can be considered outdated. + constructor(address bridge_, address tokenRateUpdater_, uint256 heartbeatPeriodTime_) { bridge = bridge_; tokenRateUpdater = tokenRateUpdater_; + heartbeatPeriodTime = heartbeatPeriodTime_; } /// @inheritdoc ITokenRateOracle @@ -70,6 +75,11 @@ contract TokenRateOracle is ITokenRateOracle { rateL1Timestamp = rateL1Timestamp_; } + /// @notice Returns flag that shows that token rate can be considered outdated. + function isLikelyOutdated() external view returns (bool) { + return block.timestamp - rateL1Timestamp > heartbeatPeriodTime; + } + /// @dev validates that method called by one of the owners modifier onlyOwner() { if (msg.sender != bridge && msg.sender != tokenRateUpdater) { diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/token/interfaces/ITokenRateOracle.sol index 1c33fc21..2f2d32f6 100644 --- a/contracts/token/interfaces/ITokenRateOracle.sol +++ b/contracts/token/interfaces/ITokenRateOracle.sol @@ -25,11 +25,15 @@ interface ITokenRateOracle { ); /// @notice get the lastest token rate. + /// @return wstETH/stETH token rate. 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); /// @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; } \ No newline at end of file diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index f5981628..95384c96 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -81,7 +81,8 @@ async function ctxFactory() { const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( bridge.address, - updater.address + updater.address, + 86400 ); return { diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index a73a1ed4..dbbd9fba 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -118,6 +118,7 @@ export default function deployment( args: [ expectedL2TokenBridgeProxyAddress, expectedL2TokenBridgeProxyAddress, + 86400, options?.overrides, ], afterDeploy: (c) => From 171db5ab523a34b3865faff2639af5d0aff51ca0 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 22 Jan 2024 10:08:26 +0100 Subject: [PATCH 23/61] pr fixes from first rount review: apply unstructured storage in token, rename consts, rename variables, fix comments --- .solhint.json | 3 +- .../optimism/BridgeableTokensOptimism.sol | 24 +- contracts/optimism/CrossDomainEnabled.sol | 10 +- contracts/optimism/DepositDataCodec.sol | 19 +- contracts/optimism/L1ERC20TokenBridge.sol | 49 +++-- contracts/optimism/L2ERC20TokenBridge.sol | 69 +++--- contracts/optimism/TokenRateOracle.sol | 34 +-- ...rapableStub.sol => ERC20WrappableStub.sol} | 4 +- contracts/stubs/TokenRateOracleStub.sol | 8 +- contracts/token/ERC20Rebasable.sol | 205 ++++++++++-------- contracts/token/UnstructuredRefStorage.sol | 18 ++ contracts/token/UnstructuredStorage.sol | 38 ++++ .../token/interfaces/IERC20BridgedShares.sol | 2 +- ...IERC20Wrapable.sol => IERC20Wrappable.sol} | 6 +- .../optimism.integration.test.ts | 10 +- test/optimism/TokenRateOracle.unit.test.ts | 8 +- .../bridging-rebase.integration.test.ts | 8 +- test/token/ERC20Rebasable.unit.test.ts | 10 +- utils/optimism/testing.ts | 6 +- 19 files changed, 309 insertions(+), 222 deletions(-) rename contracts/stubs/{ERC20WrapableStub.sol => ERC20WrappableStub.sol} (92%) create mode 100644 contracts/token/UnstructuredRefStorage.sol create mode 100644 contracts/token/UnstructuredStorage.sol rename contracts/token/interfaces/{IERC20Wrapable.sol => IERC20Wrappable.sol} (86%) diff --git a/.solhint.json b/.solhint.json index 83b993c5..ff8b9e54 100644 --- a/.solhint.json +++ b/.solhint.json @@ -14,7 +14,6 @@ "ignoreConstructors": true } ], - "lido/fixed-compiler-version": "error", - "const-name-snakecase": false + "lido/fixed-compiler-version": "error" } } diff --git a/contracts/optimism/BridgeableTokensOptimism.sol b/contracts/optimism/BridgeableTokensOptimism.sol index 087c845d..6bf417a3 100644 --- a/contracts/optimism/BridgeableTokensOptimism.sol +++ b/contracts/optimism/BridgeableTokensOptimism.sol @@ -7,31 +7,31 @@ pragma solidity 0.8.10; /// @notice Contains the logic for validation of tokens used in the bridging process contract BridgeableTokensOptimism { /// @notice Address of the bridged non rebasable token in the L1 chain - address public immutable l1TokenNonRebasable; + address public immutable L1_TOKEN_NON_REBASABLE; /// @notice Address of the bridged rebasable token in the L1 chain - address public immutable l1TokenRebasable; + address public immutable L1_TOKEN_REBASABLE; /// @notice Address of the non rebasable token minted on the L2 chain when token bridged - address public immutable l2TokenNonRebasable; + address public immutable L2_TOKEN_NON_REBASABLE; /// @notice Address of the rebasable token minted on the L2 chain when token bridged - address public immutable l2TokenRebasable; + 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_) { - l1TokenNonRebasable = l1TokenNonRebasable_; - l1TokenRebasable = l1TokenRebasable_; - l2TokenNonRebasable = l2TokenNonRebasable_; - l2TokenRebasable = l2TokenRebasable_; + L1_TOKEN_NON_REBASABLE = l1TokenNonRebasable_; + L1_TOKEN_REBASABLE = l1TokenRebasable_; + L2_TOKEN_NON_REBASABLE = l2TokenNonRebasable_; + L2_TOKEN_REBASABLE = l2TokenRebasable_; } /// @dev Validates that passed l1Token_ is supported by the bridge modifier onlySupportedL1Token(address l1Token_) { - if (l1Token_ != l1TokenNonRebasable && l1Token_ != l1TokenRebasable) { + if (l1Token_ != L1_TOKEN_NON_REBASABLE && l1Token_ != L1_TOKEN_REBASABLE) { revert ErrorUnsupportedL1Token(); } _; @@ -39,7 +39,7 @@ contract BridgeableTokensOptimism { /// @dev Validates that passed l2Token_ is supported by the bridge modifier onlySupportedL2Token(address l2Token_) { - if (l2Token_ != l2TokenNonRebasable && l2Token_ != l2TokenRebasable) { + if (l2Token_ != L2_TOKEN_NON_REBASABLE && l2Token_ != L2_TOKEN_REBASABLE) { revert ErrorUnsupportedL2Token(); } _; @@ -54,11 +54,11 @@ contract BridgeableTokensOptimism { } function isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { - return l1Token_ == l1TokenRebasable && l2Token_ == l2TokenRebasable; + return l1Token_ == L1_TOKEN_REBASABLE && l2Token_ == L2_TOKEN_REBASABLE; } function isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { - return l1Token_ == l1TokenNonRebasable && l2Token_ == l2TokenNonRebasable; + return l1Token_ == L1_TOKEN_NON_REBASABLE && l2Token_ == L2_TOKEN_NON_REBASABLE; } error ErrorUnsupportedL1Token(); 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/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol index 55dd9ea8..68ada77b 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/optimism/DepositDataCodec.sol @@ -3,18 +3,23 @@ pragma solidity 0.8.10; +/// @author kovalgek +/// @notice encodes and decodes DepositData for crosschain transfering. contract DepositDataCodec { + + uint8 internal constant RATE_FIELD_SIZE = 12; + uint8 internal constant TIMESTAMP_FIELD_SIZE = 5; struct DepositData { uint96 rate; - uint40 time; + 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.time), + abi.encodePacked(depositData.timestamp), abi.encodePacked(depositData.data) ); return data; @@ -22,18 +27,18 @@ contract DepositDataCodec { function decodeDepositData(bytes calldata buffer) internal pure returns (DepositData memory) { - if (buffer.length < 12 + 5) { + if (buffer.length < RATE_FIELD_SIZE + TIMESTAMP_FIELD_SIZE) { revert ErrorDepositDataLength(); } DepositData memory depositData = DepositData({ - rate: uint96(bytes12(buffer[0:12])), - time: uint40(bytes5(buffer[12:17])), - data: buffer[17:] + 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(); -} \ No newline at end of file +} diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 8baa3b56..1948040c 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -15,11 +15,8 @@ import {BridgeableTokensOptimism} from "./BridgeableTokensOptimism.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; -import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.sol"; -// Check if Optimism changed API for bridges. They could deprecate methods. -// Optimise gas usage with data transfer. Maybe cache rate and see if it changed. - /// @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 @@ -33,8 +30,7 @@ contract L1ERC20TokenBridge is { using SafeERC20 for IERC20; - /// @inheritdoc IL1ERC20Bridge - address public immutable l2TokenBridge; + address public immutable L2_TOKEN_BRIDGE; /// @param messenger_ L1 messenger address being used for cross-chain communications /// @param l2TokenBridge_ Address of the corresponding L2 bridge @@ -50,13 +46,18 @@ contract L1ERC20TokenBridge is address l2TokenNonRebasable_, address l2TokenRebasable_ ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { - l2TokenBridge = l2TokenBridge_; + L2_TOKEN_BRIDGE = l2TokenBridge_; } /// @notice Pushes token rate to L2 by depositing zero tokens. /// @param l2Gas_ Gas limit required to complete the deposit on L2. function pushTokenRate(uint32 l2Gas_) external { - _depositERC20To(l1TokenRebasable, l2TokenRebasable, l2TokenBridge, 0, l2Gas_, ""); + _depositERC20To(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, L2_TOKEN_BRIDGE, 0, l2Gas_, ""); + } + + /// @inheritdoc IL1ERC20Bridge + function l2TokenBridge() external view returns (address) { + return L2_TOKEN_BRIDGE; } /// @inheritdoc IL1ERC20Bridge @@ -75,7 +76,7 @@ contract L1ERC20TokenBridge is if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } - + _depositERC20To(l1Token_, l2Token_, msg.sender, amount_, l2Gas_, data_); } @@ -110,13 +111,13 @@ contract L1ERC20TokenBridge is whenWithdrawalsEnabled onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) - onlyFromCrossDomainAccount(l2TokenBridge) + onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) { if (isRebasableTokenFlow(l1Token_, l2Token_)) { - uint256 stETHAmount = IERC20Wrapable(l1TokenNonRebasable).unwrap(amount_); - IERC20(l1TokenRebasable).safeTransfer(to_, stETHAmount); + uint256 stETHAmount = IERC20Wrappable(L1_TOKEN_NON_REBASABLE).unwrap(amount_); + IERC20(L1_TOKEN_REBASABLE).safeTransfer(to_, stETHAmount); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenNonRebasable).safeTransfer(to_, amount_); + IERC20(L1_TOKEN_NON_REBASABLE).safeTransfer(to_, amount_); } emit ERC20WithdrawalFinalized( @@ -140,8 +141,8 @@ contract L1ERC20TokenBridge is if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ - rate: uint96(IERC20Wrapable(l1TokenNonRebasable).stETHPerToken()), - time: uint40(block.timestamp), + rate: uint96(IERC20Wrappable(L1_TOKEN_NON_REBASABLE).stETHPerToken()), + timestamp: uint40(block.timestamp), data: data_ }); @@ -152,18 +153,18 @@ contract L1ERC20TokenBridge is _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); return; } - + // maybe loosing 1 wei for stETH. Check another method - IERC20(l1TokenRebasable).safeTransferFrom(msg.sender, address(this), amount_); - if(!IERC20(l1TokenRebasable).approve(l1TokenNonRebasable, amount_)) revert ErrorRebasableTokenApprove(); + IERC20(L1_TOKEN_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); + if(!IERC20(L1_TOKEN_REBASABLE).approve(L1_TOKEN_NON_REBASABLE, amount_)) revert ErrorRebasableTokenApprove(); // when 1 wei wasnt't transfer, can this wrap be failed? - uint256 wstETHAmount = IERC20Wrapable(l1TokenNonRebasable).wrap(amount_); - _initiateERC20Deposit(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); + uint256 wstETHAmount = IERC20Wrappable(L1_TOKEN_NON_REBASABLE).wrap(amount_); + _initiateERC20Deposit(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(l1TokenNonRebasable).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l2Gas_, data_); + IERC20(L1_TOKEN_NON_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); + _initiateERC20Deposit(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, l2Gas_, data_); } } @@ -195,8 +196,8 @@ contract L1ERC20TokenBridge is amount_, data_ ); - - sendCrossDomainMessage(l2TokenBridge, l2Gas_, message); + + sendCrossDomainMessage(L2_TOKEN_BRIDGE, l2Gas_, message); emit ERC20DepositInitiated( l1Token_, diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 3bf6063e..a5cfb46d 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -8,7 +8,7 @@ import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; -import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -32,8 +32,7 @@ contract L2ERC20TokenBridge is { using SafeERC20 for IERC20; - /// @inheritdoc IL2ERC20Bridge - address public immutable l1TokenBridge; + address public immutable L1_TOKEN_BRIDGE; /// @param messenger_ L2 messenger address being used for cross-chain communications /// @param l1TokenBridge_ Address of the corresponding L1 bridge @@ -49,7 +48,12 @@ contract L2ERC20TokenBridge is address l2TokenNonRebasable_, address l2TokenRebasable_ ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { - l1TokenBridge = l1TokenBridge_; + L1_TOKEN_BRIDGE = l1TokenBridge_; + } + + /// @inheritdoc IL2ERC20Bridge + function l1TokenBridge() external view returns (address) { + return L1_TOKEN_BRIDGE; } /// @inheritdoc IL2ERC20Bridge @@ -69,30 +73,10 @@ contract L2ERC20TokenBridge is uint256 amount_, uint32 l1Gas_, bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { + ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { _withdrawTo(l2Token_, to_, amount_, l1Gas_, data_); } - function _withdrawTo( - address l2Token_, - address to_, - uint256 amount_, - uint32 l1Gas_, - bytes calldata data_ - ) internal { - if (l2Token_ == l2TokenRebasable) { - // maybe loosing 1 wei her as well - uint256 shares = ERC20Rebasable(l2TokenRebasable).getSharesByTokens(amount_); - ERC20Rebasable(l2TokenRebasable).burnShares(msg.sender, shares); - _initiateWithdrawal(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, shares, l1Gas_, data_); - emit WithdrawalInitiated(l1TokenRebasable, l2TokenRebasable, msg.sender, to_, amount_, data_); - } else if (l2Token_ == l2TokenNonRebasable) { - IERC20Bridged(l2TokenNonRebasable).bridgeBurn(msg.sender, amount_); - _initiateWithdrawal(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, l1Gas_, data_); - emit WithdrawalInitiated(l1TokenNonRebasable, l2TokenNonRebasable, msg.sender, to_, amount_, data_); - } - } - /// @inheritdoc IL2ERC20Bridge function finalizeDeposit( address l1Token_, @@ -106,21 +90,42 @@ contract L2ERC20TokenBridge is whenDepositsEnabled onlySupportedL1Token(l1Token_) onlySupportedL2Token(l2Token_) - onlyFromCrossDomainAccount(l1TokenBridge) + onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) { if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); - ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2TokenRebasable).tokenRateOracle(); - tokenRateOracle.updateRate(depositData.rate, depositData.time); + ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); + tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); //slither-disable-next-line unused-return - ERC20Rebasable(l2TokenRebasable).mintShares(to_, amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, depositData.data); + ERC20Rebasable(L2_TOKEN_REBASABLE).mintShares(to_, amount_); + uint256 rebasableTokenAmount = ERC20Rebasable(L2_TOKEN_REBASABLE).getTokensByShares(amount_); + emit DepositFinalized(l1Token_, l2Token_, from_, to_, rebasableTokenAmount, depositData.data); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20Bridged(l2TokenNonRebasable).bridgeMint(to_, amount_); + IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeMint(to_, amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); } } + function _withdrawTo( + address l2Token_, + address to_, + uint256 amount_, + uint32 l1Gas_, + bytes calldata data_ + ) internal { + if (l2Token_ == L2_TOKEN_REBASABLE) { + // maybe loosing 1 wei here as well + uint256 shares = ERC20Rebasable(L2_TOKEN_REBASABLE).getSharesByTokens(amount_); + ERC20Rebasable(L2_TOKEN_REBASABLE).burnShares(msg.sender, shares); + _initiateWithdrawal(L2_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, shares, l1Gas_, data_); + emit WithdrawalInitiated(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, amount_, data_); + } else if (l2Token_ == L2_TOKEN_NON_REBASABLE) { + IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeBurn(msg.sender, amount_); + _initiateWithdrawal(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, l1Gas_, data_); + emit WithdrawalInitiated(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, 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 @@ -150,6 +155,6 @@ contract L2ERC20TokenBridge is data_ ); - sendCrossDomainMessage(l1TokenBridge, l1Gas_, message); + sendCrossDomainMessage(L1_TOKEN_BRIDGE, l1Gas_, message); } } diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index 812e4bac..e9a1290d 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -9,31 +9,28 @@ import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; /// @notice Oracle for storing token rate. contract TokenRateOracle is ITokenRateOracle { - error NotAnOwner(address caller); - error IncorrectRateTimestamp(); - /// @notice wstETH/stETH token rate. uint256 private tokenRate; - /// @notice L1 time when token rate was pushed. + /// @notice L1 time when token rate was pushed. uint256 private rateL1Timestamp; /// @notice A bridge which can update oracle. - address public immutable bridge; + address public immutable BRIDGE; /// @notice An updater which can update oracle. - address public immutable tokenRateUpdater; + address public immutable TOKEN_RATE_UPDATER; /// @notice A time period when token rate can be considered outdated. - uint256 public immutable heartbeatPeriodTime; + uint256 public immutable RATE_VALIDITY_PERIOD; /// @param bridge_ the bridge address that has a right to updates oracle. /// @param tokenRateUpdater_ address of oracle updater that has a right to updates oracle. - /// @param heartbeatPeriodTime_ time period when token rate can be considered outdated. - constructor(address bridge_, address tokenRateUpdater_, uint256 heartbeatPeriodTime_) { - bridge = bridge_; - tokenRateUpdater = tokenRateUpdater_; - heartbeatPeriodTime = heartbeatPeriodTime_; + /// @param rateValidityPeriod_ time period when token rate can be considered outdated. + constructor(address bridge_, address tokenRateUpdater_, uint256 rateValidityPeriod_) { + BRIDGE = bridge_; + TOKEN_RATE_UPDATER = tokenRateUpdater_; + RATE_VALIDITY_PERIOD = rateValidityPeriod_; } /// @inheritdoc ITokenRateOracle @@ -69,7 +66,7 @@ contract TokenRateOracle is ITokenRateOracle { function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external onlyOwner { // reject rates from the future if (rateL1Timestamp_ < rateL1Timestamp) { - revert IncorrectRateTimestamp(); + revert ErrorIncorrectRateTimestamp(); } tokenRate = tokenRate_; rateL1Timestamp = rateL1Timestamp_; @@ -77,14 +74,17 @@ contract TokenRateOracle is ITokenRateOracle { /// @notice Returns flag that shows that token rate can be considered outdated. function isLikelyOutdated() external view returns (bool) { - return block.timestamp - rateL1Timestamp > heartbeatPeriodTime; + return block.timestamp - rateL1Timestamp > RATE_VALIDITY_PERIOD; } /// @dev validates that method called by one of the owners modifier onlyOwner() { - if (msg.sender != bridge && msg.sender != tokenRateUpdater) { - revert NotAnOwner(msg.sender); + if (msg.sender != BRIDGE && msg.sender != TOKEN_RATE_UPDATER) { + revert ErrorNotAnOwner(msg.sender); } _; } -} \ No newline at end of file + + error ErrorNotAnOwner(address caller); + error ErrorIncorrectRateTimestamp(); +} diff --git a/contracts/stubs/ERC20WrapableStub.sol b/contracts/stubs/ERC20WrappableStub.sol similarity index 92% rename from contracts/stubs/ERC20WrapableStub.sol rename to contracts/stubs/ERC20WrappableStub.sol index 3f77b88c..f1796105 100644 --- a/contracts/stubs/ERC20WrapableStub.sol +++ b/contracts/stubs/ERC20WrappableStub.sol @@ -6,10 +6,10 @@ pragma solidity 0.8.10; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Wrapable} from "../token/interfaces/IERC20Wrapable.sol"; +import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.sol"; // represents wstETH on L1 -contract ERC20WrapableStub is IERC20Wrapable, ERC20 { +contract ERC20WrappableStub is IERC20Wrappable, ERC20 { IERC20 public stETH; address public bridge; diff --git a/contracts/stubs/TokenRateOracleStub.sol b/contracts/stubs/TokenRateOracleStub.sol index 6581e512..25ab6763 100644 --- a/contracts/stubs/TokenRateOracleStub.sol +++ b/contracts/stubs/TokenRateOracleStub.sol @@ -29,9 +29,6 @@ contract TokenRateOracleStub is ITokenRateOracle { latestRoundDataUpdatedAt = updatedAt_; } - /** - * @notice get data about the latest round. - */ function latestRoundData() external view @@ -56,8 +53,7 @@ contract TokenRateOracleStub is ITokenRateOracle { } function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { - // check timestamp not late as current one. - latestRoundDataAnswer = tokenRate_; - latestRoundDataUpdatedAt = rateL1Timestamp_; + latestRoundDataAnswer = tokenRate_; + latestRoundDataUpdatedAt = rateL1Timestamp_; } } \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index cdf47609..22372f5e 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -4,45 +4,37 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Wrapable} from "./interfaces/IERC20Wrapable.sol"; +import {IERC20Wrappable} from "./interfaces/IERC20Wrappable.sol"; import {IERC20BridgedShares} from "./interfaces/IERC20BridgedShares.sol"; import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; +import {UnstructuredRefStorage} from "./UnstructuredRefStorage.sol"; +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; /// @author kovalgek -/// @notice Extends the ERC20Shared functionality -contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Metadata { +/// @notice Rebasable token that wraps/unwraps non-rebasable token and allow to mint/burn tokens by bridge. +contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Metadata { - error ErrorZeroSharesWrap(); - error ErrorZeroTokensUnwrap(); - error ErrorInvalidRateDecimals(uint8); - error ErrorWrongOracleUpdateTime(); - error ErrorOracleAnswerIsNegative(); - error ErrorTrasferToRebasableContract(); - error ErrorNotEnoughBalance(); - error ErrorNotEnoughAllowance(); - error ErrorAccountIsZeroAddress(); - error ErrorDecreasedAllowanceBelowZero(); - error ErrorNotBridge(); - error ErrorERC20Transfer(); + using UnstructuredRefStorage for bytes32; + using UnstructuredStorage for bytes32; /// @inheritdoc IERC20BridgedShares - address public immutable bridge; + address public immutable BRIDGE; /// @notice Contract of non-rebasable token to wrap. - IERC20 public immutable wrappedToken; + IERC20 public immutable WRAPPED_TOKEN; /// @notice Oracle contract used to get token rate for wrapping/unwrapping tokens. - ITokenRateOracle public immutable tokenRateOracle; + ITokenRateOracle public immutable TOKEN_RATE_ORACLE; - /// @inheritdoc IERC20 - mapping(address => mapping(address => uint256)) public allowance; + /// @dev token allowance slot position. + bytes32 internal constant TOKEN_ALLOWANCE_POSITION = keccak256("ERC20Rebasable.TOKEN_ALLOWANCE_POSITION"); - /// @notice Basic unit representing the token holder's share in the total amount of ether controlled by the protocol. - mapping (address => uint256) private shares; + /// @dev user shares slot position. + bytes32 internal constant SHARES_POSITION = keccak256("ERC20Rebasable.SHARES_POSITION"); - /// @notice The total amount of shares in existence. - uint256 private totalShares; + /// @dev token shares slot position. + bytes32 internal constant TOTAL_SHARES_POSITION = keccak256("ERC20Rebasable.TOTAL_SHARES_POSITION"); /// @param name_ The name of the token /// @param symbol_ The symbol of the token @@ -58,9 +50,9 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met address tokenRateOracle_, address bridge_ ) ERC20Metadata(name_, symbol_, decimals_) { - wrappedToken = IERC20(wrappedToken_); - tokenRateOracle = ITokenRateOracle(tokenRateOracle_); - bridge = bridge_; + WRAPPED_TOKEN = IERC20(wrappedToken_); + TOKEN_RATE_ORACLE = ITokenRateOracle(tokenRateOracle_); + BRIDGE = bridge_; } /// @notice Sets the name and the symbol of the tokens if they both are empty @@ -71,30 +63,30 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met _setERC20MetadataSymbol(symbol_); } - /// @inheritdoc IERC20Wrapable + /// @inheritdoc IERC20Wrappable function wrap(uint256 sharesAmount_) external returns (uint256) { if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); _mintShares(msg.sender, sharesAmount_); - if(!wrappedToken.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); + if(!WRAPPED_TOKEN.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); return _getTokensByShares(sharesAmount_); } - /// @inheritdoc IERC20Wrapable + /// @inheritdoc IERC20Wrappable function unwrap(uint256 tokenAmount_) external returns (uint256) { if (tokenAmount_ == 0) revert ErrorZeroTokensUnwrap(); uint256 sharesAmount = _getSharesByTokens(tokenAmount_); _burnShares(msg.sender, sharesAmount); - if(!wrappedToken.transfer(msg.sender, sharesAmount)) revert ErrorERC20Transfer(); + if(!WRAPPED_TOKEN.transfer(msg.sender, sharesAmount)) revert ErrorERC20Transfer(); return sharesAmount; } - /// @inheritdoc IERC20Wrapable + /// @inheritdoc IERC20Wrappable function stETHPerToken() external view returns (uint256) { - return uint256(tokenRateOracle.latestAnswer()); + return uint256(TOKEN_RATE_ORACLE.latestAnswer()); } /// @inheritdoc IERC20BridgedShares @@ -107,17 +99,14 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met _burnShares(account_, amount_); } - /// @dev Validates that sender of the transaction is the bridge - modifier onlyBridge() { - if (msg.sender != bridge) { - revert ErrorNotBridge(); - } - _; + /// @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(totalShares); + return _getTokensByShares(_getTotalShares()); } /// @inheritdoc IERC20 @@ -125,6 +114,32 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met 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 @@ -161,19 +176,19 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met _approve( msg.sender, spender_, - allowance[msg.sender][spender_] + addedValue_ + _getTokenAllowance()[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 + /// @param subtractedValue_ An amount to decrease the allowance function decreaseAllowance(address spender_, uint256 subtractedValue_) external returns (bool) { - uint256 currentAllowance = allowance[msg.sender][spender_]; + uint256 currentAllowance = _getTokenAllowance()[msg.sender][spender_]; if (currentAllowance < subtractedValue_) { revert ErrorDecreasedAllowanceBelowZero(); } @@ -183,6 +198,25 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met 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 @@ -200,14 +234,14 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met /// @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 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 = allowance[owner_][spender_]; + uint256 currentAllowance = _getTokenAllowance()[owner_][spender_]; if (currentAllowance == type(uint256).max) { return; } @@ -221,49 +255,19 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met /// @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 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_) { - allowance[owner_][spender_] = amount_; + _getTokenAllowance()[owner_][spender_] = amount_; emit Approval(owner_, spender_, amount_); } - /// @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_); - } - function _sharesOf(address account_) internal view returns (uint256) { - return shares[account_]; - } - - function _getTotalShares() internal view returns (uint256) { - return totalShares; + return _getShares()[account_]; } function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { @@ -277,7 +281,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met } function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { - uint8 rateDecimals = tokenRateOracle.decimals(); + uint8 rateDecimals = TOKEN_RATE_ORACLE.decimals(); if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); @@ -287,7 +291,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met , , uint256 updatedAt - ,) = tokenRateOracle.latestRoundData(); + ,) = TOKEN_RATE_ORACLE.latestRoundData(); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); if (answer <= 0) revert ErrorOracleAnswerIsNegative(); @@ -302,10 +306,10 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met address recipient_, uint256 amount_ ) internal onlyNonZeroAccount(recipient_) returns (uint256) { - totalShares = totalShares + amount_; - shares[recipient_] = shares[recipient_] + amount_; + _setTotalShares(_getTotalShares() + amount_); + _getShares()[recipient_] = _getShares()[recipient_] + amount_; emit Transfer(address(0), recipient_, amount_); - return totalShares; + return _getTotalShares(); } /// @dev Destroys amount_ shares from account_, reducing the total shares supply. @@ -315,12 +319,12 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met address account_, uint256 amount_ ) internal onlyNonZeroAccount(account_) returns (uint256) { - uint256 accountShares = shares[account_]; + uint256 accountShares = _getShares()[account_]; if (accountShares < amount_) revert ErrorNotEnoughBalance(); - totalShares = totalShares - amount_; - shares[account_] = accountShares - amount_; + _setTotalShares(_getTotalShares() - amount_); + _getShares()[account_] = accountShares - amount_; emit Transfer(account_, address(0), amount_); - return totalShares; + return _getTotalShares(); } /// @dev Moves `sharesAmount_` shares from `sender_` to `recipient_`. @@ -335,11 +339,11 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met if (recipient_ == address(this)) revert ErrorTrasferToRebasableContract(); - uint256 currentSenderShares = shares[sender_]; + uint256 currentSenderShares = _getShares()[sender_]; if (sharesAmount_ > currentSenderShares) revert ErrorNotEnoughBalance(); - shares[sender_] = currentSenderShares - sharesAmount_; - shares[recipient_] = shares[recipient_] + sharesAmount_; + _getShares()[sender_] = currentSenderShares - sharesAmount_; + _getShares()[recipient_] = _getShares()[recipient_] + sharesAmount_; } /// @dev validates that account_ is not zero address @@ -349,4 +353,25 @@ contract ERC20Rebasable is IERC20, IERC20Wrapable, IERC20BridgedShares, ERC20Met } _; } + + /// @dev Validates that sender of the transaction is the bridge + modifier onlyBridge() { + if (msg.sender != BRIDGE) { + revert ErrorNotBridge(); + } + _; + } + + error ErrorZeroSharesWrap(); + error ErrorZeroTokensUnwrap(); + error ErrorInvalidRateDecimals(uint8); + error ErrorWrongOracleUpdateTime(); + error ErrorOracleAnswerIsNegative(); + error ErrorTrasferToRebasableContract(); + error ErrorNotEnoughBalance(); + error ErrorNotEnoughAllowance(); + error ErrorAccountIsZeroAddress(); + error ErrorDecreasedAllowanceBelowZero(); + error ErrorNotBridge(); + error ErrorERC20Transfer(); } \ No newline at end of file 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/IERC20BridgedShares.sol b/contracts/token/interfaces/IERC20BridgedShares.sol index 11f3ba78..2b95cb1a 100644 --- a/contracts/token/interfaces/IERC20BridgedShares.sol +++ b/contracts/token/interfaces/IERC20BridgedShares.sol @@ -9,7 +9,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens interface IERC20BridgedShares is IERC20 { /// @notice Returns bridge which can mint and burn tokens on L2 - function bridge() external view returns (address); + 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 diff --git a/contracts/token/interfaces/IERC20Wrapable.sol b/contracts/token/interfaces/IERC20Wrappable.sol similarity index 86% rename from contracts/token/interfaces/IERC20Wrapable.sol rename to contracts/token/interfaces/IERC20Wrappable.sol index 0fb2d2dc..809e3c82 100644 --- a/contracts/token/interfaces/IERC20Wrapable.sol +++ b/contracts/token/interfaces/IERC20Wrappable.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.10; /// @author kovalgek /// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens -interface IERC20Wrapable { +interface IERC20Wrappable { /** * @notice Exchanges wstETH to stETH @@ -22,13 +22,13 @@ interface IERC20Wrapable { /** * @notice Exchanges stETH to wstETH - * @param wrapableTokenAmount_ amount of stETH to uwrap in exchange for wstETH + * @param wrappableTokenAmount_ amount of stETH to uwrap in exchange for wstETH * @dev Requirements: * - `stETHAmount_` must be non-zero * - msg.sender must have at least `stETHAmount_` stETH. * @return Amount of wstETH user receives after unwrap */ - function unwrap(uint256 wrapableTokenAmount_) external returns (uint256); + function unwrap(uint256 wrappableTokenAmount_) external returns (uint256); /** * @notice Get amount of wstETH for a one stETH diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index ab28543e..ab3bc69d 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -7,6 +7,7 @@ import { ERC20Bridged__factory, ERC20Rebasable__factory, TokenRateOracle__factory, + ERC20WrappableStub__factory, } from "../../typechain"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; @@ -214,13 +215,12 @@ async function ctxFactory() { "TT" ); - const l1TokenRebasable = await new ERC20Rebasable__factory(l1Deployer).deploy( - "Test Token Rebasable", - "TTR" + const l1TokenRebasable = await new ERC20WrappableStub__factory(l1Deployer).deploy( + l1Token.address, + "Test Token", + "TT" ); - const tokenRateOracleStub = await new TokenRateOracle__factory(l2Deployer).deploy(); - const optAddresses = optimism.addresses(networkName); const govBridgeExecutor = testingOnDeployedContracts diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index 95384c96..42e42cf5 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -9,8 +9,8 @@ unit("TokenRateOracle", ctxFactory) const { tokenRateOracle } = ctx.contracts; const { bridge, updater } = ctx.accounts; - assert.equal(await tokenRateOracle.bridge(), bridge.address); - assert.equal(await tokenRateOracle.tokenRateUpdater(), updater.address); + assert.equal(await tokenRateOracle.BRIDGE(), bridge.address); + assert.equal(await tokenRateOracle.TOKEN_RATE_UPDATER(), updater.address); assert.equalBN(await tokenRateOracle.latestAnswer(), 0); @@ -35,7 +35,7 @@ unit("TokenRateOracle", ctxFactory) const { bridge, updater, stranger } = ctx.accounts; tokenRateOracle.connect(bridge).updateRate(10, 20); tokenRateOracle.connect(updater).updateRate(10, 23); - await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "NotAnOwner(\""+stranger.address+"\")"); + await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "ErrorNotAnOwner(\""+stranger.address+"\")"); }) .test("incorrect time", async (ctx) => { @@ -43,7 +43,7 @@ unit("TokenRateOracle", ctxFactory) const { bridge } = ctx.accounts; tokenRateOracle.connect(bridge).updateRate(10, 1000); - await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "IncorrectRateTimestamp()"); + await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "ErrorIncorrectRateTimestamp()"); }) .test("state after update token rate", async (ctx) => { diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index d5664bc3..77f89921 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -7,7 +7,7 @@ import testing, { scenario } from "../../utils/testing"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { ERC20WrapableStub } from "../../typechain"; +import { ERC20WrappableStub } from "../../typechain"; scenario("Optimism :: Bridging integration test", ctxFactory) .after(async (ctx) => { @@ -442,7 +442,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - depositAmountNonRebasable, + depositAmountRebasable, "0x", ]); @@ -700,7 +700,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderB.address, - depositAmountNonRebasable, + depositAmountRebasable, "0x", ]); @@ -917,7 +917,7 @@ async function ctxFactory() { }; } -async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapableStub) { +async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrappableStub) { const stETHPerToken = await l1Token.stETHPerToken(); const blockNumber = await l1Provider.getBlockNumber(); const blockTimestamp = (await l1Provider.getBlock(blockNumber)).timestamp; diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index f429ebbd..a7049144 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -87,7 +87,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).unwrap(0), "ErrorZeroTokensUnwrap()"); }) - .test("wrap() positive scenario", async (ctx) => { + .test("wrap() happy path", async (ctx) => { const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; @@ -170,7 +170,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNegative()"); }) - .test("unwrap() positive scenario", async (ctx) => { + .test("unwrap() happy path", async (ctx) => { const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; @@ -261,7 +261,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`4 ether`), "ErrorNotEnoughBalance()"); }) - .test("mintShares() positive scenario", async (ctx) => { + .test("mintShares() happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const {user1, user2, owner } = ctx.accounts; @@ -308,9 +308,9 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted).add(user2TokensMinted)); }) - .test("burnShares() positive scenario", async (ctx) => { + .test("burnShares() happy path", async (ctx) => { - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + const { rebasableProxied } = ctx.contracts; const {user1, user2, owner } = ctx.accounts; const { rate, decimals, premintShares, premintTokens } = ctx.constants; diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 1da87bb5..bc0380af 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -9,7 +9,7 @@ import { L2ERC20TokenBridge, ERC20Bridged__factory, ERC20BridgedStub__factory, - ERC20WrapableStub__factory, + ERC20WrappableStub__factory, TokenRateOracle__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, @@ -156,7 +156,7 @@ async function loadDeployedBridges( l2SignerOrProvider: SignerOrProvider ) { return { - l1Token: ERC20WrapableStub__factory.connect( + l1Token: ERC20WrappableStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), @@ -192,7 +192,7 @@ async function deployTestBridge( "TTR" ); - const l1Token = await new ERC20WrapableStub__factory(ethDeployer).deploy( + const l1Token = await new ERC20WrappableStub__factory(ethDeployer).deploy( l1TokenRebasable.address, "Test Token", "TT" From 76b4ff3e3077c51d5af05614c6f11a26e81e373b Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 29 Jan 2024 15:45:33 +0100 Subject: [PATCH 24/61] PR fixes: fix comments, rename interface, abstract wst --- .editorconfig | 15 ++++ contracts/optimism/L1ERC20TokenBridge.sol | 32 ++++---- contracts/optimism/L2ERC20TokenBridge.sol | 18 ++--- ...sol => RebasableAndNonRebasableTokens.sol} | 2 +- contracts/optimism/TokenRateOracle.sol | 52 +++++++------ ...WrappableStub.sol => ERC20WrapperStub.sol} | 4 +- contracts/token/ERC20Rebasable.sol | 41 +++++----- .../token/L1TokenNonRebasableAdapter.sol | 23 ++++++ .../token/interfaces/IERC20BridgedShares.sol | 18 ++--- .../token/interfaces/IERC20TokenRate.sol | 12 +++ .../token/interfaces/IERC20Wrappable.sol | 38 ---------- contracts/token/interfaces/IERC20Wrapper.sol | 19 +++++ contracts/token/interfaces/IERC20WstETH.sol | 14 ++++ .../optimism.integration.test.ts | 4 +- test/optimism/L2ERC20TokenBridge.unit.test.ts | 6 +- test/optimism/TokenRateOracle.unit.test.ts | 14 ++-- .../bridging-rebase.integration.test.ts | 46 ++++++------ test/token/ERC20Rebasable.unit.test.ts | 74 +++++++++---------- utils/optimism/testing.ts | 6 +- 19 files changed, 236 insertions(+), 202 deletions(-) create mode 100644 .editorconfig rename contracts/optimism/{BridgeableTokensOptimism.sol => RebasableAndNonRebasableTokens.sol} (98%) rename contracts/stubs/{ERC20WrappableStub.sol => ERC20WrapperStub.sol} (92%) create mode 100644 contracts/token/L1TokenNonRebasableAdapter.sol create mode 100644 contracts/token/interfaces/IERC20TokenRate.sol delete mode 100644 contracts/token/interfaces/IERC20Wrappable.sol create mode 100644 contracts/token/interfaces/IERC20Wrapper.sol create mode 100644 contracts/token/interfaces/IERC20WstETH.sol 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/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 1948040c..61cdb73a 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -9,14 +9,13 @@ 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 {L1TokenNonRebasableAdapter} from "../token/L1TokenNonRebasableAdapter.sol"; import {BridgingManager} from "../BridgingManager.sol"; -import {BridgeableTokensOptimism} from "./BridgeableTokensOptimism.sol"; +import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; -import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.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 @@ -24,7 +23,7 @@ import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.sol"; contract L1ERC20TokenBridge is IL1ERC20Bridge, BridgingManager, - BridgeableTokensOptimism, + RebasableAndNonRebasableTokens, CrossDomainEnabled, DepositDataCodec { @@ -32,6 +31,8 @@ contract L1ERC20TokenBridge is address public immutable L2_TOKEN_BRIDGE; + L1TokenNonRebasableAdapter public immutable L1_TOKEN_NON_REBASABLE_ADAPTER; + /// @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 @@ -45,8 +46,9 @@ contract L1ERC20TokenBridge is address l1TokenRebasable_, address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { L2_TOKEN_BRIDGE = l2TokenBridge_; + L1_TOKEN_NON_REBASABLE_ADAPTER = new L1TokenNonRebasableAdapter(l1TokenNonRebasable_); } /// @notice Pushes token rate to L2 by depositing zero tokens. @@ -114,20 +116,13 @@ contract L1ERC20TokenBridge is onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) { if (isRebasableTokenFlow(l1Token_, l2Token_)) { - uint256 stETHAmount = IERC20Wrappable(L1_TOKEN_NON_REBASABLE).unwrap(amount_); + uint256 stETHAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); IERC20(L1_TOKEN_REBASABLE).safeTransfer(to_, stETHAmount); + emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(L1_TOKEN_NON_REBASABLE).safeTransfer(to_, amount_); + emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); } - - emit ERC20WithdrawalFinalized( - l1Token_, - l2Token_, - from_, - to_, - amount_, - data_ - ); } function _depositERC20To( @@ -141,14 +136,13 @@ contract L1ERC20TokenBridge is if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ - rate: uint96(IERC20Wrappable(L1_TOKEN_NON_REBASABLE).stETHPerToken()), + rate: uint96(L1_TOKEN_NON_REBASABLE_ADAPTER.tokenRate()), timestamp: uint40(block.timestamp), data: data_ }); bytes memory encodedDepositData = encodeDepositData(depositData); - // probably need to add a new method for amount zero if (amount_ == 0) { _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); return; @@ -159,7 +153,7 @@ contract L1ERC20TokenBridge is if(!IERC20(L1_TOKEN_REBASABLE).approve(L1_TOKEN_NON_REBASABLE, amount_)) revert ErrorRebasableTokenApprove(); // when 1 wei wasnt't transfer, can this wrap be failed? - uint256 wstETHAmount = IERC20Wrappable(L1_TOKEN_NON_REBASABLE).wrap(amount_); + uint256 wstETHAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).wrap(amount_); _initiateERC20Deposit(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index a5cfb46d..f939eb62 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -3,17 +3,18 @@ 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 {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; -import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; -import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; +import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; import {BridgingManager} from "../BridgingManager.sol"; -import {BridgeableTokensOptimism} from "./BridgeableTokensOptimism.sol"; +import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; @@ -26,7 +27,7 @@ import {DepositDataCodec} from "./DepositDataCodec.sol"; contract L2ERC20TokenBridge is IL2ERC20Bridge, BridgingManager, - BridgeableTokensOptimism, + RebasableAndNonRebasableTokens, CrossDomainEnabled, DepositDataCodec { @@ -47,7 +48,7 @@ contract L2ERC20TokenBridge is address l1TokenRebasable_, address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) BridgeableTokensOptimism(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { L1_TOKEN_BRIDGE = l1TokenBridge_; } @@ -96,7 +97,6 @@ contract L2ERC20TokenBridge is DepositData memory depositData = decodeDepositData(data_); ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); - //slither-disable-next-line unused-return ERC20Rebasable(L2_TOKEN_REBASABLE).mintShares(to_, amount_); uint256 rebasableTokenAmount = ERC20Rebasable(L2_TOKEN_REBASABLE).getTokensByShares(amount_); emit DepositFinalized(l1Token_, l2Token_, from_, to_, rebasableTokenAmount, depositData.data); @@ -114,7 +114,7 @@ contract L2ERC20TokenBridge is bytes calldata data_ ) internal { if (l2Token_ == L2_TOKEN_REBASABLE) { - // maybe loosing 1 wei here as well + // TODO: maybe loosing 1 wei here as well uint256 shares = ERC20Rebasable(L2_TOKEN_REBASABLE).getSharesByTokens(amount_); ERC20Rebasable(L2_TOKEN_REBASABLE).burnShares(msg.sender, shares); _initiateWithdrawal(L2_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, shares, l1Gas_, data_); diff --git a/contracts/optimism/BridgeableTokensOptimism.sol b/contracts/optimism/RebasableAndNonRebasableTokens.sol similarity index 98% rename from contracts/optimism/BridgeableTokensOptimism.sol rename to contracts/optimism/RebasableAndNonRebasableTokens.sol index 6bf417a3..a7cca519 100644 --- a/contracts/optimism/BridgeableTokensOptimism.sol +++ b/contracts/optimism/RebasableAndNonRebasableTokens.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.10; /// @author psirex /// @notice Contains the logic for validation of tokens used in the bridging process -contract BridgeableTokensOptimism { +contract RebasableAndNonRebasableTokens { /// @notice Address of the bridged non rebasable token in the L1 chain address public immutable L1_TOKEN_NON_REBASABLE; diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index e9a1290d..b01030d1 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -9,12 +9,6 @@ import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; /// @notice Oracle for storing token rate. contract TokenRateOracle is ITokenRateOracle { - /// @notice wstETH/stETH token rate. - uint256 private tokenRate; - - /// @notice L1 time when token rate was pushed. - uint256 private rateL1Timestamp; - /// @notice A bridge which can update oracle. address public immutable BRIDGE; @@ -22,15 +16,24 @@ contract TokenRateOracle is ITokenRateOracle { address public immutable TOKEN_RATE_UPDATER; /// @notice A time period when token rate can be considered outdated. - uint256 public immutable RATE_VALIDITY_PERIOD; + uint256 public immutable RATE_OUTDATED_DELAY; + + /// @notice wstETH/stETH token rate. + uint256 private tokenRate; + + /// @notice L1 time when token rate was pushed. + uint256 private rateL1Timestamp; + + /// @notice Decimals of the oracle response. + uint8 private constant DECIMALS = 18; /// @param bridge_ the bridge address that has a right to updates oracle. /// @param tokenRateUpdater_ address of oracle updater that has a right to updates oracle. - /// @param rateValidityPeriod_ time period when token rate can be considered outdated. - constructor(address bridge_, address tokenRateUpdater_, uint256 rateValidityPeriod_) { + /// @param rateOutdatedDelay_ time period when token rate can be considered outdated. + constructor(address bridge_, address tokenRateUpdater_, uint256 rateOutdatedDelay_) { BRIDGE = bridge_; TOKEN_RATE_UPDATER = tokenRateUpdater_; - RATE_VALIDITY_PERIOD = rateValidityPeriod_; + RATE_OUTDATED_DELAY = rateOutdatedDelay_; } /// @inheritdoc ITokenRateOracle @@ -59,32 +62,37 @@ contract TokenRateOracle is ITokenRateOracle { /// @inheritdoc ITokenRateOracle function decimals() external pure returns (uint8) { - return 18; + return DECIMALS; } /// @inheritdoc ITokenRateOracle - function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external onlyOwner { - // reject rates from the future + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { + + if (msg.sender != BRIDGE && msg.sender != TOKEN_RATE_UPDATER) { + revert ErrorNoRights(msg.sender); + } + if (rateL1Timestamp_ < rateL1Timestamp) { revert ErrorIncorrectRateTimestamp(); } + + 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 > RATE_VALIDITY_PERIOD; + return block.timestamp - rateL1Timestamp > RATE_OUTDATED_DELAY; } - /// @dev validates that method called by one of the owners - modifier onlyOwner() { - if (msg.sender != BRIDGE && msg.sender != TOKEN_RATE_UPDATER) { - revert ErrorNotAnOwner(msg.sender); - } - _; - } + event RateUpdated(uint256 tokenRate_, uint256 rateL1Timestamp_); - error ErrorNotAnOwner(address caller); + error ErrorNoRights(address caller); error ErrorIncorrectRateTimestamp(); } diff --git a/contracts/stubs/ERC20WrappableStub.sol b/contracts/stubs/ERC20WrapperStub.sol similarity index 92% rename from contracts/stubs/ERC20WrappableStub.sol rename to contracts/stubs/ERC20WrapperStub.sol index f1796105..414065dd 100644 --- a/contracts/stubs/ERC20WrappableStub.sol +++ b/contracts/stubs/ERC20WrapperStub.sol @@ -6,10 +6,10 @@ pragma solidity 0.8.10; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Wrappable} from "../token/interfaces/IERC20Wrappable.sol"; +import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; // represents wstETH on L1 -contract ERC20WrappableStub is IERC20Wrappable, ERC20 { +contract ERC20WrapperStub is IERC20WstETH, ERC20 { IERC20 public stETH; address public bridge; diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 22372f5e..38bc18b3 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Wrappable} from "./interfaces/IERC20Wrappable.sol"; +import {IERC20Wrapper} from "./interfaces/IERC20Wrapper.sol"; import {IERC20BridgedShares} from "./interfaces/IERC20BridgedShares.sol"; import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.sol"; import {ERC20Metadata} from "./ERC20Metadata.sol"; @@ -13,7 +13,7 @@ import {UnstructuredStorage} from "./UnstructuredStorage.sol"; /// @author kovalgek /// @notice Rebasable token that wraps/unwraps non-rebasable token and allow to mint/burn tokens by bridge. -contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Metadata { +contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Metadata { using UnstructuredRefStorage for bytes32; using UnstructuredStorage for bytes32; @@ -26,7 +26,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me /// @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("ERC20Rebasable.TOKEN_ALLOWANCE_POSITION"); @@ -63,17 +63,17 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me _setERC20MetadataSymbol(symbol_); } - /// @inheritdoc IERC20Wrappable + /// @inheritdoc IERC20Wrapper function wrap(uint256 sharesAmount_) external returns (uint256) { if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); - + _mintShares(msg.sender, sharesAmount_); if(!WRAPPED_TOKEN.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); return _getTokensByShares(sharesAmount_); } - /// @inheritdoc IERC20Wrappable + /// @inheritdoc IERC20Wrapper function unwrap(uint256 tokenAmount_) external returns (uint256) { if (tokenAmount_ == 0) revert ErrorZeroTokensUnwrap(); @@ -84,14 +84,9 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me return sharesAmount; } - /// @inheritdoc IERC20Wrappable - function stETHPerToken() external view returns (uint256) { - return uint256(TOKEN_RATE_ORACLE.latestAnswer()); - } - /// @inheritdoc IERC20BridgedShares - function mintShares(address account_, uint256 amount_) external onlyBridge returns (uint256) { - return _mintShares(account_, amount_); + function mintShares(address account_, uint256 amount_) external onlyBridge { + _mintShares(account_, amount_); } /// @inheritdoc IERC20BridgedShares @@ -206,7 +201,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me 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(); @@ -271,19 +266,19 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me } function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { - (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); return (sharesAmount_ * tokensRate) / (10 ** decimals); } function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) { - (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); return (tokenAmount_ * (10 ** decimals)) / tokensRate; } function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { uint8 rateDecimals = TOKEN_RATE_ORACLE.decimals(); - if (rateDecimals == uint8(0) || rateDecimals > uint8(18)) revert ErrorInvalidRateDecimals(rateDecimals); + if (rateDecimals == uint8(0)) revert ErrorTokenRateDecimalsIsZero(); //slither-disable-next-line unused-return (, @@ -305,11 +300,10 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me function _mintShares( address recipient_, uint256 amount_ - ) internal onlyNonZeroAccount(recipient_) returns (uint256) { + ) internal onlyNonZeroAccount(recipient_) { _setTotalShares(_getTotalShares() + amount_); _getShares()[recipient_] = _getShares()[recipient_] + amount_; emit Transfer(address(0), recipient_, amount_); - return _getTotalShares(); } /// @dev Destroys amount_ shares from account_, reducing the total shares supply. @@ -318,13 +312,12 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me function _burnShares( address account_, uint256 amount_ - ) internal onlyNonZeroAccount(account_) returns (uint256) { + ) 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_); - return _getTotalShares(); } /// @dev Moves `sharesAmount_` shares from `sender_` to `recipient_`. @@ -336,7 +329,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me address recipient_, uint256 sharesAmount_ ) internal onlyNonZeroAccount(sender_) onlyNonZeroAccount(recipient_) { - + if (recipient_ == address(this)) revert ErrorTrasferToRebasableContract(); uint256 currentSenderShares = _getShares()[sender_]; @@ -364,7 +357,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me error ErrorZeroSharesWrap(); error ErrorZeroTokensUnwrap(); - error ErrorInvalidRateDecimals(uint8); + error ErrorTokenRateDecimalsIsZero(); error ErrorWrongOracleUpdateTime(); error ErrorOracleAnswerIsNegative(); error ErrorTrasferToRebasableContract(); @@ -374,4 +367,4 @@ contract ERC20Rebasable is IERC20, IERC20Wrappable, IERC20BridgedShares, ERC20Me error ErrorDecreasedAllowanceBelowZero(); error ErrorNotBridge(); error ErrorERC20Transfer(); -} \ No newline at end of file +} diff --git a/contracts/token/L1TokenNonRebasableAdapter.sol b/contracts/token/L1TokenNonRebasableAdapter.sol new file mode 100644 index 00000000..1d943853 --- /dev/null +++ b/contracts/token/L1TokenNonRebasableAdapter.sol @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IERC20TokenRate} from "../token/interfaces/IERC20TokenRate.sol"; +import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; + +/// @author kovalgek +/// @notice Hides wstETH concept from other contracts to save level of abstraction. +contract L1TokenNonRebasableAdapter is IERC20TokenRate { + + IERC20WstETH public immutable WSTETH; + + constructor(address wstETH_) { + WSTETH = IERC20WstETH(wstETH_); + } + + /// @inheritdoc IERC20TokenRate + function tokenRate() external view returns (uint256) { + return WSTETH.stETHPerToken(); + } +} diff --git a/contracts/token/interfaces/IERC20BridgedShares.sol b/contracts/token/interfaces/IERC20BridgedShares.sol index 2b95cb1a..df7a0d9e 100644 --- a/contracts/token/interfaces/IERC20BridgedShares.sol +++ b/contracts/token/interfaces/IERC20BridgedShares.sol @@ -6,18 +6,18 @@ pragma solidity 0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @author kovalgek -/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens +/// @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 tokens on L2 + /// @notice Returns bridge which can mint and burn shares 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 mintShares(address account_, uint256 amount_) external returns (uint256); + /// @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 mintShares(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 + /// @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 burnShares(address account_, uint256 amount_) external; } diff --git a/contracts/token/interfaces/IERC20TokenRate.sol b/contracts/token/interfaces/IERC20TokenRate.sol new file mode 100644 index 00000000..16c51870 --- /dev/null +++ b/contracts/token/interfaces/IERC20TokenRate.sol @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice Token rate interface. +interface IERC20TokenRate { + + /// @notice Returns token rate. + function tokenRate() external view returns (uint256); +} diff --git a/contracts/token/interfaces/IERC20Wrappable.sol b/contracts/token/interfaces/IERC20Wrappable.sol deleted file mode 100644 index 809e3c82..00000000 --- a/contracts/token/interfaces/IERC20Wrappable.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @notice Extends the ERC20 functionality that allows the bridge to mint/burn tokens -interface IERC20Wrappable { - - /** - * @notice Exchanges wstETH to stETH - * @param sharesAmount_ amount of wstETH to wrap in exchange for stETH - * @dev Requirements: - * - `wstETHAmount_` must be non-zero - * - msg.sender must approve at least `wstETHAmount_` stETH to this - * contract. - * - msg.sender must have at least `wstETHAmount_` of stETH. - * User should first approve wstETHAmount_ to the StETH contract - * @return Amount of StETH user receives after wrap - */ - function wrap(uint256 sharesAmount_) external returns (uint256); - - /** - * @notice Exchanges stETH to wstETH - * @param wrappableTokenAmount_ amount of stETH to uwrap in exchange for wstETH - * @dev Requirements: - * - `stETHAmount_` must be non-zero - * - msg.sender must have at least `stETHAmount_` stETH. - * @return Amount of wstETH user receives after unwrap - */ - function unwrap(uint256 wrappableTokenAmount_) external returns (uint256); - - /** - * @notice Get amount of wstETH for a one stETH - * @return Amount of wstETH for a 1 stETH - */ - function stETHPerToken() external view returns (uint256); -} \ No newline at end of file diff --git a/contracts/token/interfaces/IERC20Wrapper.sol b/contracts/token/interfaces/IERC20Wrapper.sol new file mode 100644 index 00000000..6b3125d4 --- /dev/null +++ b/contracts/token/interfaces/IERC20Wrapper.sol @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 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/contracts/token/interfaces/IERC20WstETH.sol b/contracts/token/interfaces/IERC20WstETH.sol new file mode 100644 index 00000000..19a9badb --- /dev/null +++ b/contracts/token/interfaces/IERC20WstETH.sol @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @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); +} diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index ab3bc69d..c4ad2883 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -7,7 +7,7 @@ import { ERC20Bridged__factory, ERC20Rebasable__factory, TokenRateOracle__factory, - ERC20WrappableStub__factory, + ERC20WrapperStub__factory, } from "../../typechain"; import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; @@ -215,7 +215,7 @@ async function ctxFactory() { "TT" ); - const l1TokenRebasable = await new ERC20WrappableStub__factory(l1Deployer).deploy( + const l1TokenRebasable = await new ERC20WrapperStub__factory(l1Deployer).deploy( l1Token.address, "Test Token", "TT" diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index 9c5eae8f..20c594c9 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -375,7 +375,7 @@ unit("Optimism:: L2ERC20TokenBridge", ctxFactory) .run(); async function ctxFactory() { - const [deployer, stranger, recipient, l1TokenBridgeEOA, token2] = + const [deployer, stranger, recipient, l1TokenBridgeEOA, rebasableToken] = await hre.ethers.getSigners(); const l2Messenger = await new CrossDomainMessengerStub__factory( @@ -405,9 +405,9 @@ async function ctxFactory() { l2Messenger.address, l1TokenBridgeEOA.address, l1Token.address, - token2.address, + rebasableToken.address, l2Token.address, - token2.address + rebasableToken.address ); const l2TokenBridgeProxy = await new OssifiableProxy__factory( diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index 42e42cf5..3150f773 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -30,23 +30,23 @@ unit("TokenRateOracle", ctxFactory) assert.equalBN(await tokenRateOracle.decimals(), 18); }) - .test("wrong owner", async (ctx) => { + .test("updateRate() :: no rights to call", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { bridge, updater, stranger } = ctx.accounts; tokenRateOracle.connect(bridge).updateRate(10, 20); tokenRateOracle.connect(updater).updateRate(10, 23); - await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "ErrorNotAnOwner(\""+stranger.address+"\")"); + await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "ErrorNoRights(\""+stranger.address+"\")"); }) - .test("incorrect time", async (ctx) => { + .test("updateRate() :: incorrect time", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { bridge } = ctx.accounts; - + tokenRateOracle.connect(bridge).updateRate(10, 1000); await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "ErrorIncorrectRateTimestamp()"); }) - .test("state after update token rate", async (ctx) => { + .test("updateRate() :: happy path", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { updater } = ctx.accounts; @@ -83,8 +83,8 @@ async function ctxFactory() { bridge.address, updater.address, 86400 - ); - + ); + return { accounts: { deployer, bridge, updater, stranger }, contracts: { tokenRateOracle } diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 77f89921..757f6188 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -7,7 +7,7 @@ import testing, { scenario } from "../../utils/testing"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { ERC20WrappableStub } from "../../typechain"; +import { ERC20WrapperStub } from "../../typechain"; scenario("Optimism :: Bridging integration test", ctxFactory) .after(async (ctx) => { @@ -119,13 +119,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2ERC20TokenBridge, l1Provider } = ctx; - + const { l1Stranger } = ctx.accounts; const tokenHolderStrangerBalanceBefore = await l1TokenRebasable.balanceOf( l1Stranger.address ); - + const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( l1ERC20TokenBridge.address ); @@ -144,7 +144,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 0, dataToSend, ]); - + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( "finalizeDeposit", [ @@ -156,9 +156,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) dataToSend, ] ); - + const messageNonce = await l1CrossDomainMessenger.messageNonce(); - + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, l1ERC20TokenBridge.address, @@ -166,12 +166,12 @@ scenario("Optimism :: Bridging integration test", ctxFactory) messageNonce, 200_000, ]); - + assert.equalBN( await l1Token.balanceOf(l1ERC20TokenBridge.address), l1ERC20TokenBridgeBalanceBefore ); - + assert.equalBN( await l1TokenRebasable.balanceOf(l1Stranger.address), tokenHolderStrangerBalanceBefore @@ -190,7 +190,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - + await l1TokenRebasable .connect(tokenHolderA.l1Signer) .approve(l1ERC20TokenBridge.address, 0); @@ -269,16 +269,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } = 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( @@ -329,7 +329,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmountNonRebasable, depositAmountRebasable } = ctx.common; - + await l1TokenRebasable .connect(tokenHolderA.l1Signer) .approve(l1ERC20TokenBridge.address, depositAmountRebasable); @@ -414,7 +414,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( tokenHolderA.address ); - + const l2TokenRebasableTotalSupplyBefore = await l2TokenRebasable.totalSupply(); const dataToReceive = await packedTokenRateAndTimestamp(l2Provider, l1Token); @@ -601,7 +601,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 200_000, "0x" ); - + const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ @@ -612,7 +612,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) depositAmountNonRebasable, dataToSend, ]); - + const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( "finalizeDeposit", [ @@ -624,9 +624,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) dataToSend, ] ); - + const messageNonce = await l1CrossDomainMessenger.messageNonce(); - + await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, l1ERC20TokenBridge.address, @@ -639,7 +639,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await l1Token.balanceOf(l1ERC20TokenBridge.address), l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) ); - + assert.equalBN( await l1TokenRebasable.balanceOf(tokenHolderA.address), // stETH tokenHolderABalanceBefore.sub(depositAmountRebasable) @@ -664,7 +664,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } = ctx.accounts; const { exchangeRate } = ctx.common; - + const depositAmountNonRebasable = wei`0.03 ether`; const depositAmountRebasable = wei.toBigNumber(depositAmountNonRebasable).mul(exchangeRate); @@ -832,7 +832,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) async function ctxFactory() { const networkName = env.network("TESTING_OPT_NETWORK", "mainnet"); - + const { l1Provider, l2Provider, @@ -917,11 +917,11 @@ async function ctxFactory() { }; } -async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrappableStub) { +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]); -} \ No newline at end of file +} diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index a7049144..ab8e5dd1 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -8,21 +8,21 @@ import { BigNumber } from "ethers"; unit("ERC20Rebasable", ctxFactory) - .test("wrappedToken", async (ctx) => { + .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { const { rebasableProxied, wrappedTokenStub } = ctx.contracts; - assert.equal(await rebasableProxied.wrappedToken(), wrappedTokenStub.address) + assert.equal(await rebasableProxied.WRAPPED_TOKEN(), wrappedTokenStub.address) }) - .test("tokenRateOracle", async (ctx) => { + .test("tokenRateOracle() :: has the same address is in constructor", async (ctx) => { const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; - assert.equal(await rebasableProxied.tokenRateOracle(), tokenRateOracleStub.address) + assert.equal(await rebasableProxied.TOKEN_RATE_ORACLE(), tokenRateOracleStub.address) }) - .test("name()", async (ctx) => + .test("name() :: has the same value is in constructor", async (ctx) => assert.equal(await ctx.contracts.rebasableProxied.name(), ctx.constants.name) ) - .test("symbol()", async (ctx) => + .test("symbol() :: has the same value is in constructor", async (ctx) => assert.equal(await ctx.contracts.rebasableProxied.symbol(), ctx.constants.symbol) ) @@ -30,8 +30,8 @@ unit("ERC20Rebasable", ctxFactory) const { deployer, owner } = ctx.accounts; // deploy new implementation - const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( "name", "", @@ -50,8 +50,8 @@ unit("ERC20Rebasable", ctxFactory) const { deployer, owner } = ctx.accounts; // deploy new implementation - const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( "", "symbol", @@ -66,29 +66,29 @@ unit("ERC20Rebasable", ctxFactory) ); }) - .test("decimals", async (ctx) => + .test("decimals() :: has the same value is in constructor", async (ctx) => assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimalsToSet) ) - .test("totalShares", async (ctx) => { + .test("getTotalShares() :: returns preminted amount", async (ctx) => { const { premintShares } = ctx.constants; assert.equalBN(await ctx.contracts.rebasableProxied.getTotalShares(), premintShares); }) - .test("wrap(0)", async (ctx) => { + .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("unwrap(0)", async (ctx) => { + .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("wrap() happy path", async (ctx) => { - + .test("wrap() :: happy path", async (ctx) => { + const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; const { rate, decimals, premintShares } = ctx.constants; @@ -114,7 +114,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equal(await wrappedTokenStub.transferFromAddress(), user1.address); assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); assert.equalBN(await wrappedTokenStub.transferFromAmount(), user1Shares); - + // common state changes assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares)); assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens)); @@ -140,19 +140,16 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); }) - .test("wrap() with wrong oracle decimals", async (ctx) => { + .test("wrap() :: wrong oracle decimals", async (ctx) => { const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; const { user1 } = ctx.accounts; await tokenRateOracleStub.setDecimals(0); - await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorInvalidRateDecimals(0)"); - - await tokenRateOracleStub.setDecimals(19); - await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorInvalidRateDecimals(19)"); + await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorTokenRateDecimalsIsZero()"); }) - .test("wrap() with wrong oracle update time", async (ctx) => { + .test("wrap() :: wrong oracle update time", async (ctx) => { const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; const { user1 } = ctx.accounts; @@ -161,7 +158,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).wrap(5), "ErrorWrongOracleUpdateTime()"); }) - .test("wrap() with wrong oracle answer", async (ctx) => { + .test("wrap() :: wrong oracle answer", async (ctx) => { const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; const { user1 } = ctx.accounts; @@ -170,7 +167,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNegative()"); }) - .test("unwrap() happy path", async (ctx) => { + .test("unwrap() :: happy path", async (ctx) => { const { rebasableProxied, wrappedTokenStub } = ctx.contracts; const {user1, user2 } = ctx.accounts; @@ -230,7 +227,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), totalSupply.add(user1Tokens).add(user2Tokens)); }) - .test("unwrap() with wrong oracle decimals", async (ctx) => { + .test("unwrap() :: with wrong oracle decimals", async (ctx) => { const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; const { user1 } = ctx.accounts; @@ -238,13 +235,10 @@ unit("ERC20Rebasable", ctxFactory) await rebasableProxied.connect(user1).wrap(wei`2 ether`); await tokenRateOracleStub.setDecimals(0); - await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorInvalidRateDecimals(0)"); - - await tokenRateOracleStub.setDecimals(19); - await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorInvalidRateDecimals(19)"); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorTokenRateDecimalsIsZero()"); }) - .test("unwrap() with wrong oracle update time", async (ctx) => { + .test("unwrap() :: with wrong oracle update time", async (ctx) => { const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; const { user1 } = ctx.accounts; @@ -254,14 +248,14 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`1 ether`), "ErrorWrongOracleUpdateTime()"); }) - .test("unwrap() when no balance", async (ctx) => { + .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("mintShares() happy path", async (ctx) => { + .test("mintShares() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const {user1, user2, owner } = ctx.accounts; @@ -308,7 +302,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted).add(user2TokensMinted)); }) - .test("burnShares() happy path", async (ctx) => { + .test("burnShares() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const {user1, user2, owner } = ctx.accounts; @@ -367,7 +361,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1Tokens).add(user2Tokens)); }) - .test("approve()", async (ctx) => { + .test("approve() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const { holder, spender } = ctx.accounts; @@ -453,7 +447,7 @@ unit("ERC20Rebasable", ctxFactory) ); }) - .test("transfer()", async (ctx) => { + .test("transfer() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const { premintTokens } = ctx.constants; const { recipient, holder } = ctx.accounts; @@ -485,7 +479,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), premintTokens); }) - .test("transferFrom()", async (ctx) => { + .test("transferFrom() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const { premintTokens } = ctx.constants; const { recipient, holder, spender } = ctx.accounts; @@ -937,8 +931,8 @@ async function ctxFactory() { user2 ] = await hre.ethers.getSigners(); - const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); + const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( name, symbol, @@ -963,7 +957,7 @@ async function ctxFactory() { symbol, ]) ); - + const rebasableProxied = ERC20Rebasable__factory.connect( l2TokensProxy.address, holder diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index bc0380af..a4879427 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -9,7 +9,7 @@ import { L2ERC20TokenBridge, ERC20Bridged__factory, ERC20BridgedStub__factory, - ERC20WrappableStub__factory, + ERC20WrapperStub__factory, TokenRateOracle__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, @@ -156,7 +156,7 @@ async function loadDeployedBridges( l2SignerOrProvider: SignerOrProvider ) { return { - l1Token: ERC20WrappableStub__factory.connect( + l1Token: ERC20WrapperStub__factory.connect( testingUtils.env.OPT_L1_TOKEN(), l1SignerOrProvider ), @@ -192,7 +192,7 @@ async function deployTestBridge( "TTR" ); - const l1Token = await new ERC20WrappableStub__factory(ethDeployer).deploy( + const l1Token = await new ERC20WrapperStub__factory(ethDeployer).deploy( l1TokenRebasable.address, "Test Token", "TT" From 7abf60f92ba37f4d40019855e69ce60060a0b9fb Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 2 Feb 2024 17:18:52 +0100 Subject: [PATCH 25/61] use real implemnations of non rebasable token and oracle contracts in rebnasable token tests --- contracts/optimism/TokenRateOracle.sol | 9 +- contracts/stubs/ERC20Stub.sol | 65 ------ contracts/stubs/TokenRateOracleStub.sol | 59 ----- contracts/token/ERC20Rebasable.sol | 10 +- test/optimism/TokenRateOracle.unit.test.ts | 15 +- test/token/ERC20Rebasable.unit.test.ts | 247 ++++++++++++++------- utils/optimism/deployment.ts | 5 +- 7 files changed, 177 insertions(+), 233 deletions(-) delete mode 100644 contracts/stubs/ERC20Stub.sol delete mode 100644 contracts/stubs/TokenRateOracleStub.sol diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index b01030d1..dae90761 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -12,9 +12,6 @@ contract TokenRateOracle is ITokenRateOracle { /// @notice A bridge which can update oracle. address public immutable BRIDGE; - /// @notice An updater which can update oracle. - address public immutable TOKEN_RATE_UPDATER; - /// @notice A time period when token rate can be considered outdated. uint256 public immutable RATE_OUTDATED_DELAY; @@ -28,11 +25,9 @@ contract TokenRateOracle is ITokenRateOracle { uint8 private constant DECIMALS = 18; /// @param bridge_ the bridge address that has a right to updates oracle. - /// @param tokenRateUpdater_ address of oracle updater that has a right to updates oracle. /// @param rateOutdatedDelay_ time period when token rate can be considered outdated. - constructor(address bridge_, address tokenRateUpdater_, uint256 rateOutdatedDelay_) { + constructor(address bridge_, uint256 rateOutdatedDelay_) { BRIDGE = bridge_; - TOKEN_RATE_UPDATER = tokenRateUpdater_; RATE_OUTDATED_DELAY = rateOutdatedDelay_; } @@ -68,7 +63,7 @@ contract TokenRateOracle is ITokenRateOracle { /// @inheritdoc ITokenRateOracle function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { - if (msg.sender != BRIDGE && msg.sender != TOKEN_RATE_UPDATER) { + if (msg.sender != BRIDGE) { revert ErrorNoRights(msg.sender); } diff --git a/contracts/stubs/ERC20Stub.sol b/contracts/stubs/ERC20Stub.sol deleted file mode 100644 index b8ef5902..00000000 --- a/contracts/stubs/ERC20Stub.sol +++ /dev/null @@ -1,65 +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"; - -contract ERC20Stub is IERC20 { - - uint256 totalSupply_; - uint256 balanceOf_; - bool transfer_; - uint256 allowance_; - bool approve_; - bool transferFrom_; - - constructor() { - totalSupply_ = 0; - balanceOf_ = 0; - transfer_ = true; - allowance_ = 0; - approve_ = true; - transferFrom_ = true; - } - - function totalSupply() external view returns (uint256) { - return totalSupply_; - } - - function balanceOf(address account) external view returns (uint256) { - return balanceOf_; - } - - address public transferTo; - uint256 public transferAmount; - - function transfer(address to, uint256 amount) external returns (bool) { - transferTo = to; - transferAmount = amount; - return true; - } - - function allowance(address owner, address spender) external view returns (uint256) { - return 0; - } - - function approve(address spender, uint256 amount) external returns (bool) { - return true; - } - - address public transferFromAddress; - address public transferFromTo; - uint256 public transferFromAmount; - - function transferFrom( - address from, - address to, - uint256 amount - ) external returns (bool) { - transferFromAddress = from; - transferFromTo = to; - transferFromAmount = amount; - return true; - } -} \ No newline at end of file diff --git a/contracts/stubs/TokenRateOracleStub.sol b/contracts/stubs/TokenRateOracleStub.sol deleted file mode 100644 index 25ab6763..00000000 --- a/contracts/stubs/TokenRateOracleStub.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; - -contract TokenRateOracleStub is ITokenRateOracle { - - uint8 public _decimals; - - function setDecimals(uint8 decimals_) external { - _decimals = decimals_; - } - - function decimals() external view returns (uint8) { - return _decimals; - } - - uint256 public latestRoundDataAnswer; - - function setLatestRoundDataAnswer(uint256 answer_) external { - latestRoundDataAnswer = answer_; - } - - uint256 public latestRoundDataUpdatedAt; - - function setUpdatedAt(uint256 updatedAt_) external { - latestRoundDataUpdatedAt = updatedAt_; - } - - function latestRoundData() - external - view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ) { - return ( - 0, - int256(latestRoundDataAnswer), - 0, - latestRoundDataUpdatedAt, - 0 - ); - } - - function latestAnswer() external view returns (int256) { - return int256(latestRoundDataAnswer); - } - - function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { - latestRoundDataAnswer = tokenRate_; - latestRoundDataUpdatedAt = rateL1Timestamp_; - } -} \ No newline at end of file diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 38bc18b3..81c4eb09 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -266,16 +266,16 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta } function _getTokensByShares(uint256 sharesAmount_) internal view returns (uint256) { - (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + (uint256 tokensRate, uint256 decimals) = _getTokenRateAndDecimal(); return (sharesAmount_ * tokensRate) / (10 ** decimals); } function _getSharesByTokens(uint256 tokenAmount_) internal view returns (uint256) { - (uint256 tokensRate, uint256 decimals) = _getTokensRateAndDecimal(); + (uint256 tokensRate, uint256 decimals) = _getTokenRateAndDecimal(); return (tokenAmount_ * (10 ** decimals)) / tokensRate; } - function _getTokensRateAndDecimal() internal view returns (uint256, uint256) { + function _getTokenRateAndDecimal() internal view returns (uint256, uint256) { uint8 rateDecimals = TOKEN_RATE_ORACLE.decimals(); if (rateDecimals == uint8(0)) revert ErrorTokenRateDecimalsIsZero(); @@ -289,7 +289,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ,) = TOKEN_RATE_ORACLE.latestRoundData(); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); - if (answer <= 0) revert ErrorOracleAnswerIsNegative(); + if (answer <= 0) revert ErrorOracleAnswerIsNotPositive(); return (uint256(answer), uint256(rateDecimals)); } @@ -359,7 +359,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta error ErrorZeroTokensUnwrap(); error ErrorTokenRateDecimalsIsZero(); error ErrorWrongOracleUpdateTime(); - error ErrorOracleAnswerIsNegative(); + error ErrorOracleAnswerIsNotPositive(); error ErrorTrasferToRebasableContract(); error ErrorNotEnoughBalance(); error ErrorNotEnoughAllowance(); diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index 3150f773..a2bde5e0 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -7,10 +7,9 @@ unit("TokenRateOracle", ctxFactory) .test("state after init", async (ctx) => { const { tokenRateOracle } = ctx.contracts; - const { bridge, updater } = ctx.accounts; + const { bridge } = ctx.accounts; assert.equal(await tokenRateOracle.BRIDGE(), bridge.address); - assert.equal(await tokenRateOracle.TOKEN_RATE_UPDATER(), updater.address); assert.equalBN(await tokenRateOracle.latestAnswer(), 0); @@ -32,9 +31,8 @@ unit("TokenRateOracle", ctxFactory) .test("updateRate() :: no rights to call", async (ctx) => { const { tokenRateOracle } = ctx.contracts; - const { bridge, updater, stranger } = ctx.accounts; + const { bridge, stranger } = ctx.accounts; tokenRateOracle.connect(bridge).updateRate(10, 20); - tokenRateOracle.connect(updater).updateRate(10, 23); await assert.revertsWith(tokenRateOracle.connect(stranger).updateRate(10, 40), "ErrorNoRights(\""+stranger.address+"\")"); }) @@ -48,12 +46,12 @@ unit("TokenRateOracle", ctxFactory) .test("updateRate() :: happy path", async (ctx) => { const { tokenRateOracle } = ctx.contracts; - const { updater } = ctx.accounts; + const { bridge } = ctx.accounts; const currentTime = Date.now(); const tokenRate = 123; - await tokenRateOracle.connect(updater).updateRate(tokenRate, currentTime ); + await tokenRateOracle.connect(bridge).updateRate(tokenRate, currentTime ); assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRate); @@ -77,16 +75,15 @@ unit("TokenRateOracle", ctxFactory) async function ctxFactory() { - const [deployer, bridge, updater, stranger] = await hre.ethers.getSigners(); + const [deployer, bridge, stranger] = await hre.ethers.getSigners(); const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( bridge.address, - updater.address, 86400 ); return { - accounts: { deployer, bridge, updater, stranger }, + accounts: { deployer, bridge, stranger }, contracts: { tokenRateOracle } }; } diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index ab8e5dd1..2561256c 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -3,19 +3,24 @@ import { assert } from "chai"; import { unit } from "../../utils/testing"; import { wei } from "../../utils/wei"; -import { ERC20Stub__factory, ERC20Rebasable__factory, TokenRateOracleStub__factory, OssifiableProxy__factory } from "../../typechain"; +import { + ERC20Bridged__factory, + TokenRateOracle__factory, + ERC20Rebasable__factory, + OssifiableProxy__factory +} from "../../typechain"; import { BigNumber } from "ethers"; unit("ERC20Rebasable", ctxFactory) .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { - const { rebasableProxied, wrappedTokenStub } = ctx.contracts; - assert.equal(await rebasableProxied.WRAPPED_TOKEN(), wrappedTokenStub.address) + const { rebasableProxied, wrappedToken } = ctx.contracts; + assert.equal(await rebasableProxied.WRAPPED_TOKEN(), wrappedToken.address) }) .test("tokenRateOracle() :: has the same address is in constructor", async (ctx) => { - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; - assert.equal(await rebasableProxied.TOKEN_RATE_ORACLE(), tokenRateOracleStub.address) + 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) => @@ -28,16 +33,25 @@ unit("ERC20Rebasable", ctxFactory) .test("initialize() :: name already set", async (ctx) => { const { deployer, owner } = ctx.accounts; + const { decimalsToSet } = ctx.constants; // deploy new implementation - const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + owner.address, + 86400 + ); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( "name", "", 10, - wrappedTokenStub.address, - tokenRateOracleStub.address, + wrappedToken.address, + tokenRateOracle.address, owner.address ); await assert.revertsWith( @@ -48,16 +62,25 @@ unit("ERC20Rebasable", ctxFactory) .test("initialize() :: symbol already set", async (ctx) => { const { deployer, owner } = ctx.accounts; + const { decimalsToSet } = ctx.constants; // deploy new implementation - const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + owner.address, + 86400 + ); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( "", "symbol", 10, - wrappedTokenStub.address, - tokenRateOracleStub.address, + wrappedToken.address, + tokenRateOracle.address, owner.address ); await assert.revertsWith( @@ -66,7 +89,7 @@ unit("ERC20Rebasable", ctxFactory) ); }) - .test("decimals() :: has the same value is in constructor", async (ctx) => + .test("decimals() :: has the same value as is in constructor", async (ctx) => assert.equal(await ctx.contracts.rebasableProxied.decimals(), ctx.constants.decimalsToSet) ) @@ -81,39 +104,86 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).wrap(0), "ErrorZeroSharesWrap()"); }) - .test("unwrap() :: revert if unwrap 0 wstETH", async (ctx) => { - const { rebasableProxied } = ctx.contracts; + .test("wrap() :: wrong oracle update time", async (ctx) => { + + const { deployer, user1, owner } = 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( + owner.address, + 86400 + ); + const rebasableProxied = await new ERC20Rebasable__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() :: wrong oracle answer", async (ctx) => { + + const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; + const { user1, owner } = ctx.accounts; + + await tokenRateOracle.connect(owner).updateRate(0, 2000); + await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); + await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); + + await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNotPositive()"); + }) + + .test("wrap() :: when no balance", async (ctx) => { + const { rebasableProxied, wrappedToken } = ctx.contracts; const { user1 } = ctx.accounts; - await assert.revertsWith(rebasableProxied.connect(user1).unwrap(0), "ErrorZeroTokensUnwrap()"); + + 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, wrappedTokenStub } = ctx.contracts; - const {user1, user2 } = ctx.accounts; + const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; + const {user1, user2, owner } = 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); - const user1Shares = wei`100 ether`; - const user1Tokens = rate.mul(user1Shares).div(decimals); + 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); assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); - - assert.equal(await wrappedTokenStub.transferFromAddress(), user1.address); - assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); - assert.equalBN(await wrappedTokenStub.transferFromAmount(), user1Shares); + assert.equalBN(await wrappedToken.balanceOf(rebasableProxied.address), user1Shares); // common state changes assert.equalBN(await rebasableProxied.getTotalShares(), BigNumber.from(premintShares).add(user1Shares)); @@ -126,51 +196,31 @@ unit("ERC20Rebasable", ctxFactory) 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); assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); - assert.equal(await wrappedTokenStub.transferFromAddress(), user2.address); - assert.equal(await wrappedTokenStub.transferFromTo(), rebasableProxied.address); - assert.equalBN(await wrappedTokenStub.transferFromAmount(), user2Shares); + 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("wrap() :: wrong oracle decimals", async (ctx) => { - - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; - const { user1 } = ctx.accounts; - - await tokenRateOracleStub.setDecimals(0); - await assert.revertsWith(rebasableProxied.connect(user1).wrap(23), "ErrorTokenRateDecimalsIsZero()"); - }) - - .test("wrap() :: wrong oracle update time", async (ctx) => { - - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; - const { user1 } = ctx.accounts; - - await tokenRateOracleStub.setUpdatedAt(0); - await assert.revertsWith(rebasableProxied.connect(user1).wrap(5), "ErrorWrongOracleUpdateTime()"); - }) - - .test("wrap() :: wrong oracle answer", async (ctx) => { - - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; + .test("unwrap() :: revert if unwrap 0 wstETH", async (ctx) => { + const { rebasableProxied } = ctx.contracts; const { user1 } = ctx.accounts; - - await tokenRateOracleStub.setLatestRoundDataAnswer(0); - await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNegative()"); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(0), "ErrorZeroTokensUnwrap()"); }) .test("unwrap() :: happy path", async (ctx) => { - const { rebasableProxied, wrappedTokenStub } = ctx.contracts; - const {user1, user2 } = ctx.accounts; + 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); @@ -179,6 +229,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), totalSupply); // user1 + assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); @@ -189,14 +240,16 @@ unit("ERC20Rebasable", ctxFactory) 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.equal(await wrappedTokenStub.transferTo(), user1.address); - assert.equalBN(await wrappedTokenStub.transferAmount(), user1SharesToUnwrap); + assert.equalBN(await wrappedToken.balanceOf(rebasableProxied.address), user1Shares); // common state changes assert.equalBN(await rebasableProxied.getTotalShares(), premintShares.add(user1Shares)); @@ -213,39 +266,63 @@ unit("ERC20Rebasable", ctxFactory) 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.equal(await wrappedTokenStub.transferTo(), user2.address); - assert.equalBN(await wrappedTokenStub.transferAmount(), user2SharesToUnwrap); + 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 decimals", async (ctx) => { + .test("unwrap() :: with wrong oracle update time", async (ctx) => { - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; - const { user1 } = ctx.accounts; + const { deployer, user1, owner } = 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( + owner.address, + 86400 + ); + const rebasableProxied = await new ERC20Rebasable__factory(deployer).deploy( + "", + "symbol", + 10, + wrappedToken.address, + tokenRateOracle.address, + owner.address + ); - await rebasableProxied.connect(user1).wrap(wei`2 ether`); + await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); + await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); - await tokenRateOracleStub.setDecimals(0); - await assert.revertsWith(rebasableProxied.connect(user1).unwrap(23), "ErrorTokenRateDecimalsIsZero()"); + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(5), "ErrorWrongOracleUpdateTime()"); }) - .test("unwrap() :: with wrong oracle update time", async (ctx) => { +.test("unwrap() :: wrong oracle answer", async (ctx) => { - const { rebasableProxied, tokenRateOracleStub } = ctx.contracts; - const { user1 } = ctx.accounts; + const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; + const { user1, owner } = ctx.accounts; - await rebasableProxied.connect(user1).wrap(wei`6 ether`); - await tokenRateOracleStub.setUpdatedAt(0); - await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`1 ether`), "ErrorWrongOracleUpdateTime()"); + await tokenRateOracle.connect(owner).updateRate(0, 2000); + await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); + await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); + + await assert.revertsWith(rebasableProxied.connect(user1).unwrap(21), "ErrorOracleAnswerIsNotPositive()"); }) .test("unwrap() :: when no balance", async (ctx) => { @@ -271,7 +348,6 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); - assert.equalBN(await rebasableProxied.connect(owner).callStatic.mintShares(user1.address, user1SharesToMint), premintShares.add(user1SharesToMint)); const tx0 = await rebasableProxied.connect(owner).mintShares(user1.address, user1SharesToMint); assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); @@ -288,10 +364,6 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - assert.equalBN( - await rebasableProxied.connect(owner).callStatic.mintShares(user2.address, user2SharesToMint), - premintShares.add(user1SharesToMint).add(user2SharesToMint) - ); const tx1 = await rebasableProxied.connect(owner).mintShares(user2.address, user2SharesToMint); assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); @@ -914,7 +986,7 @@ unit("ERC20Rebasable", ctxFactory) async function ctxFactory() { const name = "StETH Test Token"; const symbol = "StETH"; - const decimalsToSet = 16; + 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`); @@ -931,14 +1003,22 @@ async function ctxFactory() { user2 ] = await hre.ethers.getSigners(); - const wrappedTokenStub = await new ERC20Stub__factory(deployer).deploy(); - const tokenRateOracleStub = await new TokenRateOracleStub__factory(deployer).deploy(); + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + owner.address, + 86400 + ); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( name, symbol, decimalsToSet, - wrappedTokenStub.address, - tokenRateOracleStub.address, + wrappedToken.address, + tokenRateOracle.address, owner.address ); @@ -963,15 +1043,12 @@ async function ctxFactory() { holder ); - await tokenRateOracleStub.setDecimals(decimalsToSet); - await tokenRateOracleStub.setLatestRoundDataAnswer(rate); - await tokenRateOracleStub.setUpdatedAt(1000); - + await tokenRateOracle.connect(owner).updateRate(rate, 1000); await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); return { accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate }, - contracts: { rebasableProxied, wrappedTokenStub, tokenRateOracleStub } + contracts: { rebasableProxied, wrappedToken, tokenRateOracle } }; } diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index dbbd9fba..354c6eb4 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -48,7 +48,7 @@ export default function deployment( expectedL1TokenBridgeImplAddress, expectedL1TokenBridgeProxyAddress, ] = await network.predictAddresses(l1Params.deployer, 2); - + const [ expectedL2TokenRateOracleImplAddress, expectedL2TokenImplAddress, @@ -116,7 +116,6 @@ export default function deployment( .addStep({ factory: TokenRateOracle__factory, args: [ - expectedL2TokenBridgeProxyAddress, expectedL2TokenBridgeProxyAddress, 86400, options?.overrides, @@ -124,7 +123,7 @@ export default function deployment( afterDeploy: (c) => assert.equal(c.address, expectedL2TokenRateOracleImplAddress), }) - + .addStep({ factory: ERC20Bridged__factory, args: [ From 707da9b41e755208e4407b75502bf2adca9ffff0 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 8 Feb 2024 09:59:18 +0100 Subject: [PATCH 26/61] update unit tests for bridges --- contracts/optimism/L2ERC20TokenBridge.sol | 6 +- contracts/stubs/ERC20WrapperStub.sol | 3 +- test/optimism/L1ERC20TokenBridge.unit.test.ts | 535 ++++++-- test/optimism/L2ERC20TokenBridge.unit.test.ts | 1217 +++++++++++------ test/optimism/TokenRateOracle.unit.test.ts | 11 + 5 files changed, 1258 insertions(+), 514 deletions(-) diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index f939eb62..ae195718 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -114,13 +114,17 @@ contract L2ERC20TokenBridge is bytes calldata data_ ) internal { if (l2Token_ == L2_TOKEN_REBASABLE) { + // TODO: maybe loosing 1 wei here as well uint256 shares = ERC20Rebasable(L2_TOKEN_REBASABLE).getSharesByTokens(amount_); ERC20Rebasable(L2_TOKEN_REBASABLE).burnShares(msg.sender, shares); - _initiateWithdrawal(L2_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, shares, l1Gas_, data_); + + _initiateWithdrawal(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, shares, l1Gas_, data_); emit WithdrawalInitiated(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, amount_, data_); } else if (l2Token_ == L2_TOKEN_NON_REBASABLE) { + IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeBurn(msg.sender, amount_); + _initiateWithdrawal(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, l1Gas_, data_); emit WithdrawalInitiated(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, data_); } diff --git a/contracts/stubs/ERC20WrapperStub.sol b/contracts/stubs/ERC20WrapperStub.sol index 414065dd..2adaa9e9 100644 --- a/contracts/stubs/ERC20WrapperStub.sol +++ b/contracts/stubs/ERC20WrapperStub.sol @@ -7,9 +7,10 @@ import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; +import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; // represents wstETH on L1 -contract ERC20WrapperStub is IERC20WstETH, ERC20 { +contract ERC20WrapperStub is IERC20Wrapper, IERC20WstETH, ERC20 { IERC20 public stETH; address public bridge; diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts index 774811c2..05236679 100644 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L1ERC20TokenBridge.unit.test.ts @@ -2,14 +2,18 @@ import { assert } from "chai"; import hre, { ethers } from "hardhat"; import { ERC20BridgedStub__factory, + ERC20WrapperStub__factory, L1ERC20TokenBridge__factory, L2ERC20TokenBridge__factory, OssifiableProxy__factory, EmptyContractStub__factory, } 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"; +import { BigNumber } from "ethers"; +import { ERC20WrapperStub } from "../../typechain"; unit("Optimism :: L1ERC20TokenBridge", ctxFactory) .test("l2TokenBridge()", async (ctx) => { @@ -26,33 +30,54 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) await assert.revertsWith( ctx.l1TokenBridge.depositERC20( - ctx.stubs.l1Token.address, - ctx.stubs.l2Token.address, + 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("depositsERC20() :: wrong l1Token address", async (ctx) => { await assert.revertsWith( ctx.l1TokenBridge.depositERC20( ctx.accounts.stranger.address, - ctx.stubs.l2Token.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("depositsERC20() :: wrong l2Token address", async (ctx) => { await assert.revertsWith( ctx.l1TokenBridge.depositERC20( - ctx.stubs.l1Token.address, + ctx.stubs.l1TokenNonRebasable.address, ctx.accounts.stranger.address, wei`1 ether`, wei`1 gwei`, @@ -60,6 +85,16 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "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() :: not from EOA", async (ctx) => { @@ -67,43 +102,55 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ctx.l1TokenBridge .connect(ctx.accounts.emptyContractAsEOA) .depositERC20( - ctx.stubs.l1Token.address, - ctx.stubs.l2Token.address, + 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()", async (ctx) => { + .test("depositERC20() :: non rebasable token flow", async (ctx) => { const { l1TokenBridge, accounts: { deployer, l2TokenBridgeEOA }, - stubs: { l1Token, l2Token, l1Messenger }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, } = ctx; const l2Gas = wei`0.99 wei`; const amount = wei`1 ether`; const data = "0xdeadbeaf"; - await l1Token.approve(l1TokenBridge.address, amount); + await l1TokenNonRebasable.approve(l1TokenBridge.address, amount); - const deployerBalanceBefore = await l1Token.balanceOf(deployer.address); - const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); + const deployerBalanceBefore = await l1TokenNonRebasable.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); const tx = await l1TokenBridge.depositERC20( - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, amount, l2Gas, data ); await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, deployer.address, deployer.address, amount, @@ -116,8 +163,8 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( "finalizeDeposit", [ - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, deployer.address, deployer.address, amount, @@ -129,20 +176,89 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(deployer.address), + await l1TokenNonRebasable.balanceOf(deployer.address), deployerBalanceBefore.sub(amount) ); assert.equalBN( - await l1Token.balanceOf(l1TokenBridge.address), + 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, + amountWrapped, + dataToReceive, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20TokenBridge__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: { l1Token, l2Token }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, accounts: { recipient }, } = ctx; await l1TokenBridge.disableDeposits(); @@ -151,8 +267,8 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) await assert.revertsWith( l1TokenBridge.depositERC20To( - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, recipient.address, wei`1 ether`, wei`1 gwei`, @@ -160,12 +276,24 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorDepositsDisabled()" ); + + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); }) .test("depositsERC20To() :: wrong l1Token address", async (ctx) => { const { l1TokenBridge, - stubs: { l2Token }, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, accounts: { recipient, stranger }, } = ctx; await l1TokenBridge.disableDeposits(); @@ -175,7 +303,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) await assert.revertsWith( l1TokenBridge.depositERC20To( stranger.address, - l2Token.address, + l2TokenNonRebasable.address, recipient.address, wei`1 ether`, wei`1 gwei`, @@ -183,12 +311,23 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorDepositsDisabled()" ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + stranger.address, + l2TokenRebasable.address, + recipient.address, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorDepositsDisabled()" + ); }) .test("depositsERC20To() :: wrong l2Token address", async (ctx) => { const { l1TokenBridge, - stubs: { l1Token }, + stubs: { l1TokenNonRebasable, l1TokenRebasable }, accounts: { recipient, stranger }, } = ctx; await l1TokenBridge.disableDeposits(); @@ -197,7 +336,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) await assert.revertsWith( l1TokenBridge.depositERC20To( - l1Token.address, + l1TokenNonRebasable.address, stranger.address, recipient.address, wei`1 ether`, @@ -206,19 +345,29 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorDepositsDisabled()" ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenRebasable.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 }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable } } = ctx; await assert.revertsWith( l1TokenBridge.depositERC20To( - l1Token.address, - stranger.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, ethers.constants.AddressZero, wei`1 ether`, wei`1 gwei`, @@ -226,27 +375,38 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorAccountIsZeroAddress()" ); + await assert.revertsWith( + l1TokenBridge.depositERC20To( + l1TokenRebasable.address, + l2TokenRebasable.address, + ethers.constants.AddressZero, + wei`1 ether`, + wei`1 gwei`, + "0x" + ), + "ErrorAccountIsZeroAddress()" + ); }) - .test("depositERC20To()", async (ctx) => { + .test("depositERC20To() :: non rebasable token flow", async (ctx) => { const { l1TokenBridge, accounts: { deployer, l2TokenBridgeEOA, recipient }, - stubs: { l1Token, l2Token, l1Messenger }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, } = ctx; const l2Gas = wei`0.99 wei`; const amount = wei`1 ether`; const data = "0x"; - await l1Token.approve(l1TokenBridge.address, amount); + await l1TokenNonRebasable.approve(l1TokenBridge.address, amount); - const deployerBalanceBefore = await l1Token.balanceOf(deployer.address); - const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); + const deployerBalanceBefore = await l1TokenNonRebasable.balanceOf(deployer.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); const tx = await l1TokenBridge.depositERC20To( - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, recipient.address, amount, l2Gas, @@ -254,8 +414,8 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ); await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, deployer.address, recipient.address, amount, @@ -268,8 +428,8 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( "finalizeDeposit", [ - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, deployer.address, recipient.address, amount, @@ -281,22 +441,94 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(deployer.address), + await l1TokenNonRebasable.balanceOf(deployer.address), deployerBalanceBefore.sub(amount) ); assert.equalBN( - await l1Token.balanceOf(l1TokenBridge.address), + 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, + amountWrapped, + dataToReceive, + ]); + + await assert.emits(l1Messenger, tx, "SentMessage", [ + l2TokenBridgeEOA.address, + l1TokenBridge.address, + L2ERC20TokenBridge__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: { l1Token, l2Token }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, accounts: { deployer, recipient, l2TokenBridgeEOA }, } = ctx; await l1TokenBridge.disableWithdrawals(); @@ -307,8 +539,21 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l1TokenBridge .connect(l2TokenBridgeEOA) .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, + 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`, @@ -322,7 +567,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) .test("finalizeERC20Withdrawal() :: wrong l1Token", async (ctx) => { const { l1TokenBridge, - stubs: { l2Token }, + stubs: { l2TokenNonRebasable, l2TokenRebasable }, accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, } = ctx; @@ -331,7 +576,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) .connect(l2TokenBridgeEOA) .finalizeERC20Withdrawal( stranger.address, - l2Token.address, + l2TokenNonRebasable.address, deployer.address, recipient.address, wei`1 ether`, @@ -339,12 +584,26 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorUnsupportedL1Token()" ); + + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + stranger.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL1Token()" + ); }) .test("finalizeERC20Withdrawal() :: wrong l2Token", async (ctx) => { const { l1TokenBridge, - stubs: { l1Token }, + stubs: { l1TokenNonRebasable, l1TokenRebasable }, accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, } = ctx; @@ -352,7 +611,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l1TokenBridge .connect(l2TokenBridgeEOA) .finalizeERC20Withdrawal( - l1Token.address, + l1TokenNonRebasable.address, stranger.address, deployer.address, recipient.address, @@ -361,12 +620,26 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorUnsupportedL2Token()" ); + + await assert.revertsWith( + l1TokenBridge + .connect(l2TokenBridgeEOA) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + stranger.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnsupportedL2Token()" + ); }) .test("finalizeERC20Withdrawal() :: unauthorized messenger", async (ctx) => { const { l1TokenBridge, - stubs: { l1Token, l2Token }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, accounts: { deployer, recipient, stranger }, } = ctx; @@ -374,8 +647,8 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l1TokenBridge .connect(stranger) .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, deployer.address, recipient.address, wei`1 ether`, @@ -383,6 +656,19 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ), "ErrorUnauthorizedMessenger()" ); + await assert.revertsWith( + l1TokenBridge + .connect(stranger) + .finalizeERC20Withdrawal( + l1TokenRebasable.address, + l2TokenRebasable.address, + deployer.address, + recipient.address, + wei`1 ether`, + "0x" + ), + "ErrorUnauthorizedMessenger()" + ); }) .test( @@ -390,7 +676,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) async (ctx) => { const { l1TokenBridge, - stubs: { l1Token, l2Token, l1Messenger }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable, l1Messenger }, accounts: { deployer, recipient, stranger, l1MessengerStubAsEOA }, } = ctx; @@ -400,8 +686,21 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l1TokenBridge .connect(l1MessengerStubAsEOA) .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, + 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`, @@ -412,25 +711,74 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) } ) - .test("finalizeERC20Withdrawal()", async (ctx) => { + .test("finalizeERC20Withdrawal() :: non rebasable token flow", async (ctx) => { const { l1TokenBridge, - stubs: { l1Token, l2Token, l1Messenger }, + stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, accounts: { deployer, recipient, l1MessengerStubAsEOA, l2TokenBridgeEOA }, } = ctx; await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); - const bridgeBalanceBefore = await l1Token.balanceOf(l1TokenBridge.address); + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + + 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; 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 deployerBalanceBefore = await l1TokenRebasable.balanceOf(deployer.address); + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + await l1TokenRebasable.transfer(l1TokenNonRebasable.address, wei`100 ether`); + + const bridgeBalanceBefore = await l1TokenRebasable.balanceOf(l1TokenBridge.address); const tx = await l1TokenBridge .connect(l1MessengerStubAsEOA) .finalizeERC20Withdrawal( - l1Token.address, - l2Token.address, + l1TokenRebasable.address, + l2TokenRebasable.address, deployer.address, recipient.address, amount, @@ -438,17 +786,17 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) ); await assert.emits(l1TokenBridge, tx, "ERC20WithdrawalFinalized", [ - l1Token.address, - l2Token.address, + l1TokenRebasable.address, + l2TokenRebasable.address, deployer.address, recipient.address, amount, data, ]); - assert.equalBN(await l1Token.balanceOf(recipient.address), amount); + assert.equalBN(await l1TokenRebasable.balanceOf(recipient.address), amountUnwrapped); assert.equalBN( - await l1Token.balanceOf(l1TokenBridge.address), + await l1TokenNonRebasable.balanceOf(l1TokenBridge.address), bridgeBalanceBefore.sub(amount) ); }) @@ -456,21 +804,35 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) .run(); async function ctxFactory() { - const [deployer, l2TokenBridgeEOA, stranger, recipient, rebasableToken] = + 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 l1TokenStub = await new ERC20BridgedStub__factory(deployer).deploy( - "L1 Token", - "L1" + 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 l2TokenStub = await new ERC20BridgedStub__factory(deployer).deploy( - "L2 Token", - "L2" + 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({ @@ -487,10 +849,10 @@ async function ctxFactory() { ).deploy( l1MessengerStub.address, l2TokenBridgeEOA.address, - l1TokenStub.address, - rebasableToken.address, - l2TokenStub.address, - rebasableToken.address + l1TokenNonRebasableStub.address, + l1TokenRebasableStub.address, + l2TokenNonRebasableStub.address, + l2TokenRebasableStub.address ); const l1TokenBridgeProxy = await new OssifiableProxy__factory( @@ -508,7 +870,8 @@ async function ctxFactory() { deployer ); - await l1TokenStub.transfer(l1TokenBridge.address, wei`100 ether`); + 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(), @@ -525,6 +888,7 @@ async function ctxFactory() { await l1TokenBridge.enableWithdrawals(); return { + provider: provider, accounts: { deployer, stranger, @@ -534,12 +898,21 @@ async function ctxFactory() { l1MessengerStubAsEOA, }, stubs: { - l1Token: l1TokenStub, - l2Token: l2TokenStub, - l1TokenRebasable: l1TokenStub, - l2TokenRebasable: l2TokenStub, + 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/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index 20c594c9..17bbb425 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -1,456 +1,811 @@ -import hre from "hardhat"; +import hre, { ethers } from "hardhat"; import { - ERC20BridgedStub__factory, - L1ERC20TokenBridge__factory, - L2ERC20TokenBridge__factory, - OssifiableProxy__factory, - EmptyContractStub__factory, - CrossDomainMessengerStub__factory, + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + TokenRateOracle__factory, + ERC20Rebasable__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"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { getContractAddress } from "ethers/lib/utils"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { BigNumber } from "ethers"; 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, - ]); + .test("l1TokenBridge()", async (ctx) => { + assert.equal( + await ctx.l2TokenBridge.l1TokenBridge(), + ctx.accounts.l1TokenBridgeEOA.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() :: 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, + L1ERC20TokenBridge__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, + 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, + 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, + L1ERC20TokenBridge__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("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, + L1ERC20TokenBridge__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, + L1ERC20TokenBridge__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("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() :: 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 tx = await l2TokenBridge + .connect(l2MessengerStubEOA) + .finalizeDeposit( + l1TokenNonRebasable.address, + l2TokenNonRebasable.address, + deployer.address, + recipient.address, + amount, + data + ); + + 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(); - assert.equalBN( - await l2TokenStub.balanceOf(deployer.address), - deployerBalanceBefore.sub(amount) +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 [ + l1TokenRebasableAddress, + l1TokenNonRebasableAddress, + l2TokenNonRebasableAddress, + tokenRateOracleAddress, + l2TokenRebasableAddress, + l2TokenBridgeImplAddress, + l2TokenBridgeProxyAddress + ] = await predictAddresses(deployer, 7); + + const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L1 Token Rebasable", + "L1R" ); - 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 + const l1TokenNonRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( + l1TokenRebasableStub.address, + "L1 Token Non Rebasable", + "L1NR" ); - 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) + const l2TokenNonRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( + "L2 Token Non Rebasable", + "L2NR" ); - 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()" + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + l2TokenBridgeProxyAddress, + 86400 ); - }) - - .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; + const l2TokenRebasableStub = await new ERC20Rebasable__factory(deployer).deploy( + "L2 Token Rebasable", + "L2R", + decimals, + l2TokenNonRebasableStub.address, + tokenRateOracle.address, + l2TokenBridgeProxyAddress + ); - await l2Messenger.setXDomainMessageSender(l1TokenBridgeEOA.address); + const l2TokenBridgeImpl = await new L2ERC20TokenBridge__factory( + deployer + ).deploy( + l2MessengerStub.address, + l1TokenBridgeEOA.address, + l1TokenNonRebasableStub.address, + l1TokenRebasableStub.address, + l2TokenNonRebasableStub.address, + l2TokenRebasableStub.address + ); - const totalSupplyBefore = await l2Token.totalSupply(); + const l2TokenBridgeProxy = await new OssifiableProxy__factory( + deployer + ).deploy( + l2TokenBridgeImpl.address, + deployer.address, + l2TokenBridgeImpl.interface.encodeFunctionData("initialize", [ + deployer.address, + ]) + ); - const amount = wei`1 ether`; - const data = "0xdeadbeaf"; + const l2TokenBridge = L2ERC20TokenBridge__factory.connect( + l2TokenBridgeProxy.address, + deployer + ); - 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, + const roles = await Promise.all([ + l2TokenBridge.DEPOSITS_ENABLER_ROLE(), + l2TokenBridge.DEPOSITS_DISABLER_ROLE(), + l2TokenBridge.WITHDRAWALS_ENABLER_ROLE(), + l2TokenBridge.WITHDRAWALS_DISABLER_ROLE(), ]); - assert.equalBN(await l2Token.balanceOf(recipient.address), amount); - assert.equalBN(await l2Token.totalSupply(), totalSupplyBefore.add(amount)); - }) + 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 + }; +} - .run(); +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 ctxFactory() { - const [deployer, stranger, recipient, l1TokenBridgeEOA, rebasableToken] = - 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, - rebasableToken.address, - l2Token.address, - rebasableToken.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, - }; +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]); } diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index a2bde5e0..56aafc81 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -44,6 +44,17 @@ unit("TokenRateOracle", ctxFactory) await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "ErrorIncorrectRateTimestamp()"); }) + .test("updateRate() :: dont update state if values are the same", async (ctx) => { + const { tokenRateOracle } = ctx.contracts; + const { bridge } = ctx.accounts; + + const tx1 = await tokenRateOracle.connect(bridge).updateRate(10, 1000); + await assert.emits(tokenRateOracle, tx1, "RateUpdated", [10, 1000]); + + const tx2 = await tokenRateOracle.connect(bridge).updateRate(10, 1000); + await assert.notEmits(tokenRateOracle, tx2, "RateUpdated"); + }) + .test("updateRate() :: happy path", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { bridge } = ctx.accounts; From 1cb344aa7b0355dadd8d95774111986fa4219d47 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 13 Feb 2024 11:23:11 +0100 Subject: [PATCH 27/61] fix evetns + formating --- contracts/optimism/L1ERC20TokenBridge.sol | 102 ++++++++++++++---- contracts/optimism/L2ERC20TokenBridge.sol | 62 +++++++++-- test/optimism/L1ERC20TokenBridge.unit.test.ts | 10 +- .../bridging-rebase.integration.test.ts | 8 +- 4 files changed, 140 insertions(+), 42 deletions(-) diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 61cdb73a..be7852ed 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -116,12 +116,28 @@ contract L1ERC20TokenBridge is onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) { if (isRebasableTokenFlow(l1Token_, l2Token_)) { - uint256 stETHAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); - IERC20(L1_TOKEN_REBASABLE).safeTransfer(to_, stETHAmount); - emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + uint256 rebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); + IERC20(L1_TOKEN_REBASABLE).safeTransfer(to_, rebasableTokenAmount); + + emit ERC20WithdrawalFinalized( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + from_, + to_, + rebasableTokenAmount, + data_ + ); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(L1_TOKEN_NON_REBASABLE).safeTransfer(to_, amount_); - emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + + emit ERC20WithdrawalFinalized( + L1_TOKEN_NON_REBASABLE, + L2_TOKEN_NON_REBASABLE, + from_, + to_, + amount_, + data_ + ); } } @@ -134,31 +150,81 @@ contract L1ERC20TokenBridge is bytes memory data_ ) internal { if (isRebasableTokenFlow(l1Token_, l2Token_)) { - DepositData memory depositData = DepositData({ rate: uint96(L1_TOKEN_NON_REBASABLE_ADAPTER.tokenRate()), timestamp: uint40(block.timestamp), data: data_ }); - bytes memory encodedDepositData = encodeDepositData(depositData); if (amount_ == 0) { - _initiateERC20Deposit(l1Token_, l2Token_, msg.sender, to_, amount_, l2Gas_, encodedDepositData); + _initiateERC20Deposit( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + msg.sender, + to_, + 0, + l2Gas_, + encodedDepositData + ); + + emit ERC20DepositInitiated( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + msg.sender, + to_, + 0, + encodedDepositData + ); + return; } - // maybe loosing 1 wei for stETH. Check another method IERC20(L1_TOKEN_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); - if(!IERC20(L1_TOKEN_REBASABLE).approve(L1_TOKEN_NON_REBASABLE, amount_)) revert ErrorRebasableTokenApprove(); + if(!IERC20(L1_TOKEN_REBASABLE).approve(L1_TOKEN_NON_REBASABLE, amount_)) { + revert ErrorRebasableTokenApprove(); + } + uint256 nonRebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).wrap(amount_); - // when 1 wei wasnt't transfer, can this wrap be failed? - uint256 wstETHAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).wrap(amount_); - _initiateERC20Deposit(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, wstETHAmount, l2Gas_, encodedDepositData); + _initiateERC20Deposit( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + msg.sender, + to_, + nonRebasableTokenAmount, + l2Gas_, + encodedDepositData + ); + emit ERC20DepositInitiated( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + msg.sender, + to_, + amount_, + encodedDepositData + ); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(L1_TOKEN_NON_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); - _initiateERC20Deposit(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, l2Gas_, data_); + + _initiateERC20Deposit( + L1_TOKEN_NON_REBASABLE, + L2_TOKEN_NON_REBASABLE, + msg.sender, + to_, + amount_, + l2Gas_, + data_ + ); + + emit ERC20DepositInitiated( + L1_TOKEN_NON_REBASABLE, + L2_TOKEN_NON_REBASABLE, + msg.sender, + to_, + amount_, + data_ + ); } } @@ -180,7 +246,6 @@ contract L1ERC20TokenBridge is uint32 l2Gas_, bytes memory data_ ) internal { - bytes memory message = abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, l1Token_, @@ -192,15 +257,6 @@ contract L1ERC20TokenBridge is ); sendCrossDomainMessage(L2_TOKEN_BRIDGE, l2Gas_, message); - - emit ERC20DepositInitiated( - l1Token_, - l2Token_, - from_, - to_, - amount_, - data_ - ); } error ErrorSenderNotEOA(); diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index ae195718..cb8cc58f 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -97,12 +97,28 @@ contract L2ERC20TokenBridge is DepositData memory depositData = decodeDepositData(data_); ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); + ERC20Rebasable(L2_TOKEN_REBASABLE).mintShares(to_, amount_); + uint256 rebasableTokenAmount = ERC20Rebasable(L2_TOKEN_REBASABLE).getTokensByShares(amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, rebasableTokenAmount, depositData.data); + emit DepositFinalized( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + from_, + to_, + rebasableTokenAmount, + depositData.data + ); } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeMint(to_, amount_); - emit DepositFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); + emit DepositFinalized( + L1_TOKEN_NON_REBASABLE, + L2_TOKEN_NON_REBASABLE, + from_, + to_, + amount_, + data_ + ); } } @@ -114,19 +130,46 @@ contract L2ERC20TokenBridge is bytes calldata data_ ) internal { if (l2Token_ == L2_TOKEN_REBASABLE) { - - // TODO: maybe loosing 1 wei here as well uint256 shares = ERC20Rebasable(L2_TOKEN_REBASABLE).getSharesByTokens(amount_); ERC20Rebasable(L2_TOKEN_REBASABLE).burnShares(msg.sender, shares); - _initiateWithdrawal(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, shares, l1Gas_, data_); - emit WithdrawalInitiated(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, msg.sender, to_, amount_, data_); + _initiateWithdrawal( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + msg.sender, + to_, + shares, + l1Gas_, + data_ + ); + emit WithdrawalInitiated( + L1_TOKEN_REBASABLE, + L2_TOKEN_REBASABLE, + msg.sender, + to_, + amount_, + data_ + ); } else if (l2Token_ == L2_TOKEN_NON_REBASABLE) { - IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeBurn(msg.sender, amount_); - _initiateWithdrawal(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, l1Gas_, data_); - emit WithdrawalInitiated(L1_TOKEN_NON_REBASABLE, L2_TOKEN_NON_REBASABLE, msg.sender, to_, amount_, data_); + _initiateWithdrawal( + L1_TOKEN_NON_REBASABLE, + L2_TOKEN_NON_REBASABLE, + msg.sender, + to_, + amount_, + l1Gas_, + data_ + ); + emit WithdrawalInitiated( + L1_TOKEN_NON_REBASABLE, + L2_TOKEN_NON_REBASABLE, + msg.sender, + to_, + amount_, + data_ + ); } } @@ -148,7 +191,6 @@ contract L2ERC20TokenBridge is uint32 l1Gas_, bytes memory data_ ) internal { - bytes memory message = abi.encodeWithSelector( IL1ERC20Bridge.finalizeERC20Withdrawal.selector, l1Token_, diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts index 05236679..e89ecc8e 100644 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L1ERC20TokenBridge.unit.test.ts @@ -222,7 +222,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l2TokenRebasable.address, deployer.address, deployer.address, - amountWrapped, + amount, dataToReceive, ]); @@ -474,8 +474,8 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); const tx = await l1TokenBridge.depositERC20To( - l1TokenRebasable.address, - l2TokenRebasable.address, + l1TokenRebasable.address, + l2TokenRebasable.address, recipient.address, amount, l2Gas, @@ -490,7 +490,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l2TokenRebasable.address, deployer.address, recipient.address, - amountWrapped, + amount, dataToReceive, ]); @@ -790,7 +790,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) l2TokenRebasable.address, deployer.address, recipient.address, - amount, + amountUnwrapped, data, ]); diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 757f6188..1ec08c0a 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -359,7 +359,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - depositAmountNonRebasable, + depositAmountRebasable, dataToSend, ]); @@ -546,7 +546,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderA.address, - withdrawalAmountNonRebasable, + withdrawalAmountRebasable, "0x", ]); @@ -609,7 +609,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderA.address, tokenHolderB.address, - depositAmountNonRebasable, + depositAmountRebasable, dataToSend, ]); @@ -813,7 +813,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable.address, tokenHolderB.address, tokenHolderA.address, - withdrawalAmountNonRebasable, + withdrawalAmountRebasable, "0x", ]); From 3fd1e94d555fb2b7b9f6a1f04b8c87c75b3691ab Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 6 Mar 2024 10:29:34 +0200 Subject: [PATCH 28/61] move token rate to the base contract --- contracts/optimism/L1ERC20TokenBridge.sol | 35 +++++++++++++++---- contracts/stubs/ERC20WrapperStub.sol | 2 +- .../token/L1TokenNonRebasableAdapter.sol | 23 ------------ contracts/token/interfaces/IERC20WstETH.sol | 2 +- test/optimism/L1ERC20TokenBridge.unit.test.ts | 12 +++---- test/optimism/L2ERC20TokenBridge.unit.test.ts | 4 +-- .../bridging-rebase.integration.test.ts | 6 ++-- 7 files changed, 42 insertions(+), 42 deletions(-) delete mode 100644 contracts/token/L1TokenNonRebasableAdapter.sol diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index be7852ed..dc72fd1b 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -10,17 +10,17 @@ 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 {L1TokenNonRebasableAdapter} from "../token/L1TokenNonRebasableAdapter.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "./DepositDataCodec.sol"; +import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.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 -contract L1ERC20TokenBridge is +abstract contract L1ERC20TokenBridgeBase is IL1ERC20Bridge, BridgingManager, RebasableAndNonRebasableTokens, @@ -31,8 +31,6 @@ contract L1ERC20TokenBridge is address public immutable L2_TOKEN_BRIDGE; - L1TokenNonRebasableAdapter public immutable L1_TOKEN_NON_REBASABLE_ADAPTER; - /// @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 @@ -48,9 +46,10 @@ contract L1ERC20TokenBridge is address l2TokenRebasable_ ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { L2_TOKEN_BRIDGE = l2TokenBridge_; - L1_TOKEN_NON_REBASABLE_ADAPTER = new L1TokenNonRebasableAdapter(l1TokenNonRebasable_); } + function tokenRate() virtual internal view returns (uint256); + /// @notice Pushes token rate to L2 by depositing zero tokens. /// @param l2Gas_ Gas limit required to complete the deposit on L2. function pushTokenRate(uint32 l2Gas_) external { @@ -151,7 +150,7 @@ contract L1ERC20TokenBridge is ) internal { if (isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ - rate: uint96(L1_TOKEN_NON_REBASABLE_ADAPTER.tokenRate()), + rate: uint96(tokenRate()), timestamp: uint40(block.timestamp), data: data_ }); @@ -262,3 +261,27 @@ contract L1ERC20TokenBridge is error ErrorSenderNotEOA(); error ErrorRebasableTokenApprove(); } + +contract L1ERC20TokenBridge is L1ERC20TokenBridgeBase { + + constructor( + address messenger_, + address l2TokenBridge_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) L1ERC20TokenBridgeBase( + messenger_, + l2TokenBridge_, + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { + } + + function tokenRate() override internal view returns (uint256) { + return IERC20WstETH(L1_TOKEN_NON_REBASABLE).stEthPerToken(); + } +} diff --git a/contracts/stubs/ERC20WrapperStub.sol b/contracts/stubs/ERC20WrapperStub.sol index 2adaa9e9..b23817bc 100644 --- a/contracts/stubs/ERC20WrapperStub.sol +++ b/contracts/stubs/ERC20WrapperStub.sol @@ -47,7 +47,7 @@ contract ERC20WrapperStub is IERC20Wrapper, IERC20WstETH, ERC20 { return stETHAmount; } - function stETHPerToken() external view returns (uint256) { + function stEthPerToken() external view returns (uint256) { return tokensRate; } } diff --git a/contracts/token/L1TokenNonRebasableAdapter.sol b/contracts/token/L1TokenNonRebasableAdapter.sol deleted file mode 100644 index 1d943853..00000000 --- a/contracts/token/L1TokenNonRebasableAdapter.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {IERC20TokenRate} from "../token/interfaces/IERC20TokenRate.sol"; -import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; - -/// @author kovalgek -/// @notice Hides wstETH concept from other contracts to save level of abstraction. -contract L1TokenNonRebasableAdapter is IERC20TokenRate { - - IERC20WstETH public immutable WSTETH; - - constructor(address wstETH_) { - WSTETH = IERC20WstETH(wstETH_); - } - - /// @inheritdoc IERC20TokenRate - function tokenRate() external view returns (uint256) { - return WSTETH.stETHPerToken(); - } -} diff --git a/contracts/token/interfaces/IERC20WstETH.sol b/contracts/token/interfaces/IERC20WstETH.sol index 19a9badb..4bb216c4 100644 --- a/contracts/token/interfaces/IERC20WstETH.sol +++ b/contracts/token/interfaces/IERC20WstETH.sol @@ -10,5 +10,5 @@ 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); + function stEthPerToken() external view returns (uint256); } diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts index e89ecc8e..00f269e1 100644 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L1ERC20TokenBridge.unit.test.ts @@ -196,7 +196,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) const l2Gas = wei`0.99 wei`; const amount = wei`1 ether`; const data = "0xdeadbeaf"; - const rate = await l1TokenNonRebasable.stETHPerToken(); + const rate = await l1TokenNonRebasable.stEthPerToken(); const decimalsStr = await l1TokenNonRebasable.decimals(); const decimals = BigNumber.from(10).pow(decimalsStr); @@ -462,7 +462,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) const amount = wei`1 ether`; const data = "0x"; - const rate = await l1TokenNonRebasable.stETHPerToken(); + const rate = await l1TokenNonRebasable.stEthPerToken(); const decimalsStr = await l1TokenNonRebasable.decimals(); const decimals = BigNumber.from(10).pow(decimalsStr); @@ -761,7 +761,7 @@ unit("Optimism :: L1ERC20TokenBridge", ctxFactory) const amount = wei`1 ether`; const data = "0xdeadbeaf"; - const rate = await l1TokenNonRebasable.stETHPerToken(); + const rate = await l1TokenNonRebasable.stEthPerToken(); const decimalsStr = await l1TokenNonRebasable.decimals(); const decimals = BigNumber.from(10).pow(decimalsStr); @@ -909,10 +909,10 @@ async function ctxFactory() { } async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapperStub) { - const stETHPerToken = await l1Token.stETHPerToken(); + 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 stEthPerTokenStr = ethers.utils.hexZeroPad(stEthPerToken.toHexString(), 12); const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); - return ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); } diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index 17bbb425..4695718d 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -805,7 +805,7 @@ async function predictAddresses(account: SignerWithAddress, txsCount: number) { 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 stEthPerTokenStr = ethers.utils.hexZeroPad(tokenRate.toHexString(), 12); const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); - return ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); } diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 1ec08c0a..7ff06ba1 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -918,10 +918,10 @@ async function ctxFactory() { } async function packedTokenRateAndTimestamp(l1Provider: JsonRpcProvider, l1Token: ERC20WrapperStub) { - const stETHPerToken = await l1Token.stETHPerToken(); + 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 stEthPerTokenStr = ethers.utils.hexZeroPad(stEthPerToken.toHexString(), 12); const blockTimestampStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(blockTimestamp), 5); - return ethers.utils.hexConcat([stETHPerTokenStr, blockTimestampStr]); + return ethers.utils.hexConcat([stEthPerTokenStr, blockTimestampStr]); } From 66b250119a702ccdd07f3a7e5932ea5ca041e075 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 6 Mar 2024 15:20:51 +0200 Subject: [PATCH 29/61] use inheritance for token rate in l1 bridge, fix e2e tests --- contracts/optimism/L1ERC20TokenBridge.sol | 26 +---- contracts/optimism/L1LidoTokensBridge.sol | 33 ++++++ test/optimism/L1ERC20TokenBridge.unit.test.ts | 8 +- test/optimism/L2ERC20TokenBridge.unit.test.ts | 10 +- test/optimism/_launch.test.ts | 26 ++--- .../bridging-rebase.integration.test.ts | 110 +++++++++--------- test/optimism/bridging-to.e2e.test.ts | 10 +- test/optimism/bridging.e2e.test.ts | 12 +- test/optimism/bridging.integration.test.ts | 74 ++++++------ test/optimism/deployment.acceptance.test.ts | 36 +++--- test/optimism/deposit-gas-estimation.test.ts | 34 +++--- utils/optimism/LidoBridgeAdapter.ts | 45 +++++++ utils/optimism/deployment.ts | 7 +- utils/optimism/testing.ts | 30 ++--- 14 files changed, 257 insertions(+), 204 deletions(-) create mode 100644 contracts/optimism/L1LidoTokensBridge.sol create mode 100644 utils/optimism/LidoBridgeAdapter.ts diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index dc72fd1b..7b30efe2 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -20,7 +20,7 @@ import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; /// @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 L1ERC20TokenBridgeBase is +abstract contract L1ERC20TokenBridge is IL1ERC20Bridge, BridgingManager, RebasableAndNonRebasableTokens, @@ -261,27 +261,3 @@ abstract contract L1ERC20TokenBridgeBase is error ErrorSenderNotEOA(); error ErrorRebasableTokenApprove(); } - -contract L1ERC20TokenBridge is L1ERC20TokenBridgeBase { - - constructor( - address messenger_, - address l2TokenBridge_, - address l1TokenNonRebasable_, - address l1TokenRebasable_, - address l2TokenNonRebasable_, - address l2TokenRebasable_ - ) L1ERC20TokenBridgeBase( - 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/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol new file mode 100644 index 00000000..b78e223e --- /dev/null +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {L1ERC20TokenBridge} from "./L1ERC20TokenBridge.sol"; +import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; + +/// @author kovalgek +/// @notice Hides wstETH concept from other contracts to save level of abstraction. +contract L1LidoTokensBridge is L1ERC20TokenBridge { + + constructor( + address messenger_, + address l2TokenBridge_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) L1ERC20TokenBridge( + messenger_, + l2TokenBridge_, + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { + } + + function tokenRate() override internal view returns (uint256) { + return IERC20WstETH(L1_TOKEN_NON_REBASABLE).stEthPerToken(); + } +} diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts index 00f269e1..efafeb57 100644 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L1ERC20TokenBridge.unit.test.ts @@ -3,7 +3,7 @@ import hre, { ethers } from "hardhat"; import { ERC20BridgedStub__factory, ERC20WrapperStub__factory, - L1ERC20TokenBridge__factory, + L1LidoTokensBridge__factory, L2ERC20TokenBridge__factory, OssifiableProxy__factory, EmptyContractStub__factory, @@ -15,7 +15,7 @@ import { wei } from "../../utils/wei"; import { BigNumber } from "ethers"; import { ERC20WrapperStub } from "../../typechain"; -unit("Optimism :: L1ERC20TokenBridge", ctxFactory) +unit("Optimism :: L1LidoTokensBridge", ctxFactory) .test("l2TokenBridge()", async (ctx) => { assert.equal( await ctx.l1TokenBridge.l2TokenBridge(), @@ -844,7 +844,7 @@ async function ctxFactory() { l1MessengerStub.address ); - const l1TokenBridgeImpl = await new L1ERC20TokenBridge__factory( + const l1TokenBridgeImpl = await new L1LidoTokensBridge__factory( deployer ).deploy( l1MessengerStub.address, @@ -865,7 +865,7 @@ async function ctxFactory() { ]) ); - const l1TokenBridge = L1ERC20TokenBridge__factory.connect( + const l1TokenBridge = L1LidoTokensBridge__factory.connect( l1TokenBridgeProxy.address, deployer ); diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index 4695718d..a3906b0b 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -4,7 +4,7 @@ import { ERC20WrapperStub__factory, TokenRateOracle__factory, ERC20Rebasable__factory, - L1ERC20TokenBridge__factory, + L1LidoTokensBridge__factory, L2ERC20TokenBridge__factory, OssifiableProxy__factory, EmptyContractStub__factory, @@ -105,7 +105,7 @@ unit("Optimism:: L2ERC20TokenBridge", ctxFactory) await assert.emits(l2Messenger, tx, "SentMessage", [ l1TokenBridgeEOA.address, l2TokenBridge.address, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( "finalizeERC20Withdrawal", [ l1TokenNonRebasable.address, @@ -184,7 +184,7 @@ unit("Optimism:: L2ERC20TokenBridge", ctxFactory) await assert.emits(l2Messenger, tx, "SentMessage", [ l1TokenBridgeEOA.address, l2TokenBridge.address, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( "finalizeERC20Withdrawal", [ l1TokenRebasable.address, @@ -298,7 +298,7 @@ unit("Optimism:: L2ERC20TokenBridge", ctxFactory) await assert.emits(l2MessengerStub, tx, "SentMessage", [ l1TokenBridgeEOA.address, l2TokenBridge.address, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( "finalizeERC20Withdrawal", [ l1TokenNonRebasable.address, @@ -379,7 +379,7 @@ unit("Optimism:: L2ERC20TokenBridge", ctxFactory) await assert.emits(l2Messenger, tx, "SentMessage", [ l1TokenBridgeEOA.address, l2TokenBridge.address, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( "finalizeERC20Withdrawal", [ l1TokenRebasable.address, 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-rebase.integration.test.ts b/test/optimism/bridging-rebase.integration.test.ts index 7ff06ba1..3d5f62a8 100644 --- a/test/optimism/bridging-rebase.integration.test.ts +++ b/test/optimism/bridging-rebase.integration.test.ts @@ -16,13 +16,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .step("Activate bridging on L1", async (ctx) => { - const { l1ERC20TokenBridge } = ctx; + const { l1LidoTokensBridge } = ctx; const { l1ERC20TokenBridgeAdmin } = ctx.accounts; - const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { - await l1ERC20TokenBridge + await l1LidoTokensBridge .connect(l1ERC20TokenBridgeAdmin) .enableDeposits(); } else { @@ -30,18 +30,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } const isWithdrawalsEnabled = - await l1ERC20TokenBridge.isWithdrawalsEnabled(); + await l1LidoTokensBridge.isWithdrawalsEnabled(); if (!isWithdrawalsEnabled) { - await l1ERC20TokenBridge + await l1LidoTokensBridge .connect(l1ERC20TokenBridgeAdmin) .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) => { @@ -79,7 +79,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l1TokenRebasable, l2TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, l2Provider @@ -93,7 +93,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2ERC20TokenBridge.address, 0, 300_000, @@ -113,7 +113,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -127,16 +127,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(l1Stranger) .pushTokenRate(200_000); const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, l1Stranger.address, @@ -161,14 +161,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore ); @@ -182,7 +182,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -193,17 +193,17 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await l1TokenRebasable .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, 0); + .approve(l1LidoTokensBridge.address, 0); const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1TokenRebasable.address, @@ -215,7 +215,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -240,14 +240,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore ); @@ -262,7 +262,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l1TokenRebasable, l2TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, l2Provider @@ -283,7 +283,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2ERC20TokenBridge.address, 0, 300_000, @@ -321,7 +321,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -332,17 +332,17 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await l1TokenRebasable .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, depositAmountRebasable); + .approve(l1LidoTokensBridge.address, depositAmountRebasable); const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1TokenRebasable.address, @@ -354,7 +354,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -379,14 +379,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) ); @@ -401,7 +401,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l1TokenRebasable, l2TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, l2Provider @@ -422,7 +422,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2ERC20TokenBridge.address, 0, 300_000, @@ -503,7 +503,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l1TokenRebasable, l1CrossDomainMessenger, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2TokenRebasable, l2ERC20TokenBridge, @@ -515,7 +515,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); await l1CrossDomainMessenger @@ -525,9 +525,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tx = await l1CrossDomainMessenger .connect(l1Stranger) .relayMessage( - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2CrossDomainMessenger.address, - l1ERC20TokenBridge.interface.encodeFunctionData( + l1LidoTokensBridge.interface.encodeFunctionData( "finalizeERC20Withdrawal", [ l1TokenRebasable.address, @@ -541,7 +541,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 0 ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20WithdrawalFinalized", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -551,7 +551,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) ); @@ -567,7 +567,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -582,16 +582,16 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await l1TokenRebasable .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, depositAmountRebasable); + .approve(l1LidoTokensBridge.address, depositAmountRebasable); const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20To( l1TokenRebasable.address, @@ -604,7 +604,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -629,14 +629,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) ); @@ -650,7 +650,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2TokenRebasable, l2CrossDomainMessenger, l2ERC20TokenBridge, @@ -680,7 +680,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2ERC20TokenBridge.address, 0, 300_000, @@ -763,7 +763,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l1TokenRebasable, l1CrossDomainMessenger, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2TokenRebasable, l2ERC20TokenBridge, @@ -782,7 +782,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); await l1CrossDomainMessenger @@ -792,9 +792,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tx = await l1CrossDomainMessenger .connect(l1Stranger) .relayMessage( - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2CrossDomainMessenger.address, - l1ERC20TokenBridge.interface.encodeFunctionData( + l1LidoTokensBridge.interface.encodeFunctionData( "finalizeERC20Withdrawal", [ l1TokenRebasable.address, @@ -808,7 +808,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 0 ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20WithdrawalFinalized", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20WithdrawalFinalized", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderB.address, @@ -818,7 +818,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) ); @@ -889,7 +889,7 @@ async function ctxFactory() { l2Provider ); - await contracts.l1ERC20TokenBridge.connect(l1ERC20TokenBridgeAdmin).pushTokenRate(1000000); + await contracts.l1LidoTokensBridge.connect(l1ERC20TokenBridgeAdmin).pushTokenRate(1000000); return { l1Provider, diff --git a/test/optimism/bridging-to.e2e.test.ts b/test/optimism/bridging-to.e2e.test.ts index a7a0dbc6..5c9788ed 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, @@ -145,7 +145,7 @@ async function ctxFactory() { l2Tester: testingSetup.l2Tester, l1Token: testingSetup.l1Token, l2Token: testingSetup.l2Token, - l1ERC20TokenBridge: testingSetup.l1ERC20TokenBridge, + l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, crossChainMessenger: new CrossChainMessenger({ l2ChainId: network.chainId("opt", networkName), @@ -155,7 +155,7 @@ async function ctxFactory() { bridges: { LidoBridge: { Adapter: DAIBridgeAdapter, - l1Bridge: testingSetup.l1ERC20TokenBridge.address, + l1Bridge: testingSetup.l1LidoTokensBridge.address, l2Bridge: testingSetup.l2ERC20TokenBridge.address, }, }, diff --git a/test/optimism/bridging.e2e.test.ts b/test/optimism/bridging.e2e.test.ts index d578e860..ea346bdd 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,8 +142,8 @@ async function ctxFactory() { l2SignerOrProvider: testingSetup.l2Tester, bridges: { LidoBridge: { - Adapter: DAIBridgeAdapter, - l1Bridge: testingSetup.l1ERC20TokenBridge.address, + Adapter: LidoBridgeAdapter, + l1Bridge: testingSetup.l1LidoTokensBridge.address, l2Bridge: testingSetup.l2ERC20TokenBridge.address, }, }, diff --git a/test/optimism/bridging.integration.test.ts b/test/optimism/bridging.integration.test.ts index 23eb66f6..57d9e5b3 100644 --- a/test/optimism/bridging.integration.test.ts +++ b/test/optimism/bridging.integration.test.ts @@ -12,13 +12,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .step("Activate bridging on L1", async (ctx) => { - const { l1ERC20TokenBridge } = ctx; + const { l1LidoTokensBridge } = ctx; const { l1ERC20TokenBridgeAdmin } = ctx.accounts; - const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { - await l1ERC20TokenBridge + await l1LidoTokensBridge .connect(l1ERC20TokenBridgeAdmin) .enableDeposits(); } else { @@ -26,18 +26,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } const isWithdrawalsEnabled = - await l1ERC20TokenBridge.isWithdrawalsEnabled(); + await l1LidoTokensBridge.isWithdrawalsEnabled(); if (!isWithdrawalsEnabled) { - await l1ERC20TokenBridge + await l1LidoTokensBridge .connect(l1ERC20TokenBridgeAdmin) .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) => { @@ -72,7 +72,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("L1 -> L2 deposit via depositERC20() method", async (ctx) => { const { l1Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2Token, l1CrossDomainMessenger, l2ERC20TokenBridge, @@ -82,16 +82,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 + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1Token.address, @@ -101,7 +101,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -126,14 +126,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.add(depositAmount) ); @@ -147,7 +147,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l2Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2ERC20TokenBridge, } = ctx; @@ -164,7 +164,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2ERC20TokenBridge.address, 0, 300_000, @@ -233,7 +233,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1CrossDomainMessenger, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2Token, l2ERC20TokenBridge, @@ -245,7 +245,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); await l1CrossDomainMessenger @@ -255,9 +255,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tx = await l1CrossDomainMessenger .connect(l1Stranger) .relayMessage( - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2CrossDomainMessenger.address, - l1ERC20TokenBridge.interface.encodeFunctionData( + l1LidoTokensBridge.interface.encodeFunctionData( "finalizeERC20Withdrawal", [ l1Token.address, @@ -271,7 +271,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,7 +281,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) ); @@ -295,7 +295,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l2Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2ERC20TokenBridge, l1CrossDomainMessenger, } = ctx; @@ -306,16 +306,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 + l1LidoTokensBridge.address ); - const tx = await l1ERC20TokenBridge + const tx = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20To( l1Token.address, @@ -326,7 +326,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l1ERC20TokenBridge, tx, "ERC20DepositInitiated", [ + await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -351,14 +351,14 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ l2ERC20TokenBridge.address, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, 200_000, ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.add(depositAmount) ); @@ -371,7 +371,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Finalize deposit on L2", async (ctx) => { const { l1Token, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2Token, l2CrossDomainMessenger, l2ERC20TokenBridge, @@ -392,7 +392,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .connect(l1CrossDomainMessengerAliased) .relayMessage( 1, - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2ERC20TokenBridge.address, 0, 300_000, @@ -470,7 +470,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1Token, l1CrossDomainMessenger, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2CrossDomainMessenger, l2Token, l2ERC20TokenBridge, @@ -486,7 +486,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); await l1CrossDomainMessenger @@ -496,9 +496,9 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tx = await l1CrossDomainMessenger .connect(l1Stranger) .relayMessage( - l1ERC20TokenBridge.address, + l1LidoTokensBridge.address, l2CrossDomainMessenger.address, - l1ERC20TokenBridge.interface.encodeFunctionData( + l1LidoTokensBridge.interface.encodeFunctionData( "finalizeERC20Withdrawal", [ l1Token.address, @@ -512,7 +512,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,7 +522,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ]); assert.equalBN( - await l1Token.balanceOf(l1ERC20TokenBridge.address), + await l1Token.balanceOf(l1LidoTokensBridge.address), l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) ); diff --git a/test/optimism/deployment.acceptance.test.ts b/test/optimism/deployment.acceptance.test.ts index be49d3c5..b5a7a766 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(), + await ctx.l1LidoTokensBridge.l2TokenBridge(), ctx.l2ERC20TokenBridge.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 = @@ -142,18 +142,18 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) ); }) .step("L2 bridge :: L1 token", async (ctx) => { - assert.equal(await ctx.l2ERC20TokenBridge.l1Token(), ctx.deployment.token); + assert.equal(await ctx.l2ERC20TokenBridge.L1_TOKEN_NON_REBASABLE(), ctx.deployment.token); }) .step("L2 bridge :: L2 token", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridge.l2Token(), + await ctx.l2ERC20TokenBridge.L2_TOKEN_NON_REBASABLE(), ctx.erc20Bridged.address ); }) .step("L2 bridge :: L1 token bridge", async (ctx) => { assert.equal( await ctx.l2ERC20TokenBridge.l1TokenBridge(), - ctx.l1ERC20TokenBridge.address + ctx.l1LidoTokensBridge.address ); }) .step("L2 Bridge :: is deposits enabled", async (ctx) => { @@ -282,9 +282,9 @@ 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, diff --git a/test/optimism/deposit-gas-estimation.test.ts b/test/optimism/deposit-gas-estimation.test.ts index 5a14cda6..ea3ae063 100644 --- a/test/optimism/deposit-gas-estimation.test.ts +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -12,13 +12,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .step("Activate bridging on L1", async (ctx) => { - const { l1ERC20TokenBridge } = ctx; + const { l1LidoTokensBridge } = ctx; const { l1ERC20TokenBridgeAdmin } = ctx.accounts; - const isDepositsEnabled = await l1ERC20TokenBridge.isDepositsEnabled(); + const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { - await l1ERC20TokenBridge + await l1LidoTokensBridge .connect(l1ERC20TokenBridgeAdmin) .enableDeposits(); } else { @@ -26,18 +26,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) } const isWithdrawalsEnabled = - await l1ERC20TokenBridge.isWithdrawalsEnabled(); + await l1LidoTokensBridge.isWithdrawalsEnabled(); if (!isWithdrawalsEnabled) { - await l1ERC20TokenBridge + await l1LidoTokensBridge .connect(l1ERC20TokenBridgeAdmin) .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) => { @@ -74,26 +74,26 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l2Token, l1TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2TokenRebasable } = ctx; const { accountA: tokenHolderA } = ctx.accounts; - const stETHPerToken = await l1Token.stETHPerToken(); - + const stEthPerToken = await l1Token.stEthPerToken(); + await l1TokenRebasable .connect(tokenHolderA.l1Signer) - .approve(l1ERC20TokenBridge.address, 0); + .approve(l1LidoTokensBridge.address, 0); const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( - l1ERC20TokenBridge.address + l1LidoTokensBridge.address ); - const tx0 = await l1ERC20TokenBridge + const tx0 = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1Token.address, @@ -106,7 +106,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const receipt0 = await tx0.wait(); console.log("l1Token gasUsed=",receipt0.gasUsed); - const tx1 = await l1ERC20TokenBridge + const tx1 = await l1LidoTokensBridge .connect(tokenHolderA.l1Signer) .depositERC20( l1TokenRebasable.address, @@ -118,19 +118,19 @@ scenario("Optimism :: Bridging integration test", ctxFactory) 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, diff --git a/utils/optimism/LidoBridgeAdapter.ts b/utils/optimism/LidoBridgeAdapter.ts new file mode 100644 index 00000000..14e70967 --- /dev/null +++ b/utils/optimism/LidoBridgeAdapter.ts @@ -0,0 +1,45 @@ +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', + }, + ], this.messenger.l1Provider); + const allowedL1Token = await l1Bridge.L1_TOKEN_NON_REBASABLE(); + if (!(0, hexStringEquals)(allowedL1Token, (0, toAddress)(l1Token))) { + return false; + } + const allowedL2Token = await l1Bridge.L2_TOKEN_NON_REBASABLE(); + if (!(0, hexStringEquals)(allowedL2Token, (0, toAddress)(l2Token))) { + return false; + } + return true; + } +} diff --git a/utils/optimism/deployment.ts b/utils/optimism/deployment.ts index 354c6eb4..6a64d757 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deployment.ts @@ -4,10 +4,9 @@ import { ERC20Bridged__factory, ERC20Rebasable__factory, IERC20Metadata__factory, - L1ERC20TokenBridge__factory, + L1LidoTokensBridge__factory, L2ERC20TokenBridge__factory, OssifiableProxy__factory, - TokenRateOracle, TokenRateOracle__factory, } from "../../typechain"; @@ -64,7 +63,7 @@ export default function deployment( options?.logger ) .addStep({ - factory: L1ERC20TokenBridge__factory, + factory: L1LidoTokensBridge__factory, args: [ optAddresses.L1CrossDomainMessenger, expectedL2TokenBridgeProxyAddress, @@ -82,7 +81,7 @@ export default function deployment( args: [ expectedL1TokenBridgeImplAddress, l1Params.admins.proxy, - L1ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L1LidoTokensBridge__factory.createInterface().encodeFunctionData( "initialize", [l1Params.admins.bridge] ), diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 2d6b4434..9517877c 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -5,13 +5,13 @@ import { IERC20, ERC20Bridged, IERC20__factory, - L1ERC20TokenBridge, + L1LidoTokensBridge, L2ERC20TokenBridge, ERC20Bridged__factory, ERC20BridgedStub__factory, ERC20WrapperStub__factory, TokenRateOracle__factory, - L1ERC20TokenBridge__factory, + L1LidoTokensBridge__factory, L2ERC20TokenBridge__factory, CrossDomainMessengerStub__factory, ERC20Rebasable__factory, @@ -59,7 +59,7 @@ export default function testing(networkName: NetworkName) { : await deployTestBridge(networkName, ethProvider, optProvider); const [l1ERC20TokenBridgeAdminAddress] = - await BridgingManagement.getAdmins(bridgeContracts.l1ERC20TokenBridge); + await BridgingManagement.getAdmins(bridgeContracts.l1LidoTokensBridge); const [l2ERC20TokenBridgeAdminAddress] = await BridgingManagement.getAdmins(bridgeContracts.l2ERC20TokenBridge); @@ -166,10 +166,10 @@ async function loadDeployedBridges( ...connectBridgeContracts( { - tokenRateOracle: testingUtils.env.OPT_L2_TOKEN(), // fix + tokenRateOracle: testingUtils.env.OPT_L2_TOKEN_RATE_ORACLE(), l2Token: testingUtils.env.OPT_L2_TOKEN(), - l2TokenRebasable: testingUtils.env.OPT_L2_TOKEN(), // fix - l1ERC20TokenBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), + l2TokenRebasable: testingUtils.env.OPT_L2_REBASABLE_TOKEN(), + l1LidoTokensBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), l2ERC20TokenBridge: testingUtils.env.OPT_L2_ERC20_TOKEN_BRIDGE(), }, l1SignerOrProvider, @@ -215,9 +215,9 @@ async function deployTestBridge( await ethDeployScript.run(); await optDeployScript.run(); - const l1ERC20TokenBridgeProxyDeployStepIndex = 1; + const l1LidoTokensBridgeProxyDeployStepIndex = 1; const l1BridgingManagement = new BridgingManagement( - ethDeployScript.getContractAddress(l1ERC20TokenBridgeProxyDeployStepIndex), + ethDeployScript.getContractAddress(l1LidoTokensBridgeProxyDeployStepIndex), ethDeployer ); @@ -247,7 +247,7 @@ async function deployTestBridge( tokenRateOracle: optDeployScript.getContractAddress(0), l2Token: optDeployScript.getContractAddress(2), l2TokenRebasable: optDeployScript.getContractAddress(4), - l1ERC20TokenBridge: ethDeployScript.getContractAddress(1), + l1LidoTokensBridge: ethDeployScript.getContractAddress(1), l2ERC20TokenBridge: optDeployScript.getContractAddress(6) }, ethProvider, @@ -261,15 +261,15 @@ function connectBridgeContracts( tokenRateOracle: string; l2Token: string; l2TokenRebasable: string; - l1ERC20TokenBridge: string; + l1LidoTokensBridge: string; l2ERC20TokenBridge: 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( @@ -292,7 +292,7 @@ function connectBridgeContracts( tokenRateOracle, l2Token, l2TokenRebasable, - l1ERC20TokenBridge, + l1LidoTokensBridge, l2ERC20TokenBridge }; } @@ -302,7 +302,7 @@ async function printLoadedTestConfig( bridgeContracts: { l1Token: IERC20; l2Token: ERC20Bridged; - l1ERC20TokenBridge: L1ERC20TokenBridge; + l1LidoTokensBridge: L1LidoTokensBridge; l2ERC20TokenBridge: L2ERC20TokenBridge; }, l1TokensHolder?: Signer @@ -323,7 +323,7 @@ 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}` From 7badd9eec2e2080621de6f5308c3f7f084eb3bac Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 6 Mar 2024 17:23:24 +0200 Subject: [PATCH 30/61] add rebasable token e2e tests --- .env.example | 3 + test/optimism/bridging-rebasable.e2e.test.ts | 152 ++++++++++++++++++ ...=> bridging-rebasable.integration.test.ts} | 0 utils/optimism/LidoBridgeAdapter.ts | 43 ++++- utils/optimism/testing.ts | 2 +- utils/testing/env.ts | 9 ++ 6 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 test/optimism/bridging-rebasable.e2e.test.ts rename test/optimism/{bridging-rebase.integration.test.ts => bridging-rebasable.integration.test.ts} (100%) diff --git a/.env.example b/.env.example index 767859ae..fbf40032 100644 --- a/.env.example +++ b/.env.example @@ -77,6 +77,9 @@ TESTING_ARB_L2_GATEWAY_ROUTER=0x57f54f87C44d816f60b92864e23b8c0897D4d81D TESTING_OPT_NETWORK= TESTING_OPT_L1_TOKEN=0xaF8a2F0aE374b03376155BF745A3421Dac711C12 TESTING_OPT_L2_TOKEN=0xAED5F9aaF167923D34174b8E636aaF040A11f6F7 +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/test/optimism/bridging-rebasable.e2e.test.ts b/test/optimism/bridging-rebasable.e2e.test.ts new file mode 100644 index 00000000..868f0a68 --- /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.l2ERC20TokenBridge.address, + }, + }, + }), + }; + } diff --git a/test/optimism/bridging-rebase.integration.test.ts b/test/optimism/bridging-rebasable.integration.test.ts similarity index 100% rename from test/optimism/bridging-rebase.integration.test.ts rename to test/optimism/bridging-rebasable.integration.test.ts diff --git a/utils/optimism/LidoBridgeAdapter.ts b/utils/optimism/LidoBridgeAdapter.ts index 14e70967..ac561d4b 100644 --- a/utils/optimism/LidoBridgeAdapter.ts +++ b/utils/optimism/LidoBridgeAdapter.ts @@ -31,13 +31,48 @@ export class LidoBridgeAdapter extends StandardBridgeAdapter { 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 allowedL1Token = await l1Bridge.L1_TOKEN_NON_REBASABLE(); - if (!(0, hexStringEquals)(allowedL1Token, (0, toAddress)(l1Token))) { + + 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 allowedL2Token = await l1Bridge.L2_TOKEN_NON_REBASABLE(); - if (!(0, hexStringEquals)(allowedL2Token, (0, toAddress)(l2Token))) { + + 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/testing.ts b/utils/optimism/testing.ts index 9517877c..5f36907c 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -160,7 +160,7 @@ async function loadDeployedBridges( l1SignerOrProvider ), l1TokenRebasable: IERC20__factory.connect( - testingUtils.env.OPT_L1_TOKEN(), + testingUtils.env.OPT_L1_REBASABLE_TOKEN(), l1SignerOrProvider ), diff --git a/utils/testing/env.ts b/utils/testing/env.ts index faf8acd6..1a00941e 100644 --- a/utils/testing/env.ts +++ b/utils/testing/env.ts @@ -33,6 +33,15 @@ export default { OPT_L2_TOKEN() { return env.address("TESTING_OPT_L2_TOKEN"); }, + 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"); }, From 72bc8f329e782149e29f7ae4fb03fb25309da62e Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 6 Mar 2024 19:09:44 +0200 Subject: [PATCH 31/61] add bridging-to e2e tests for rebasable token --- .../bridging-rebasable-to.e2e.test.ts | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 test/optimism/bridging-rebasable-to.e2e.test.ts 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..ca582770 --- /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.l2ERC20TokenBridge + .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, + l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, + 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.l2ERC20TokenBridge.address, + }, + }, + }), + }; + } From 83e292b4dc107969c3cb1cd4553b71820582e515 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 7 Mar 2024 11:54:37 +0200 Subject: [PATCH 32/61] add token rate observer --- contracts/lido/IPostTokenRebaseReceiver.sol | 20 +++++++++ contracts/lido/ITokenRateObserver.sol | 10 +++++ contracts/lido/TokenRateNotifier.sol | 44 +++++++++++++++++++ .../optimism/OptimismTokenRateObserver.sol | 22 ++++++++++ 4 files changed, 96 insertions(+) create mode 100644 contracts/lido/IPostTokenRebaseReceiver.sol create mode 100644 contracts/lido/ITokenRateObserver.sol create mode 100644 contracts/lido/TokenRateNotifier.sol create mode 100644 contracts/optimism/OptimismTokenRateObserver.sol diff --git a/contracts/lido/IPostTokenRebaseReceiver.sol b/contracts/lido/IPostTokenRebaseReceiver.sol new file mode 100644 index 00000000..e5eb7e90 --- /dev/null +++ b/contracts/lido/IPostTokenRebaseReceiver.sol @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice An interface for Lido core protocol rebase event. +interface IPostTokenRebaseReceiver { + + /// @notice Is called when Lido core protocol rebasable event occures. + function handlePostTokenRebase( + uint256 _reportTimestamp, + uint256 _timeElapsed, + uint256 _preTotalShares, + uint256 _preTotalEther, + uint256 _postTotalShares, + uint256 _postTotalEther, + uint256 _sharesMintedAsFees + ) external; +} diff --git a/contracts/lido/ITokenRateObserver.sol b/contracts/lido/ITokenRateObserver.sol new file mode 100644 index 00000000..25c7c9f2 --- /dev/null +++ b/contracts/lido/ITokenRateObserver.sol @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice An interface for Lido core protocol rebase event. +interface ITokenRateObserver { + function update() external; +} diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol new file mode 100644 index 00000000..df735f41 --- /dev/null +++ b/contracts/lido/TokenRateNotifier.sol @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IPostTokenRebaseReceiver} from "./IPostTokenRebaseReceiver.sol"; +import {ITokenRateObserver} from "./ITokenRateObserver.sol"; + +/// @author kovalgek +/// @notice An interface for Lido core protocol rebase event. +contract TokenRateNotifier is IPostTokenRebaseReceiver { + + event FailObserverNotification(address indexed observer); + + address[] private observers; + + constructor() { + } + + function registerObserver(address observer) external { + observers.push(observer); + } + + function _notifyObservers() internal { + for(uint observerIndex = 0; observerIndex < observers.length; observerIndex++) { + _notifyObserver(observers[observerIndex]); + } + } + + function _notifyObserver(address observer) internal { + ITokenRateObserver tokenRateObserver = ITokenRateObserver(observer); + + (bool success, bytes memory returnData) = address(observer).call( + abi.encodePacked(tokenRateObserver.update.selector) + ); + if (!success) { + emit FailObserverNotification(observer); + } + } + + function handlePostTokenRebase(uint256, uint256, uint256, uint256, uint256, uint256, uint256) external { + _notifyObservers(); + } +} diff --git a/contracts/optimism/OptimismTokenRateObserver.sol b/contracts/optimism/OptimismTokenRateObserver.sol new file mode 100644 index 00000000..ecdb9f3a --- /dev/null +++ b/contracts/optimism/OptimismTokenRateObserver.sol @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRateObserver} from "../lido/ITokenRateObserver.sol"; +import {L1LidoTokensBridge} from "./L1LidoTokensBridge.sol"; + +/// @author kovalgek +/// @notice An interface for Lido core protocol rebase event. +contract OptimismTokenRateObserver is ITokenRateObserver { + + L1LidoTokensBridge l1LidoTokensBridge; + + constructor(address lidoTokensBridge) { + l1LidoTokensBridge = L1LidoTokensBridge(lidoTokensBridge); + } + + function update() external { + l1LidoTokensBridge.pushTokenRate(10_000); + } +} From dbf2f92129df6a8d0b1ce8d923869cdefa45ece3 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 14 Mar 2024 14:43:11 +0100 Subject: [PATCH 33/61] add observers array --- contracts/lido/ITokenRateObserver.sol | 10 -- contracts/lido/ObserversArray.sol | 94 +++++++++++++++++++ contracts/lido/TokenRateNotifier.sol | 65 +++++++------ contracts/lido/interfaces/IObserversArray.sol | 51 ++++++++++ .../IPostTokenRebaseReceiver.sol | 6 +- .../lido/interfaces/ITokenRateObserver.sol | 12 +++ .../optimism/OpStackTokenRateObserver.sol | 29 ++++++ .../optimism/OptimismTokenRateObserver.sol | 22 ----- 8 files changed, 224 insertions(+), 65 deletions(-) delete mode 100644 contracts/lido/ITokenRateObserver.sol create mode 100644 contracts/lido/ObserversArray.sol create mode 100644 contracts/lido/interfaces/IObserversArray.sol rename contracts/lido/{ => interfaces}/IPostTokenRebaseReceiver.sol (59%) create mode 100644 contracts/lido/interfaces/ITokenRateObserver.sol create mode 100644 contracts/optimism/OpStackTokenRateObserver.sol delete mode 100644 contracts/optimism/OptimismTokenRateObserver.sol diff --git a/contracts/lido/ITokenRateObserver.sol b/contracts/lido/ITokenRateObserver.sol deleted file mode 100644 index 25c7c9f2..00000000 --- a/contracts/lido/ITokenRateObserver.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @notice An interface for Lido core protocol rebase event. -interface ITokenRateObserver { - function update() external; -} diff --git a/contracts/lido/ObserversArray.sol b/contracts/lido/ObserversArray.sol new file mode 100644 index 00000000..5250d6b0 --- /dev/null +++ b/contracts/lido/ObserversArray.sol @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {IPostTokenRebaseReceiver} from "./interfaces/IPostTokenRebaseReceiver.sol"; +import {ITokenRateObserver} from "./interfaces/ITokenRateObserver.sol"; +import {IObserversArray} from "./interfaces/IObserversArray.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @author kovalgek +/// @notice Manage observers. +contract ObserversArray is Ownable, IObserversArray { + using ERC165Checker for address; + + /// @notice Maximum amount of observers to be supported. + uint256 public constant MAX_OBSERVERS_COUNT = 16; + + /// @notice Invalid interface id. + bytes4 constant INVALID_INTERFACE_ID = 0xffffffff; + + /// @notice An interface that each observer should support. + bytes4 public immutable REQUIRED_INTERFACE; + + /// @notice all observers. + address[] public observers; + + /// @param requiredInterface_ An interface that each observer should support. + constructor(bytes4 requiredInterface_) { + if (requiredInterface_ == INVALID_INTERFACE_ID) { + revert ErrorInvalidInterface(); + } + + REQUIRED_INTERFACE = requiredInterface_; + } + + /// @inheritdoc IObserversArray + function observersLength() public view returns (uint256) { + return observers.length; + } + + /// @inheritdoc IObserversArray + function addObserver(address observer_) external virtual onlyOwner { + if (observer_ == address(0)) { + revert ErrorZeroAddressObserver(); + } + if (!observer_.supportsInterface(REQUIRED_INTERFACE)) { + revert ErrorBadObserverInterface(); + } + if (observers.length >= MAX_OBSERVERS_COUNT) { + revert ErrorMaxObserversCountExceeded(); + } + + observers.push(observer_); + emit ObserverAdded(observer_); + } + + /// @inheritdoc IObserversArray + function removeObserver(address observer_) external virtual onlyOwner { + + uint256 observerIndexToRemove = _observerIndex(observer_); + + if (observerIndexToRemove == type(uint256).max) { + revert ErrorNoObserverToRemove(); + } + + for (uint256 obIndex = observerIndexToRemove; obIndex < observers.length - 1; obIndex++) { + observers[obIndex] = observers[obIndex + 1]; + } + + observers.pop(); + + emit ObserverRemoved(observer_, observerIndexToRemove); + } + + /// @notice `observer_` index in `observers` array. + function _observerIndex(address observer_) internal view returns (uint256) { + for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { + if (observers[obIndex] == observer_) { + return obIndex; + } + } + return type(uint256).max; + } + + event FailObserverNotification(address indexed observer); + + error ErrorInvalidInterface(); + error ErrorZeroAddressObserver(); + error ErrorBadObserverInterface(); + error ErrorMaxObserversCountExceeded(); + error ErrorNoObserverToRemove(); +} diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol index df735f41..15f173c5 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -1,44 +1,49 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; -import {IPostTokenRebaseReceiver} from "./IPostTokenRebaseReceiver.sol"; -import {ITokenRateObserver} from "./ITokenRateObserver.sol"; +import {IPostTokenRebaseReceiver} from "./interfaces/IPostTokenRebaseReceiver.sol"; +import {ITokenRateObserver} from "./interfaces/ITokenRateObserver.sol"; +import {ObserversArray} from "./ObserversArray.sol"; /// @author kovalgek -/// @notice An interface for Lido core protocol rebase event. -contract TokenRateNotifier is IPostTokenRebaseReceiver { +/// @notice Notifies all observers when rebase event occures. +contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { - event FailObserverNotification(address indexed observer); - - address[] private observers; - - constructor() { - } - - function registerObserver(address observer) external { - observers.push(observer); + constructor() ObserversArray(type(ITokenRateObserver).interfaceId) { } - function _notifyObservers() internal { - for(uint observerIndex = 0; observerIndex < observers.length; observerIndex++) { - _notifyObserver(observers[observerIndex]); + /// @inheritdoc IPostTokenRebaseReceiver + function handlePostTokenRebase( + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256 + ) external { + uint256 observersLength = observersLength(); + + for (uint256 obIndex = 0; obIndex < observersLength; obIndex++) { + try ITokenRateObserver(observers[obIndex]).handleTokenRebased() {} + 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 handleTokenRebased() reverts because of the + /// "out of gas" error. Here we assume that the handleTokenRebased() method doesn't + /// have reverts with empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert ErrorUnrecoverableObserver(); + emit HandleTokenRebasedFailed( + observers[obIndex], + lowLevelRevertData + ); + } } } - function _notifyObserver(address observer) internal { - ITokenRateObserver tokenRateObserver = ITokenRateObserver(observer); + event HandleTokenRebasedFailed(address indexed observer, bytes lowLevelRevertData); - (bool success, bytes memory returnData) = address(observer).call( - abi.encodePacked(tokenRateObserver.update.selector) - ); - if (!success) { - emit FailObserverNotification(observer); - } - } - - function handlePostTokenRebase(uint256, uint256, uint256, uint256, uint256, uint256, uint256) external { - _notifyObservers(); - } + error ErrorUnrecoverableObserver(); } diff --git a/contracts/lido/interfaces/IObserversArray.sol b/contracts/lido/interfaces/IObserversArray.sol new file mode 100644 index 00000000..b378af07 --- /dev/null +++ b/contracts/lido/interfaces/IObserversArray.sol @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +interface IObserversArray { + /** + * @notice Observer added event + * + * @dev emitted by `addObserver` and `insertObserver` functions + */ + event ObserverAdded(address indexed observer); + + /** + * @notice Observer removed event + * + * @dev emitted by `removeObserver` function + */ + event ObserverRemoved(address indexed observer, uint256 atIndex); + + /** + * @notice Observer length + * @return Added observers count + */ + function observersLength() external view returns (uint256); + + /** + * @notice Add a `_observer` to the back of array + * @param observer_ observer address + * + * @dev cheapest way to insert new item (doesn't incur additional moves) + */ + function addObserver(address observer_) external; + + /** + * @notice Remove a observer at the given `observer_` position + * @param observer_ observer remove position + * + * @dev remove gas cost is higher for the lower `observer_` values + */ + function removeObserver(address observer_) external; + + /** + * @notice Get observer at position + * @return Observer at the given `atIndex_` + * + * @dev function reverts if `atIndex_` is out of range + */ + function observers(uint256 atIndex_) external view returns (address); +} diff --git a/contracts/lido/IPostTokenRebaseReceiver.sol b/contracts/lido/interfaces/IPostTokenRebaseReceiver.sol similarity index 59% rename from contracts/lido/IPostTokenRebaseReceiver.sol rename to contracts/lido/interfaces/IPostTokenRebaseReceiver.sol index e5eb7e90..65b1fe90 100644 --- a/contracts/lido/IPostTokenRebaseReceiver.sol +++ b/contracts/lido/interfaces/IPostTokenRebaseReceiver.sol @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; /// @author kovalgek -/// @notice An interface for Lido core protocol rebase event. +/// @notice An interface to subscribe on the `stETH` token rebases (defined in the `Lido` core contract) interface IPostTokenRebaseReceiver { - /// @notice Is called when Lido core protocol rebasable event occures. + /// @notice Is called in the context of `Lido.handleOracleReport` to notify the subscribers about each token rebase function handlePostTokenRebase( uint256 _reportTimestamp, uint256 _timeElapsed, diff --git a/contracts/lido/interfaces/ITokenRateObserver.sol b/contracts/lido/interfaces/ITokenRateObserver.sol new file mode 100644 index 00000000..cc59efdf --- /dev/null +++ b/contracts/lido/interfaces/ITokenRateObserver.sol @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice An interface to subscribe for token rebases. Is used to handle different rollups. +interface ITokenRateObserver { + + /// @notice Is called when rebase event occures. + function handleTokenRebased() external; +} diff --git a/contracts/optimism/OpStackTokenRateObserver.sol b/contracts/optimism/OpStackTokenRateObserver.sol new file mode 100644 index 00000000..4ad4b58e --- /dev/null +++ b/contracts/optimism/OpStackTokenRateObserver.sol @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRateObserver} from "../lido/interfaces/ITokenRateObserver.sol"; +import {L1LidoTokensBridge} from "./L1LidoTokensBridge.sol"; + +/// @author kovalgek +/// @notice Pushes token rate when rebase event happens. +contract OpStackTokenRateObserver is ITokenRateObserver { + + /// @notice Contract of OpStack bridge. + L1LidoTokensBridge public immutable L1_LIDO_TOKENS_BRIDGE; + + /// @notice Gas limit required to complete pushing token rate on L2. + uint32 public immutable L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE; + + /// @param lidoTokensBridge_ OpStack bridge. + constructor(address lidoTokensBridge_, uint32 l2GasLimitForPushingTokenRate_) { + L1_LIDO_TOKENS_BRIDGE = L1LidoTokensBridge(lidoTokensBridge_); + L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE = l2GasLimitForPushingTokenRate_; + } + + /// @inheritdoc ITokenRateObserver + function handleTokenRebased() external { + L1_LIDO_TOKENS_BRIDGE.pushTokenRate(L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE); + } +} diff --git a/contracts/optimism/OptimismTokenRateObserver.sol b/contracts/optimism/OptimismTokenRateObserver.sol deleted file mode 100644 index ecdb9f3a..00000000 --- a/contracts/optimism/OptimismTokenRateObserver.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {ITokenRateObserver} from "../lido/ITokenRateObserver.sol"; -import {L1LidoTokensBridge} from "./L1LidoTokensBridge.sol"; - -/// @author kovalgek -/// @notice An interface for Lido core protocol rebase event. -contract OptimismTokenRateObserver is ITokenRateObserver { - - L1LidoTokensBridge l1LidoTokensBridge; - - constructor(address lidoTokensBridge) { - l1LidoTokensBridge = L1LidoTokensBridge(lidoTokensBridge); - } - - function update() external { - l1LidoTokensBridge.pushTokenRate(10_000); - } -} From 47c50c6d874a1841b9a10d347370819976dae2d4 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 18 Mar 2024 16:39:19 +0100 Subject: [PATCH 34/61] add factory and tests --- contracts/lido/ObserversArray.sol | 24 ++-- contracts/lido/TokenRateNotifier.sol | 2 +- contracts/lido/interfaces/IObserversArray.sol | 51 +++---- .../lido/interfaces/ITokenRateObserver.sol | 4 +- .../optimism/OpStackTokenRateObserver.sol | 12 +- .../OpStackTokenRateObserverFactory.sol | 41 ++++++ test/optimism/TokenRateNotifier.unit.test.ts | 126 ++++++++++++++++++ 7 files changed, 208 insertions(+), 52 deletions(-) create mode 100644 contracts/optimism/OpStackTokenRateObserverFactory.sol create mode 100644 test/optimism/TokenRateNotifier.unit.test.ts diff --git a/contracts/lido/ObserversArray.sol b/contracts/lido/ObserversArray.sol index 5250d6b0..3c1e6fcf 100644 --- a/contracts/lido/ObserversArray.sol +++ b/contracts/lido/ObserversArray.sol @@ -3,11 +3,9 @@ pragma solidity 0.8.10; -import {IPostTokenRebaseReceiver} from "./interfaces/IPostTokenRebaseReceiver.sol"; -import {ITokenRateObserver} from "./interfaces/ITokenRateObserver.sol"; -import {IObserversArray} from "./interfaces/IObserversArray.sol"; -import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {IObserversArray} from "./interfaces/IObserversArray.sol"; /// @author kovalgek /// @notice Manage observers. @@ -18,12 +16,12 @@ contract ObserversArray is Ownable, IObserversArray { uint256 public constant MAX_OBSERVERS_COUNT = 16; /// @notice Invalid interface id. - bytes4 constant INVALID_INTERFACE_ID = 0xffffffff; + bytes4 public constant INVALID_INTERFACE_ID = 0xffffffff; /// @notice An interface that each observer should support. bytes4 public immutable REQUIRED_INTERFACE; - /// @notice all observers. + /// @notice All observers. address[] public observers; /// @param requiredInterface_ An interface that each observer should support. @@ -35,11 +33,6 @@ contract ObserversArray is Ownable, IObserversArray { REQUIRED_INTERFACE = requiredInterface_; } - /// @inheritdoc IObserversArray - function observersLength() public view returns (uint256) { - return observers.length; - } - /// @inheritdoc IObserversArray function addObserver(address observer_) external virtual onlyOwner { if (observer_ == address(0)) { @@ -71,7 +64,12 @@ contract ObserversArray is Ownable, IObserversArray { observers.pop(); - emit ObserverRemoved(observer_, observerIndexToRemove); + emit ObserverRemoved(observer_); + } + + /// @inheritdoc IObserversArray + function observersLength() public view returns (uint256) { + return observers.length; } /// @notice `observer_` index in `observers` array. @@ -84,8 +82,6 @@ contract ObserversArray is Ownable, IObserversArray { return type(uint256).max; } - event FailObserverNotification(address indexed observer); - error ErrorInvalidInterface(); error ErrorZeroAddressObserver(); error ErrorBadObserverInterface(); diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol index 15f173c5..59904d2e 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -11,7 +11,7 @@ import {ObserversArray} from "./ObserversArray.sol"; /// @notice Notifies all observers when rebase event occures. contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { - constructor() ObserversArray(type(ITokenRateObserver).interfaceId) { + constructor() ObserversArray(type(ITokenRateObserver).interfaceId) { } /// @inheritdoc IPostTokenRebaseReceiver diff --git a/contracts/lido/interfaces/IObserversArray.sol b/contracts/lido/interfaces/IObserversArray.sol index b378af07..f3a867b7 100644 --- a/contracts/lido/interfaces/IObserversArray.sol +++ b/contracts/lido/interfaces/IObserversArray.sol @@ -4,48 +4,31 @@ pragma solidity 0.8.10; /// @author kovalgek +/// @notice An interface for observer pattern interface IObserversArray { - /** - * @notice Observer added event - * - * @dev emitted by `addObserver` and `insertObserver` functions - */ + + /// @notice Observer added event + /// @dev emitted by `addObserver` function event ObserverAdded(address indexed observer); - /** - * @notice Observer removed event - * - * @dev emitted by `removeObserver` function - */ - event ObserverRemoved(address indexed observer, uint256 atIndex); - - /** - * @notice Observer length - * @return Added observers count - */ + /// @notice Observer removed event + /// @dev emitted by `removeObserver` function + event ObserverRemoved(address indexed observer); + + /// @notice Observer length + /// @return Added observers count function observersLength() external view returns (uint256); - /** - * @notice Add a `_observer` to the back of array - * @param observer_ observer address - * - * @dev cheapest way to insert new item (doesn't incur additional moves) - */ + /// @notice Add a `observer_` to the back of array + /// @param observer_ observer address function addObserver(address observer_) external; - /** - * @notice Remove a observer at the given `observer_` position - * @param observer_ observer remove position - * - * @dev remove gas cost is higher for the lower `observer_` values - */ + /// @notice Remove a observer at the given `observer_` position + /// @param observer_ observer remove position function removeObserver(address observer_) external; - /** - * @notice Get observer at position - * @return Observer at the given `atIndex_` - * - * @dev function reverts if `atIndex_` is out of range - */ + /// @notice Get observer at position + /// @return Observer at the given `atIndex_` + /// @dev function reverts if `atIndex_` is out of range function observers(uint256 atIndex_) external view returns (address); } diff --git a/contracts/lido/interfaces/ITokenRateObserver.sol b/contracts/lido/interfaces/ITokenRateObserver.sol index cc59efdf..5f86ce24 100644 --- a/contracts/lido/interfaces/ITokenRateObserver.sol +++ b/contracts/lido/interfaces/ITokenRateObserver.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.10; /// @author kovalgek -/// @notice An interface to subscribe for token rebases. Is used to handle different rollups. +/// @notice An interface to subscribe for token rebases. It is used to handle different rollups. interface ITokenRateObserver { - /// @notice Is called when rebase event occures. + /// @notice It's called when rebase event occures. function handleTokenRebased() external; } diff --git a/contracts/optimism/OpStackTokenRateObserver.sol b/contracts/optimism/OpStackTokenRateObserver.sol index 4ad4b58e..38d08d69 100644 --- a/contracts/optimism/OpStackTokenRateObserver.sol +++ b/contracts/optimism/OpStackTokenRateObserver.sol @@ -5,10 +5,11 @@ pragma solidity 0.8.10; import {ITokenRateObserver} from "../lido/interfaces/ITokenRateObserver.sol"; import {L1LidoTokensBridge} from "./L1LidoTokensBridge.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; /// @author kovalgek /// @notice Pushes token rate when rebase event happens. -contract OpStackTokenRateObserver is ITokenRateObserver { +contract OpStackTokenRateObserver is ERC165, ITokenRateObserver { /// @notice Contract of OpStack bridge. L1LidoTokensBridge public immutable L1_LIDO_TOKENS_BRIDGE; @@ -17,6 +18,7 @@ contract OpStackTokenRateObserver is ITokenRateObserver { uint32 public immutable L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE; /// @param lidoTokensBridge_ OpStack bridge. + /// @param l2GasLimitForPushingTokenRate_ Gas limit required to complete pushing token rate on L2. constructor(address lidoTokensBridge_, uint32 l2GasLimitForPushingTokenRate_) { L1_LIDO_TOKENS_BRIDGE = L1LidoTokensBridge(lidoTokensBridge_); L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE = l2GasLimitForPushingTokenRate_; @@ -26,4 +28,12 @@ contract OpStackTokenRateObserver is ITokenRateObserver { function handleTokenRebased() external { L1_LIDO_TOKENS_BRIDGE.pushTokenRate(L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE); } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(ITokenRateObserver).interfaceId + || super.supportsInterface(_interfaceId) + ); + } } diff --git a/contracts/optimism/OpStackTokenRateObserverFactory.sol b/contracts/optimism/OpStackTokenRateObserverFactory.sol new file mode 100644 index 00000000..10065b07 --- /dev/null +++ b/contracts/optimism/OpStackTokenRateObserverFactory.sol @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {OpStackTokenRateObserver} from "./OpStackTokenRateObserver.sol"; + +/// @author kovalgek +/// @notice Factory for deploying observers for OP stack. +contract OpStackTokenRateObserverFactory { + + /// @notice deploys observer for OP stack. + /// @param lidoTokensBridge_ OpStack bridge. + /// @param l2GasLimitForPushingTokenRate_ Gas limit required to complete pushing token rate on L2. + function deployOpStackTokenRateObserver( + address lidoTokensBridge_, + uint32 l2GasLimitForPushingTokenRate_ + ) external returns (OpStackTokenRateObserver opStackTokenRateObserver) { + + opStackTokenRateObserver = new OpStackTokenRateObserver( + lidoTokensBridge_, + l2GasLimitForPushingTokenRate_ + ); + + emit OpStackTokenRateObserverDeployed( + msg.sender, + address(opStackTokenRateObserver), + lidoTokensBridge_, + l2GasLimitForPushingTokenRate_ + ); + + return opStackTokenRateObserver; + } + + event OpStackTokenRateObserverDeployed( + address indexed creator, + address indexed opStackTokenRateObserver, + address lidoTokensBridge, + uint32 l2GasLimitForPushingTokenRate + ); +} diff --git a/test/optimism/TokenRateNotifier.unit.test.ts b/test/optimism/TokenRateNotifier.unit.test.ts new file mode 100644 index 00000000..7415c5a7 --- /dev/null +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -0,0 +1,126 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { unit } from "../../utils/testing"; +import { BigNumber, utils } from 'ethers' +import { ethers } from 'hardhat' + +import { + TokenRateNotifier__factory, + ObserversArray__factory, + OpStackTokenRateObserver__factory, + ITokenRateObserver__factory, +} from "../../typechain"; + +unit("TokenRateNotifier", ctxFactory) + + .test("init with wrong interface", async (ctx) => { + const { deployer } = ctx.accounts; + await assert.revertsWith(new ObserversArray__factory(deployer).deploy(BigNumber.from("0xffffffff")._hex), "ErrorInvalidInterface()"); + }) + + .test("initial state", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + + assert.equalBN(await tokenRateNotifier.MAX_OBSERVERS_COUNT(), 16); + assert.equal(await tokenRateNotifier.INVALID_INTERFACE_ID(), "0xffffffff"); + const iTokenRateObserver = getInterfaceID(ITokenRateObserver__factory.createInterface()); + assert.equal(await tokenRateNotifier.REQUIRED_INTERFACE(), iTokenRateObserver._hex); + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + }) + + .test("add zero address observer", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + await assert.revertsWith(tokenRateNotifier.addObserver(hre.ethers.constants.AddressZero), "ErrorZeroAddressObserver()"); + }) + + .test("add bad interface observer", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const observer = await new TokenRateNotifier__factory(deployer).deploy(); + await assert.revertsWith(tokenRateNotifier.addObserver(observer.address), "ErrorBadObserverInterface()"); + }) + + .test("add too many observers", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + + const maxObservers = await tokenRateNotifier.MAX_OBSERVERS_COUNT(); + for (let i = 0; i < maxObservers.toNumber(); i++) { + const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); + await tokenRateNotifier.addObserver(observer.address); + } + + assert.equalBN(await tokenRateNotifier.observersLength(), maxObservers); + + const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); + await assert.revertsWith(tokenRateNotifier.addObserver(observer.address), "ErrorMaxObserversCountExceeded()"); + }) + + .test("add observer", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + + const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); + const tx = await tokenRateNotifier.addObserver(observer.address); + + assert.equalBN(await tokenRateNotifier.observersLength(), 1); + + await assert.emits(tokenRateNotifier, tx, "ObserverAdded", [observer.address]); + }) + + .test("remove non-added observer", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + + const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); + await assert.revertsWith(tokenRateNotifier.removeObserver(observer.address), "ErrorNoObserverToRemove()"); + }) + + .test("remove observer", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + + const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); + await tokenRateNotifier.addObserver(observer.address); + + assert.equalBN(await tokenRateNotifier.observersLength(), 1); + + const tx = await tokenRateNotifier.removeObserver(observer.address); + await assert.emits(tokenRateNotifier, tx, "ObserverRemoved", [observer.address]); + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + }) + + .run(); + +async function ctxFactory() { + + const [deployer, bridge, stranger] = await hre.ethers.getSigners(); + + const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(); + + return { + accounts: { deployer, bridge, stranger }, + contracts: { tokenRateNotifier } + }; +} + +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; +} + + From 7e454105c93cbc2332af955876fa3e0616f98bbc Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 19 Mar 2024 14:07:24 +0100 Subject: [PATCH 35/61] add unit tests for notifier --- ...TokenRateObserverWithOutOfGasErrorStub.sol | 30 ++++++++++++ .../TokenRateObserverWithSomeErrorStub.sol | 24 ++++++++++ .../optimism/interfaces/ITokenRatePusher.sol | 12 +++++ .../optimism/stubs/TokenRatePusherStub.sol | 15 ++++++ test/optimism/TokenRateNotifier.unit.test.ts | 47 ++++++++++++++++--- 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol create mode 100644 contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol create mode 100644 contracts/optimism/interfaces/ITokenRatePusher.sol create mode 100644 contracts/optimism/stubs/TokenRatePusherStub.sol diff --git a/contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol b/contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol new file mode 100644 index 00000000..09cf84a4 --- /dev/null +++ b/contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRateObserver} from "../interfaces/ITokenRateObserver.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +contract TokenRateObserverWithOutOfGasErrorStub is ERC165, ITokenRateObserver { + + error SomeError(); + + mapping (uint256 => uint256) data; + + function handleTokenRebased() external { + for (uint256 i = 0; i < 1000000000000; ++i) { + data[i] = i; + } + + //revert SomeError(); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(ITokenRateObserver).interfaceId + || super.supportsInterface(_interfaceId) + ); + } +} diff --git a/contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol b/contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol new file mode 100644 index 00000000..ff0bd615 --- /dev/null +++ b/contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRateObserver} from "../interfaces/ITokenRateObserver.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +contract TokenRateObserverWithSomeErrorStub is ERC165, ITokenRateObserver { + + error SomeError(); + + function handleTokenRebased() external { + revert SomeError(); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(ITokenRateObserver).interfaceId + || super.supportsInterface(_interfaceId) + ); + } +} diff --git a/contracts/optimism/interfaces/ITokenRatePusher.sol b/contracts/optimism/interfaces/ITokenRatePusher.sol new file mode 100644 index 00000000..14952553 --- /dev/null +++ b/contracts/optimism/interfaces/ITokenRatePusher.sol @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +/// @author kovalgek +/// @notice An interface for entity that pushes rate. +interface ITokenRatePusher { + /// @notice Pushes token rate to L2 by depositing zero tokens. + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + function pushTokenRate(uint32 l2Gas_) external; +} diff --git a/contracts/optimism/stubs/TokenRatePusherStub.sol b/contracts/optimism/stubs/TokenRatePusherStub.sol new file mode 100644 index 00000000..8748e26d --- /dev/null +++ b/contracts/optimism/stubs/TokenRatePusherStub.sol @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ITokenRatePusher} from "../interfaces/ITokenRatePusher.sol"; + +contract TokenRatePusherStub is ITokenRatePusher { + + uint32 public l2Gas; + + function pushTokenRate(uint32 l2Gas_) external { + l2Gas = l2Gas_; + } +} diff --git a/test/optimism/TokenRateNotifier.unit.test.ts b/test/optimism/TokenRateNotifier.unit.test.ts index 7415c5a7..73a1af4a 100644 --- a/test/optimism/TokenRateNotifier.unit.test.ts +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -2,13 +2,15 @@ import hre from "hardhat"; import { assert } from "chai"; import { unit } from "../../utils/testing"; import { BigNumber, utils } from 'ethers' -import { ethers } from 'hardhat' import { TokenRateNotifier__factory, ObserversArray__factory, OpStackTokenRateObserver__factory, ITokenRateObserver__factory, + TokenRateObserverWithSomeErrorStub__factory, + TokenRateObserverWithOutOfGasErrorStub__factory, + TokenRatePusherStub__factory } from "../../typechain"; unit("TokenRateNotifier", ctxFactory) @@ -100,12 +102,47 @@ unit("TokenRateNotifier", ctxFactory) assert.equalBN(await tokenRateNotifier.observersLength(), 0); }) + .test("notify observers with some error", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const observer = await new TokenRateObserverWithSomeErrorStub__factory(deployer).deploy(); + await tokenRateNotifier.addObserver(observer.address); + + const tx = await tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7); + + await assert.emits(tokenRateNotifier, tx, "HandleTokenRebasedFailed", [observer.address, "0x332e27d2"]); + }) + + .test("notify observers with out of gas error", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const observer = await new TokenRateObserverWithOutOfGasErrorStub__factory(deployer).deploy(); + await tokenRateNotifier.addObserver(observer.address); + + await assert.revertsWith(tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7), "ErrorUnrecoverableObserver()"); + }) + + .test("notify observers", async (ctx) => { + const { tokenRateNotifier } = ctx.contracts; + const { deployer } = ctx.accounts; + + const tokenRatePusher = await new TokenRatePusherStub__factory(deployer).deploy(); + const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(tokenRatePusher.address, 22); + await tokenRateNotifier.addObserver(observer.address); + + assert.equalBN(await tokenRatePusher.l2Gas(), 0); + + await tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7); + + assert.equalBN(await tokenRatePusher.l2Gas(), 22); + }) + .run(); async function ctxFactory() { - const [deployer, bridge, stranger] = await hre.ethers.getSigners(); - const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(); return { @@ -115,12 +152,10 @@ async function ctxFactory() { } export function getInterfaceID(contractInterface: utils.Interface) { - let interfaceID = ethers.constants.Zero; + let interfaceID = hre.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; } - - From 55111b8cf71c57ba96af6a8fa7a7de25e6dbe33a Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 22 Mar 2024 13:54:53 +0100 Subject: [PATCH 36/61] add token rate pusher --- contracts/lido/ObserversArray.sol | 4 +- contracts/lido/TokenRateNotifier.sol | 6 +-- .../lido/interfaces/ITokenRateObserver.sol | 12 ----- .../interfaces/ITokenRatePusher.sol | 3 +- contracts/optimism/L1ERC20TokenBridge.sol | 15 ++---- contracts/optimism/L2ERC20TokenBridge.sol | 4 +- .../optimism/OpStackTokenRateObserver.sol | 39 -------------- .../OpStackTokenRateObserverFactory.sol | 41 --------------- contracts/optimism/OpStackTokenRatePusher.sol | 51 +++++++++++++++++++ .../RebasableAndNonRebasableTokens.sol | 4 +- contracts/optimism/TokenRateOracle.sol | 19 +++++-- 11 files changed, 82 insertions(+), 116 deletions(-) delete mode 100644 contracts/lido/interfaces/ITokenRateObserver.sol rename contracts/{optimism => lido}/interfaces/ITokenRatePusher.sol (69%) delete mode 100644 contracts/optimism/OpStackTokenRateObserver.sol delete mode 100644 contracts/optimism/OpStackTokenRateObserverFactory.sol create mode 100644 contracts/optimism/OpStackTokenRatePusher.sol diff --git a/contracts/lido/ObserversArray.sol b/contracts/lido/ObserversArray.sol index 3c1e6fcf..8d1698f8 100644 --- a/contracts/lido/ObserversArray.sol +++ b/contracts/lido/ObserversArray.sol @@ -34,7 +34,7 @@ contract ObserversArray is Ownable, IObserversArray { } /// @inheritdoc IObserversArray - function addObserver(address observer_) external virtual onlyOwner { + function addObserver(address observer_) external onlyOwner { if (observer_ == address(0)) { revert ErrorZeroAddressObserver(); } @@ -50,7 +50,7 @@ contract ObserversArray is Ownable, IObserversArray { } /// @inheritdoc IObserversArray - function removeObserver(address observer_) external virtual onlyOwner { + function removeObserver(address observer_) external onlyOwner { uint256 observerIndexToRemove = _observerIndex(observer_); diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol index 59904d2e..8bd28d39 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -4,14 +4,14 @@ pragma solidity 0.8.10; import {IPostTokenRebaseReceiver} from "./interfaces/IPostTokenRebaseReceiver.sol"; -import {ITokenRateObserver} from "./interfaces/ITokenRateObserver.sol"; +import {ITokenRatePusher} from "./interfaces/ITokenRatePusher.sol"; import {ObserversArray} from "./ObserversArray.sol"; /// @author kovalgek /// @notice Notifies all observers when rebase event occures. contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { - constructor() ObserversArray(type(ITokenRateObserver).interfaceId) { + constructor() ObserversArray(type(ITokenRatePusher).interfaceId) { } /// @inheritdoc IPostTokenRebaseReceiver @@ -27,7 +27,7 @@ contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { uint256 observersLength = observersLength(); for (uint256 obIndex = 0; obIndex < observersLength; obIndex++) { - try ITokenRateObserver(observers[obIndex]).handleTokenRebased() {} + 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 diff --git a/contracts/lido/interfaces/ITokenRateObserver.sol b/contracts/lido/interfaces/ITokenRateObserver.sol deleted file mode 100644 index 5f86ce24..00000000 --- a/contracts/lido/interfaces/ITokenRateObserver.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @notice An interface to subscribe for token rebases. It is used to handle different rollups. -interface ITokenRateObserver { - - /// @notice It's called when rebase event occures. - function handleTokenRebased() external; -} diff --git a/contracts/optimism/interfaces/ITokenRatePusher.sol b/contracts/lido/interfaces/ITokenRatePusher.sol similarity index 69% rename from contracts/optimism/interfaces/ITokenRatePusher.sol rename to contracts/lido/interfaces/ITokenRatePusher.sol index 14952553..9492a240 100644 --- a/contracts/optimism/interfaces/ITokenRatePusher.sol +++ b/contracts/lido/interfaces/ITokenRatePusher.sol @@ -7,6 +7,5 @@ pragma solidity 0.8.10; /// @notice An interface for entity that pushes rate. interface ITokenRatePusher { /// @notice Pushes token rate to L2 by depositing zero tokens. - /// @param l2Gas_ Gas limit required to complete the deposit on L2. - function pushTokenRate(uint32 l2Gas_) external; + function pushTokenRate() external; } diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20TokenBridge.sol index 7b30efe2..087df07f 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20TokenBridge.sol @@ -48,14 +48,9 @@ abstract contract L1ERC20TokenBridge is L2_TOKEN_BRIDGE = l2TokenBridge_; } + /// @notice required to abstact a way token rate is requested. function tokenRate() virtual internal view returns (uint256); - /// @notice Pushes token rate to L2 by depositing zero tokens. - /// @param l2Gas_ Gas limit required to complete the deposit on L2. - function pushTokenRate(uint32 l2Gas_) external { - _depositERC20To(L1_TOKEN_REBASABLE, L2_TOKEN_REBASABLE, L2_TOKEN_BRIDGE, 0, l2Gas_, ""); - } - /// @inheritdoc IL1ERC20Bridge function l2TokenBridge() external view returns (address) { return L2_TOKEN_BRIDGE; @@ -114,7 +109,7 @@ abstract contract L1ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) { - if (isRebasableTokenFlow(l1Token_, l2Token_)) { + if (_isRebasableTokenFlow(l1Token_, l2Token_)) { uint256 rebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); IERC20(L1_TOKEN_REBASABLE).safeTransfer(to_, rebasableTokenAmount); @@ -126,7 +121,7 @@ abstract contract L1ERC20TokenBridge is rebasableTokenAmount, data_ ); - } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + } else if (_isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(L1_TOKEN_NON_REBASABLE).safeTransfer(to_, amount_); emit ERC20WithdrawalFinalized( @@ -148,7 +143,7 @@ abstract contract L1ERC20TokenBridge is uint32 l2Gas_, bytes memory data_ ) internal { - if (isRebasableTokenFlow(l1Token_, l2Token_)) { + if (_isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = DepositData({ rate: uint96(tokenRate()), timestamp: uint40(block.timestamp), @@ -203,7 +198,7 @@ abstract contract L1ERC20TokenBridge is amount_, encodedDepositData ); - } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + } else if (_isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20(L1_TOKEN_NON_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); _initiateERC20Deposit( diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index cb8cc58f..2c7b54b4 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -93,7 +93,7 @@ contract L2ERC20TokenBridge is onlySupportedL2Token(l2Token_) onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) { - if (isRebasableTokenFlow(l1Token_, l2Token_)) { + if (_isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); @@ -109,7 +109,7 @@ contract L2ERC20TokenBridge is rebasableTokenAmount, depositData.data ); - } else if (isNonRebasableTokenFlow(l1Token_, l2Token_)) { + } else if (_isNonRebasableTokenFlow(l1Token_, l2Token_)) { IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeMint(to_, amount_); emit DepositFinalized( L1_TOKEN_NON_REBASABLE, diff --git a/contracts/optimism/OpStackTokenRateObserver.sol b/contracts/optimism/OpStackTokenRateObserver.sol deleted file mode 100644 index 38d08d69..00000000 --- a/contracts/optimism/OpStackTokenRateObserver.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {ITokenRateObserver} from "../lido/interfaces/ITokenRateObserver.sol"; -import {L1LidoTokensBridge} from "./L1LidoTokensBridge.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; - -/// @author kovalgek -/// @notice Pushes token rate when rebase event happens. -contract OpStackTokenRateObserver is ERC165, ITokenRateObserver { - - /// @notice Contract of OpStack bridge. - L1LidoTokensBridge public immutable L1_LIDO_TOKENS_BRIDGE; - - /// @notice Gas limit required to complete pushing token rate on L2. - uint32 public immutable L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE; - - /// @param lidoTokensBridge_ OpStack bridge. - /// @param l2GasLimitForPushingTokenRate_ Gas limit required to complete pushing token rate on L2. - constructor(address lidoTokensBridge_, uint32 l2GasLimitForPushingTokenRate_) { - L1_LIDO_TOKENS_BRIDGE = L1LidoTokensBridge(lidoTokensBridge_); - L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE = l2GasLimitForPushingTokenRate_; - } - - /// @inheritdoc ITokenRateObserver - function handleTokenRebased() external { - L1_LIDO_TOKENS_BRIDGE.pushTokenRate(L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE); - } - - /// @inheritdoc ERC165 - function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { - return ( - _interfaceId == type(ITokenRateObserver).interfaceId - || super.supportsInterface(_interfaceId) - ); - } -} diff --git a/contracts/optimism/OpStackTokenRateObserverFactory.sol b/contracts/optimism/OpStackTokenRateObserverFactory.sol deleted file mode 100644 index 10065b07..00000000 --- a/contracts/optimism/OpStackTokenRateObserverFactory.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {OpStackTokenRateObserver} from "./OpStackTokenRateObserver.sol"; - -/// @author kovalgek -/// @notice Factory for deploying observers for OP stack. -contract OpStackTokenRateObserverFactory { - - /// @notice deploys observer for OP stack. - /// @param lidoTokensBridge_ OpStack bridge. - /// @param l2GasLimitForPushingTokenRate_ Gas limit required to complete pushing token rate on L2. - function deployOpStackTokenRateObserver( - address lidoTokensBridge_, - uint32 l2GasLimitForPushingTokenRate_ - ) external returns (OpStackTokenRateObserver opStackTokenRateObserver) { - - opStackTokenRateObserver = new OpStackTokenRateObserver( - lidoTokensBridge_, - l2GasLimitForPushingTokenRate_ - ); - - emit OpStackTokenRateObserverDeployed( - msg.sender, - address(opStackTokenRateObserver), - lidoTokensBridge_, - l2GasLimitForPushingTokenRate_ - ); - - return opStackTokenRateObserver; - } - - event OpStackTokenRateObserverDeployed( - address indexed creator, - address indexed opStackTokenRateObserver, - address lidoTokensBridge, - uint32 l2GasLimitForPushingTokenRate - ); -} diff --git a/contracts/optimism/OpStackTokenRatePusher.sol b/contracts/optimism/OpStackTokenRatePusher.sol new file mode 100644 index 00000000..336252fb --- /dev/null +++ b/contracts/optimism/OpStackTokenRatePusher.sol @@ -0,0 +1,51 @@ +// 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 "../token/interfaces/IERC20WstETH.sol"; +import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; + +/// @author kovalgek +/// @notice Pushes token rate to L2 Oracle. +contract OpStackTokenRatePusher is CrossDomainEnabled, 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 WST_ETH; + + /// @notice Gas limit required to complete pushing token rate on L2. + 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_) { + WST_ETH = wstEth_; + L2_TOKEN_RATE_ORACLE = tokenRateOracle_; + L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE = l2GasLimitForPushingTokenRate_; + } + + /// @inheritdoc ITokenRatePusher + function pushTokenRate() external { + uint256 tokenRate = IERC20WstETH(WST_ETH).stEthPerToken(); + + bytes memory message = abi.encodeWithSelector( + ITokenRateOracle.updateRate.selector, + tokenRate, + block.timestamp + ); + + sendCrossDomainMessage(L2_TOKEN_RATE_ORACLE, L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE, message); + } +} diff --git a/contracts/optimism/RebasableAndNonRebasableTokens.sol b/contracts/optimism/RebasableAndNonRebasableTokens.sol index a7cca519..bffff151 100644 --- a/contracts/optimism/RebasableAndNonRebasableTokens.sol +++ b/contracts/optimism/RebasableAndNonRebasableTokens.sol @@ -53,11 +53,11 @@ contract RebasableAndNonRebasableTokens { _; } - function isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + function _isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { return l1Token_ == L1_TOKEN_REBASABLE && l2Token_ == L2_TOKEN_REBASABLE; } - function isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { + function _isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { return l1Token_ == L1_TOKEN_NON_REBASABLE && l2Token_ == L2_TOKEN_NON_REBASABLE; } diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index dae90761..20e92a25 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -4,14 +4,18 @@ pragma solidity 0.8.10; import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; +import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; /// @author kovalgek /// @notice Oracle for storing token rate. -contract TokenRateOracle is ITokenRateOracle { +contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { /// @notice A bridge which can update oracle. address public immutable 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 RATE_OUTDATED_DELAY; @@ -24,10 +28,18 @@ contract TokenRateOracle is ITokenRateOracle { /// @notice Decimals of the oracle response. uint8 private constant DECIMALS = 18; + /// @param messenger_ L2 messenger address being used for cross-chain communications /// @param bridge_ the bridge address that has a right to updates oracle. + /// @param l1TokenRatePusher_ An address of account on L1 that can update token rate. /// @param rateOutdatedDelay_ time period when token rate can be considered outdated. - constructor(address bridge_, uint256 rateOutdatedDelay_) { + constructor( + address messenger_, + address bridge_, + address l1TokenRatePusher_, + uint256 rateOutdatedDelay_ + ) CrossDomainEnabled(messenger_) { BRIDGE = bridge_; + L1_TOKEN_RATE_PUSHER = l1TokenRatePusher_; RATE_OUTDATED_DELAY = rateOutdatedDelay_; } @@ -63,7 +75,8 @@ contract TokenRateOracle is ITokenRateOracle { /// @inheritdoc ITokenRateOracle function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { - if (msg.sender != BRIDGE) { + if ((msg.sender != address(MESSENGER) || MESSENGER.xDomainMessageSender() != L1_TOKEN_RATE_PUSHER) && + (msg.sender != BRIDGE) { revert ErrorNoRights(msg.sender); } From d99a52f961edf13c2a2931c5285c52e955342fc2 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 22 Mar 2024 14:24:40 +0100 Subject: [PATCH 37/61] update unit tests for token rate oracle --- contracts/optimism/TokenRateOracle.sol | 2 +- test/optimism/TokenRateOracle.unit.test.ts | 77 ++++++++++++++++++---- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index 20e92a25..8c908da8 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -76,7 +76,7 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { if ((msg.sender != address(MESSENGER) || MESSENGER.xDomainMessageSender() != L1_TOKEN_RATE_PUSHER) && - (msg.sender != BRIDGE) { + msg.sender != BRIDGE) { revert ErrorNoRights(msg.sender); } diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index 56aafc81..6851237d 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -1,15 +1,22 @@ import hre from "hardhat"; import { assert } from "chai"; -import { unit } from "../../utils/testing"; -import { TokenRateOracle__factory } from "../../typechain"; +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 } = ctx.contracts; - const { bridge } = ctx.accounts; + const { tokenRateOracle, l2MessengerStub } = ctx.contracts; + const { bridge, l1TokenBridgeEOA } = ctx.accounts; + assert.equal(await tokenRateOracle.MESSENGER(), l2MessengerStub.address); assert.equal(await tokenRateOracle.BRIDGE(), bridge.address); + assert.equal(await tokenRateOracle.L1_TOKEN_RATE_PUSHER(), l1TokenBridgeEOA.address); + assert.equalBN(await tokenRateOracle.RATE_OUTDATED_DELAY(), 86400); assert.equalBN(await tokenRateOracle.latestAnswer(), 0); @@ -29,22 +36,28 @@ unit("TokenRateOracle", ctxFactory) assert.equalBN(await tokenRateOracle.decimals(), 18); }) - .test("updateRate() :: no rights to call", async (ctx) => { + .test("updateRate() :: called by non-bridge account", async (ctx) => { const { tokenRateOracle } = ctx.contracts; - const { bridge, stranger } = ctx.accounts; - tokenRateOracle.connect(bridge).updateRate(10, 20); + 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; - tokenRateOracle.connect(bridge).updateRate(10, 1000); + await tokenRateOracle.connect(bridge).updateRate(10, 1000); await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "ErrorIncorrectRateTimestamp()"); }) - .test("updateRate() :: dont update state if values are the same", async (ctx) => { + .test("updateRate() :: don't update state if values are the same", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { bridge } = ctx.accounts; @@ -55,14 +68,43 @@ unit("TokenRateOracle", ctxFactory) await assert.notEmits(tokenRateOracle, tx2, "RateUpdated"); }) - .test("updateRate() :: happy path", async (ctx) => { + .test("updateRate() :: happy path called by bridge", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { bridge } = ctx.accounts; const currentTime = Date.now(); const tokenRate = 123; - await tokenRateOracle.connect(bridge).updateRate(tokenRate, currentTime ); + await tokenRateOracle.connect(bridge).updateRate(tokenRate, currentTime); + + assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRate); + + const { + roundId_, + answer_, + startedAt_, + updatedAt_, + answeredInRound_ + } = await tokenRateOracle.latestRoundData(); + + assert.equalBN(roundId_, currentTime); + assert.equalBN(answer_, tokenRate); + assert.equalBN(startedAt_, currentTime); + assert.equalBN(updatedAt_, currentTime); + assert.equalBN(answeredInRound_, currentTime); + 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; + + await l2MessengerStub.setXDomainMessageSender(l1TokenBridgeEOA.address); + + const currentTime = Date.now(); + const tokenRate = 123; + + await tokenRateOracle.connect(l2MessengerStubEOA).updateRate(tokenRate, currentTime); assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRate); @@ -86,15 +128,22 @@ unit("TokenRateOracle", ctxFactory) async function ctxFactory() { - const [deployer, bridge, stranger] = await hre.ethers.getSigners(); + 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 ); return { - accounts: { deployer, bridge, stranger }, - contracts: { tokenRateOracle } + accounts: { deployer, bridge, stranger, l1TokenBridgeEOA, l2MessengerStubEOA }, + contracts: { tokenRateOracle, l2MessengerStub } }; } From fb3eb406bd2f0557a231d84002bed5c2a22bc175 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 22 Mar 2024 19:03:57 +0100 Subject: [PATCH 38/61] remove observer array --- contracts/lido/ObserversArray.sol | 90 ----------------- contracts/lido/TokenRateNotifier.sol | 97 ++++++++++++++++--- contracts/lido/interfaces/IObserversArray.sol | 34 ------- contracts/optimism/OpStackTokenRatePusher.sol | 6 +- contracts/optimism/TokenRateOracle.sol | 6 +- test/token/ERC20Rebasable.unit.test.ts | 25 +++-- 6 files changed, 111 insertions(+), 147 deletions(-) delete mode 100644 contracts/lido/ObserversArray.sol delete mode 100644 contracts/lido/interfaces/IObserversArray.sol diff --git a/contracts/lido/ObserversArray.sol b/contracts/lido/ObserversArray.sol deleted file mode 100644 index 8d1698f8..00000000 --- a/contracts/lido/ObserversArray.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import {IObserversArray} from "./interfaces/IObserversArray.sol"; - -/// @author kovalgek -/// @notice Manage observers. -contract ObserversArray is Ownable, IObserversArray { - using ERC165Checker for address; - - /// @notice Maximum amount of observers to be supported. - uint256 public constant MAX_OBSERVERS_COUNT = 16; - - /// @notice Invalid interface id. - bytes4 public constant INVALID_INTERFACE_ID = 0xffffffff; - - /// @notice An interface that each observer should support. - bytes4 public immutable REQUIRED_INTERFACE; - - /// @notice All observers. - address[] public observers; - - /// @param requiredInterface_ An interface that each observer should support. - constructor(bytes4 requiredInterface_) { - if (requiredInterface_ == INVALID_INTERFACE_ID) { - revert ErrorInvalidInterface(); - } - - REQUIRED_INTERFACE = requiredInterface_; - } - - /// @inheritdoc IObserversArray - 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(); - } - - observers.push(observer_); - emit ObserverAdded(observer_); - } - - /// @inheritdoc IObserversArray - function removeObserver(address observer_) external onlyOwner { - - uint256 observerIndexToRemove = _observerIndex(observer_); - - if (observerIndexToRemove == type(uint256).max) { - revert ErrorNoObserverToRemove(); - } - - for (uint256 obIndex = observerIndexToRemove; obIndex < observers.length - 1; obIndex++) { - observers[obIndex] = observers[obIndex + 1]; - } - - observers.pop(); - - emit ObserverRemoved(observer_); - } - - /// @inheritdoc IObserversArray - function observersLength() public view returns (uint256) { - return observers.length; - } - - /// @notice `observer_` index in `observers` array. - function _observerIndex(address observer_) internal view returns (uint256) { - for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { - if (observers[obIndex] == observer_) { - return obIndex; - } - } - return type(uint256).max; - } - - error ErrorInvalidInterface(); - error ErrorZeroAddressObserver(); - error ErrorBadObserverInterface(); - error ErrorMaxObserversCountExceeded(); - error ErrorNoObserverToRemove(); -} diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol index 8bd28d39..55004123 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -5,13 +5,63 @@ pragma solidity 0.8.10; import {IPostTokenRebaseReceiver} from "./interfaces/IPostTokenRebaseReceiver.sol"; import {ITokenRatePusher} from "./interfaces/ITokenRatePusher.sol"; -import {ObserversArray} from "./ObserversArray.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; /// @author kovalgek /// @notice Notifies all observers when rebase event occures. -contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { +contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { + using ERC165Checker for address; - constructor() ObserversArray(type(ITokenRatePusher).interfaceId) { + /// @notice Maximum amount of observers to be supported. + uint256 public constant MAX_OBSERVERS_COUNT = 16; + + /// @notice Invalid interface id. + bytes4 public constant INVALID_INTERFACE_ID = 0xffffffff; + + /// @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; + + /// @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(); + } + + 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(); + } + + for (uint256 obIndex = observerIndexToRemove; obIndex < observers.length - 1; obIndex++) { + observers[obIndex] = observers[obIndex + 1]; + } + + observers.pop(); + + emit ObserverRemoved(observer_); } /// @inheritdoc IPostTokenRebaseReceiver @@ -24,18 +74,18 @@ contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { uint256, uint256 ) external { - uint256 observersLength = observersLength(); + uint256 obLength = observersLength(); - for (uint256 obIndex = 0; obIndex < observersLength; obIndex++) { + for (uint256 obIndex = 0; obIndex < obLength; obIndex++) { 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 handleTokenRebased() reverts because of the - /// "out of gas" error. Here we assume that the handleTokenRebased() method doesn't + /// 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 ErrorUnrecoverableObserver(); - emit HandleTokenRebasedFailed( + if (lowLevelRevertData.length == 0) revert ErrorTokenRateNotifierRevertedWithNoData(); + emit PushTokenRateFailed( observers[obIndex], lowLevelRevertData ); @@ -43,7 +93,32 @@ contract TokenRateNotifier is ObserversArray, IPostTokenRebaseReceiver { } } - event HandleTokenRebasedFailed(address indexed observer, bytes lowLevelRevertData); + /// @notice Observer length + /// @return Added observers count + function observersLength() public 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) { + uint256 obLength = observersLength(); + + for (uint256 obIndex = 0; obIndex < obLength; 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 ErrorUnrecoverableObserver(); + error ErrorTokenRateNotifierRevertedWithNoData(); + error ErrorZeroAddressObserver(); + error ErrorBadObserverInterface(); + error ErrorMaxObserversCountExceeded(); + error ErrorNoObserverToRemove(); } diff --git a/contracts/lido/interfaces/IObserversArray.sol b/contracts/lido/interfaces/IObserversArray.sol deleted file mode 100644 index f3a867b7..00000000 --- a/contracts/lido/interfaces/IObserversArray.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @notice An interface for observer pattern -interface IObserversArray { - - /// @notice Observer added event - /// @dev emitted by `addObserver` function - event ObserverAdded(address indexed observer); - - /// @notice Observer removed event - /// @dev emitted by `removeObserver` function - event ObserverRemoved(address indexed observer); - - /// @notice Observer length - /// @return Added observers count - function observersLength() external view returns (uint256); - - /// @notice Add a `observer_` to the back of array - /// @param observer_ observer address - function addObserver(address observer_) external; - - /// @notice Remove a observer at the given `observer_` position - /// @param observer_ observer remove position - function removeObserver(address observer_) external; - - /// @notice Get observer at position - /// @return Observer at the given `atIndex_` - /// @dev function reverts if `atIndex_` is out of range - function observers(uint256 atIndex_) external view returns (address); -} diff --git a/contracts/optimism/OpStackTokenRatePusher.sol b/contracts/optimism/OpStackTokenRatePusher.sol index 336252fb..c0c3e747 100644 --- a/contracts/optimism/OpStackTokenRatePusher.sol +++ b/contracts/optimism/OpStackTokenRatePusher.sol @@ -16,7 +16,7 @@ contract OpStackTokenRatePusher is CrossDomainEnabled, ITokenRatePusher { address public immutable L2_TOKEN_RATE_ORACLE; /// @notice Non-rebasable token of Core Lido procotol. - address public immutable WST_ETH; + address public immutable WSTETH; /// @notice Gas limit required to complete pushing token rate on L2. uint32 public immutable L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE; @@ -31,14 +31,14 @@ contract OpStackTokenRatePusher is CrossDomainEnabled, ITokenRatePusher { address tokenRateOracle_, uint32 l2GasLimitForPushingTokenRate_ ) CrossDomainEnabled(messenger_) { - WST_ETH = wstEth_; + WSTETH = wstEth_; L2_TOKEN_RATE_ORACLE = tokenRateOracle_; L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE = l2GasLimitForPushingTokenRate_; } /// @inheritdoc ITokenRatePusher function pushTokenRate() external { - uint256 tokenRate = IERC20WstETH(WST_ETH).stEthPerToken(); + uint256 tokenRate = IERC20WstETH(WSTETH).stEthPerToken(); bytes memory message = abi.encodeWithSelector( ITokenRateOracle.updateRate.selector, diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index 8c908da8..a191e216 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -75,8 +75,10 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { /// @inheritdoc ITokenRateOracle function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { - if ((msg.sender != address(MESSENGER) || MESSENGER.xDomainMessageSender() != L1_TOKEN_RATE_PUSHER) && - msg.sender != BRIDGE) { + if (!( + (msg.sender == address(MESSENGER) && MESSENGER.xDomainMessageSender() == L1_TOKEN_RATE_PUSHER) + || (msg.sender == BRIDGE) + )) { revert ErrorNoRights(msg.sender); } diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 2561256c..b8cb3e47 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -7,7 +7,8 @@ import { ERC20Bridged__factory, TokenRateOracle__factory, ERC20Rebasable__factory, - OssifiableProxy__factory + OssifiableProxy__factory, + CrossDomainMessengerStub__factory } from "../../typechain"; import { BigNumber } from "ethers"; @@ -32,7 +33,7 @@ unit("ERC20Rebasable", ctxFactory) ) .test("initialize() :: name already set", async (ctx) => { - const { deployer, owner } = ctx.accounts; + const { deployer, owner, zero } = ctx.accounts; const { decimalsToSet } = ctx.constants; // deploy new implementation @@ -42,8 +43,11 @@ unit("ERC20Rebasable", ctxFactory) decimalsToSet, owner.address ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, owner.address, + zero.address, 86400 ); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( @@ -61,7 +65,7 @@ unit("ERC20Rebasable", ctxFactory) }) .test("initialize() :: symbol already set", async (ctx) => { - const { deployer, owner } = ctx.accounts; + const { deployer, owner, zero } = ctx.accounts; const { decimalsToSet } = ctx.constants; // deploy new implementation @@ -72,7 +76,9 @@ unit("ERC20Rebasable", ctxFactory) owner.address ); const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, owner.address, + zero.address, 86400 ); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( @@ -106,7 +112,7 @@ unit("ERC20Rebasable", ctxFactory) .test("wrap() :: wrong oracle update time", async (ctx) => { - const { deployer, user1, owner } = ctx.accounts; + const { deployer, user1, owner, zero } = ctx.accounts; const { decimalsToSet } = ctx.constants; // deploy new implementation to test initial oracle state @@ -117,7 +123,9 @@ unit("ERC20Rebasable", ctxFactory) owner.address ); const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, owner.address, + zero.address, 86400 ); const rebasableProxied = await new ERC20Rebasable__factory(deployer).deploy( @@ -284,7 +292,7 @@ unit("ERC20Rebasable", ctxFactory) .test("unwrap() :: with wrong oracle update time", async (ctx) => { - const { deployer, user1, owner } = ctx.accounts; + const { deployer, user1, owner, zero } = ctx.accounts; const { decimalsToSet } = ctx.constants; // deploy new implementation to test initial oracle state @@ -295,7 +303,9 @@ unit("ERC20Rebasable", ctxFactory) owner.address ); const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, owner.address, + zero.address, 86400 ); const rebasableProxied = await new ERC20Rebasable__factory(deployer).deploy( @@ -1002,6 +1012,7 @@ async function ctxFactory() { 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", @@ -1010,7 +1021,9 @@ async function ctxFactory() { owner.address ); const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + zero.address, owner.address, + zero.address, 86400 ); const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( @@ -1027,8 +1040,6 @@ async function ctxFactory() { params: [hre.ethers.constants.AddressZero], }); - const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); - const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( rebasableTokenImpl.address, deployer.address, From b167ebd8276bac4076a64dec4334ef6aa25a9abb Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Sat, 23 Mar 2024 00:35:44 +0100 Subject: [PATCH 39/61] fix token rate publicher and notifier unit tests --- ...kTokenRatePusherWithOutOfGasErrorStub.sol} | 12 +- ...StackTokenRatePusherWithSomeErrorStub.sol} | 8 +- contracts/optimism/OpStackTokenRatePusher.sol | 11 +- .../optimism/stubs/TokenRatePusherStub.sol | 15 -- test/optimism/L2ERC20TokenBridge.unit.test.ts | 2 + .../OpStackTokenRatePusher.unit.test.ts | 102 +++++++++ test/optimism/TokenRateNotifier.unit.test.ts | 203 ++++++++++++------ 7 files changed, 257 insertions(+), 96 deletions(-) rename contracts/lido/stubs/{TokenRateObserverWithOutOfGasErrorStub.sol => OpStackTokenRatePusherWithOutOfGasErrorStub.sol} (63%) rename contracts/lido/stubs/{TokenRateObserverWithSomeErrorStub.sol => OpStackTokenRatePusherWithSomeErrorStub.sol} (64%) delete mode 100644 contracts/optimism/stubs/TokenRatePusherStub.sol create mode 100644 test/optimism/OpStackTokenRatePusher.unit.test.ts diff --git a/contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol similarity index 63% rename from contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol rename to contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol index 09cf84a4..818b4229 100644 --- a/contracts/lido/stubs/TokenRateObserverWithOutOfGasErrorStub.sol +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol @@ -3,27 +3,23 @@ pragma solidity 0.8.10; -import {ITokenRateObserver} from "../interfaces/ITokenRateObserver.sol"; +import {ITokenRatePusher} from "../interfaces/ITokenRatePusher.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -contract TokenRateObserverWithOutOfGasErrorStub is ERC165, ITokenRateObserver { - - error SomeError(); +contract OpStackTokenRatePusherWithOutOfGasErrorStub is ERC165, ITokenRatePusher { mapping (uint256 => uint256) data; - function handleTokenRebased() external { + function pushTokenRate() external { for (uint256 i = 0; i < 1000000000000; ++i) { data[i] = i; } - - //revert SomeError(); } /// @inheritdoc ERC165 function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { return ( - _interfaceId == type(ITokenRateObserver).interfaceId + _interfaceId == type(ITokenRatePusher).interfaceId || super.supportsInterface(_interfaceId) ); } diff --git a/contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol b/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol similarity index 64% rename from contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol rename to contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol index ff0bd615..5a8ddfdd 100644 --- a/contracts/lido/stubs/TokenRateObserverWithSomeErrorStub.sol +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol @@ -3,21 +3,21 @@ pragma solidity 0.8.10; -import {ITokenRateObserver} from "../interfaces/ITokenRateObserver.sol"; +import {ITokenRatePusher} from "../interfaces/ITokenRatePusher.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -contract TokenRateObserverWithSomeErrorStub is ERC165, ITokenRateObserver { +contract OpStackTokenRatePusherWithSomeErrorStub is ERC165, ITokenRatePusher { error SomeError(); - function handleTokenRebased() external { + function pushTokenRate() external { revert SomeError(); } /// @inheritdoc ERC165 function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { return ( - _interfaceId == type(ITokenRateObserver).interfaceId + _interfaceId == type(ITokenRatePusher).interfaceId || super.supportsInterface(_interfaceId) ); } diff --git a/contracts/optimism/OpStackTokenRatePusher.sol b/contracts/optimism/OpStackTokenRatePusher.sol index c0c3e747..65b06a34 100644 --- a/contracts/optimism/OpStackTokenRatePusher.sol +++ b/contracts/optimism/OpStackTokenRatePusher.sol @@ -7,10 +7,11 @@ import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {ITokenRatePusher} from "../lido/interfaces/ITokenRatePusher.sol"; import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; /// @author kovalgek /// @notice Pushes token rate to L2 Oracle. -contract OpStackTokenRatePusher is CrossDomainEnabled, ITokenRatePusher { +contract OpStackTokenRatePusher is CrossDomainEnabled, ERC165, ITokenRatePusher { /// @notice Oracle address on L2 for receiving token rate. address public immutable L2_TOKEN_RATE_ORACLE; @@ -48,4 +49,12 @@ contract OpStackTokenRatePusher is CrossDomainEnabled, ITokenRatePusher { 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/stubs/TokenRatePusherStub.sol b/contracts/optimism/stubs/TokenRatePusherStub.sol deleted file mode 100644 index 8748e26d..00000000 --- a/contracts/optimism/stubs/TokenRatePusherStub.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -import {ITokenRatePusher} from "../interfaces/ITokenRatePusher.sol"; - -contract TokenRatePusherStub is ITokenRatePusher { - - uint32 public l2Gas; - - function pushTokenRate(uint32 l2Gas_) external { - l2Gas = l2Gas_; - } -} diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index a3906b0b..fc0a54db 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -712,7 +712,9 @@ async function ctxFactory() { ); const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + l2MessengerStub.address, l2TokenBridgeProxyAddress, + l1TokenBridgeEOA.address, 86400 ); diff --git a/test/optimism/OpStackTokenRatePusher.unit.test.ts b/test/optimism/OpStackTokenRatePusher.unit.test.ts new file mode 100644 index 00000000..039c54dd --- /dev/null +++ b/test/optimism/OpStackTokenRatePusher.unit.test.ts @@ -0,0 +1,102 @@ +import hre 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 hre.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 hre.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 = hre.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 index 73a1af4a..9366d2bf 100644 --- a/test/optimism/TokenRateNotifier.unit.test.ts +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -1,153 +1,220 @@ import hre from "hardhat"; import { assert } from "chai"; +import { utils } from 'ethers' import { unit } from "../../utils/testing"; -import { BigNumber, utils } from 'ethers' - +import { wei } from "../../utils/wei"; import { TokenRateNotifier__factory, - ObserversArray__factory, - OpStackTokenRateObserver__factory, - ITokenRateObserver__factory, - TokenRateObserverWithSomeErrorStub__factory, - TokenRateObserverWithOutOfGasErrorStub__factory, - TokenRatePusherStub__factory + ITokenRatePusher__factory, + OpStackTokenRatePusher__factory, + ITokenRateOracle__factory, + ERC20BridgedStub__factory, + ERC20WrapperStub__factory, + CrossDomainMessengerStub__factory, + OpStackTokenRatePusherWithSomeErrorStub__factory, + OpStackTokenRatePusherWithOutOfGasErrorStub__factory } from "../../typechain"; unit("TokenRateNotifier", ctxFactory) - .test("init with wrong interface", async (ctx) => { - const { deployer } = ctx.accounts; - await assert.revertsWith(new ObserversArray__factory(deployer).deploy(BigNumber.from("0xffffffff")._hex), "ErrorInvalidInterface()"); - }) - .test("initial state", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; assert.equalBN(await tokenRateNotifier.MAX_OBSERVERS_COUNT(), 16); assert.equal(await tokenRateNotifier.INVALID_INTERFACE_ID(), "0xffffffff"); - const iTokenRateObserver = getInterfaceID(ITokenRateObserver__factory.createInterface()); + const iTokenRateObserver = getInterfaceID(ITokenRatePusher__factory.createInterface()); assert.equal(await tokenRateNotifier.REQUIRED_INTERFACE(), iTokenRateObserver._hex); assert.equalBN(await tokenRateNotifier.observersLength(), 0); }) - .test("add zero address observer", async (ctx) => { + .test("addObserver() :: not the owner", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; - await assert.revertsWith(tokenRateNotifier.addObserver(hre.ethers.constants.AddressZero), "ErrorZeroAddressObserver()"); + const { stranger } = ctx.accounts; + + await assert.revertsWith( + tokenRateNotifier + .connect(stranger) + .addObserver(hre.ethers.constants.AddressZero), + "Ownable: caller is not the owner" + ); }) - .test("add bad interface observer", async (ctx) => { + .test("addObserver() :: zero address observer", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; - const { deployer } = ctx.accounts; - const observer = await new TokenRateNotifier__factory(deployer).deploy(); - await assert.revertsWith(tokenRateNotifier.addObserver(observer.address), "ErrorBadObserverInterface()"); + await assert.revertsWith( + tokenRateNotifier.addObserver(hre.ethers.constants.AddressZero), + "ErrorZeroAddressObserver()" + ); }) - .test("add too many observers", async (ctx) => { + .test("addObserver() :: bad interface observer", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; const { deployer } = ctx.accounts; - assert.equalBN(await tokenRateNotifier.observersLength(), 0); + const observer = await new TokenRateNotifier__factory(deployer).deploy(); + await assert.revertsWith( + tokenRateNotifier.addObserver(observer.address), + "ErrorBadObserverInterface()" + ); + }) + .test("addObserver() :: too many observers", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); const maxObservers = await tokenRateNotifier.MAX_OBSERVERS_COUNT(); for (let i = 0; i < maxObservers.toNumber(); i++) { - const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); - await tokenRateNotifier.addObserver(observer.address); + await tokenRateNotifier.addObserver(opStackTokenRatePusher.address); } - assert.equalBN(await tokenRateNotifier.observersLength(), maxObservers); - const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); - await assert.revertsWith(tokenRateNotifier.addObserver(observer.address), "ErrorMaxObserversCountExceeded()"); + await assert.revertsWith( + tokenRateNotifier.addObserver(opStackTokenRatePusher.address), + "ErrorMaxObserversCountExceeded()" + ); }) - .test("add observer", async (ctx) => { - const { tokenRateNotifier } = ctx.contracts; - const { deployer } = ctx.accounts; + .test("addObserver() :: success", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); - - const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); - const tx = await tokenRateNotifier.addObserver(observer.address); - + const tx = await tokenRateNotifier.addObserver(opStackTokenRatePusher.address); assert.equalBN(await tokenRateNotifier.observersLength(), 1); - await assert.emits(tokenRateNotifier, tx, "ObserverAdded", [observer.address]); + await assert.emits(tokenRateNotifier, tx, "ObserverAdded", [opStackTokenRatePusher.address]); }) - .test("remove non-added observer", async (ctx) => { + .test("removeObserver() :: not the owner", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; - const { deployer } = ctx.accounts; + const { stranger } = ctx.accounts; + + await assert.revertsWith( + tokenRateNotifier + .connect(stranger) + .removeObserver(hre.ethers.constants.AddressZero), + "Ownable: caller is not the owner" + ); + }) + + .test("removeObserver() :: non-added observer", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); - const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); - await assert.revertsWith(tokenRateNotifier.removeObserver(observer.address), "ErrorNoObserverToRemove()"); + await assert.revertsWith( + tokenRateNotifier.removeObserver(opStackTokenRatePusher.address), + "ErrorNoObserverToRemove()" + ); }) - .test("remove observer", async (ctx) => { - const { tokenRateNotifier } = ctx.contracts; - const { deployer } = ctx.accounts; + .test("removeObserver() :: success", async (ctx) => { + const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); - const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(hre.ethers.constants.AddressZero, 10); - await tokenRateNotifier.addObserver(observer.address); + await tokenRateNotifier.addObserver(opStackTokenRatePusher.address); assert.equalBN(await tokenRateNotifier.observersLength(), 1); - const tx = await tokenRateNotifier.removeObserver(observer.address); - await assert.emits(tokenRateNotifier, tx, "ObserverRemoved", [observer.address]); + const tx = await tokenRateNotifier.removeObserver(opStackTokenRatePusher.address); + await assert.emits(tokenRateNotifier, tx, "ObserverRemoved", [opStackTokenRatePusher.address]); assert.equalBN(await tokenRateNotifier.observersLength(), 0); }) - .test("notify observers with some error", async (ctx) => { + .test("handlePostTokenRebase() :: failed with some error", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; const { deployer } = ctx.accounts; - const observer = await new TokenRateObserverWithSomeErrorStub__factory(deployer).deploy(); + const observer = await new OpStackTokenRatePusherWithSomeErrorStub__factory(deployer).deploy(); await tokenRateNotifier.addObserver(observer.address); const tx = await tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7); - await assert.emits(tokenRateNotifier, tx, "HandleTokenRebasedFailed", [observer.address, "0x332e27d2"]); + await assert.emits(tokenRateNotifier, tx, "PushTokenRateFailed", [observer.address, "0x332e27d2"]); }) - .test("notify observers with out of gas error", async (ctx) => { + .test("handlePostTokenRebase() :: out of gas error", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; const { deployer } = ctx.accounts; - const observer = await new TokenRateObserverWithOutOfGasErrorStub__factory(deployer).deploy(); + const observer = await new OpStackTokenRatePusherWithOutOfGasErrorStub__factory(deployer).deploy(); await tokenRateNotifier.addObserver(observer.address); - await assert.revertsWith(tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7), "ErrorUnrecoverableObserver()"); + await assert.revertsWith( + tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7), + "ErrorTokenRateNotifierRevertedWithNoData()" + ); }) - .test("notify observers", async (ctx) => { - const { tokenRateNotifier } = ctx.contracts; - const { deployer } = ctx.accounts; - - const tokenRatePusher = await new TokenRatePusherStub__factory(deployer).deploy(); - const observer = await new OpStackTokenRateObserver__factory(deployer).deploy(tokenRatePusher.address, 22); - await tokenRateNotifier.addObserver(observer.address); - - assert.equalBN(await tokenRatePusher.l2Gas(), 0); - - await tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7); - - assert.equalBN(await tokenRatePusher.l2Gas(), 22); + .test("handlePostTokenRebase() :: success", async (ctx) => { + const { + tokenRateNotifier, + l1MessengerStub, + opStackTokenRatePusher, + l1TokenNonRebasableStub + } = ctx.contracts; + const { tokenRateOracle } = ctx.accounts; + const { l2GasLimitForPushingTokenRate } = ctx.constants; + + let tokenRate = await l1TokenNonRebasableStub.stEthPerToken(); + await tokenRateNotifier.addObserver(opStackTokenRatePusher.address); + let tx = await tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7); + + const provider = await hre.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] = await hre.ethers.getSigners(); + const [deployer, bridge, stranger, tokenRateOracle] = await hre.ethers.getSigners(); const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(); + 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`) }); + + const l2GasLimitForPushingTokenRate = 123; + + const opStackTokenRatePusher = await new OpStackTokenRatePusher__factory(deployer).deploy( + l1MessengerStub.address, + l1TokenNonRebasableStub.address, + tokenRateOracle.address, + l2GasLimitForPushingTokenRate + ); + return { - accounts: { deployer, bridge, stranger }, - contracts: { tokenRateNotifier } + accounts: { deployer, bridge, stranger, tokenRateOracle }, + contracts: { tokenRateNotifier, opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub }, + constants: { l2GasLimitForPushingTokenRate } }; } From 55d2e3f85a4da7c768e873fb601ac3abf54b7671 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Sat, 23 Mar 2024 00:48:18 +0100 Subject: [PATCH 40/61] fix small comments from PR review --- contracts/lido/TokenRateNotifier.sol | 8 ++--- .../OpStackTokenRatePusher.unit.test.ts | 8 ++--- test/optimism/TokenRateNotifier.unit.test.ts | 32 +++++++++---------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol index 55004123..b9e9e17c 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -74,9 +74,7 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { uint256, uint256 ) external { - uint256 obLength = observersLength(); - - for (uint256 obIndex = 0; obIndex < obLength; obIndex++) { + for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { try ITokenRatePusher(observers[obIndex]).pushTokenRate() {} catch (bytes memory lowLevelRevertData) { /// @dev This check is required to prevent incorrect gas estimation of the method. @@ -102,9 +100,7 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { /// @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) { - uint256 obLength = observersLength(); - - for (uint256 obIndex = 0; obIndex < obLength; obIndex++) { + for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { if (observers[obIndex] == observer_) { return obIndex; } diff --git a/test/optimism/OpStackTokenRatePusher.unit.test.ts b/test/optimism/OpStackTokenRatePusher.unit.test.ts index 039c54dd..cf7a99f6 100644 --- a/test/optimism/OpStackTokenRatePusher.unit.test.ts +++ b/test/optimism/OpStackTokenRatePusher.unit.test.ts @@ -1,4 +1,4 @@ -import hre from "hardhat"; +import { ethers } from "hardhat"; import { assert } from "chai"; import { utils } from 'ethers' import { unit } from "../../utils/testing"; @@ -36,7 +36,7 @@ unit("OpStackTokenRatePusher", ctxFactory) let tx = await opStackTokenRatePusher.pushTokenRate(); - const provider = await hre.ethers.provider; + const provider = await ethers.provider; const blockNumber = await provider.getBlockNumber(); const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; @@ -58,7 +58,7 @@ unit("OpStackTokenRatePusher", ctxFactory) .run(); async function ctxFactory() { - const [deployer, bridge, stranger, tokenRateOracle, l1TokenBridgeEOA] = await hre.ethers.getSigners(); + const [deployer, bridge, stranger, tokenRateOracle, l1TokenBridgeEOA] = await ethers.getSigners(); const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( "L1 Token Rebasable", @@ -93,7 +93,7 @@ async function ctxFactory() { } export function getInterfaceID(contractInterface: utils.Interface) { - let interfaceID = hre.ethers.constants.Zero; + 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])); diff --git a/test/optimism/TokenRateNotifier.unit.test.ts b/test/optimism/TokenRateNotifier.unit.test.ts index 9366d2bf..14997721 100644 --- a/test/optimism/TokenRateNotifier.unit.test.ts +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -1,4 +1,4 @@ -import hre from "hardhat"; +import { ethers } from "hardhat"; import { assert } from "chai"; import { utils } from 'ethers' import { unit } from "../../utils/testing"; @@ -34,21 +34,21 @@ unit("TokenRateNotifier", ctxFactory) await assert.revertsWith( tokenRateNotifier .connect(stranger) - .addObserver(hre.ethers.constants.AddressZero), + .addObserver(ethers.constants.AddressZero), "Ownable: caller is not the owner" ); }) - .test("addObserver() :: zero address observer", async (ctx) => { + .test("addObserver() :: revert on adding zero address observer", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; await assert.revertsWith( - tokenRateNotifier.addObserver(hre.ethers.constants.AddressZero), + tokenRateNotifier.addObserver(ethers.constants.AddressZero), "ErrorZeroAddressObserver()" ); }) - .test("addObserver() :: bad interface observer", async (ctx) => { + .test("addObserver() :: revert on adding observer with bad interface", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; const { deployer } = ctx.accounts; @@ -59,7 +59,7 @@ unit("TokenRateNotifier", ctxFactory) ); }) - .test("addObserver() :: too many observers", async (ctx) => { + .test("addObserver() :: revert on adding too many observers", async (ctx) => { const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); @@ -75,7 +75,7 @@ unit("TokenRateNotifier", ctxFactory) ); }) - .test("addObserver() :: success", async (ctx) => { + .test("addObserver() :: happy path of adding observer", async (ctx) => { const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); @@ -85,19 +85,19 @@ unit("TokenRateNotifier", ctxFactory) await assert.emits(tokenRateNotifier, tx, "ObserverAdded", [opStackTokenRatePusher.address]); }) - .test("removeObserver() :: not the owner", async (ctx) => { + .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(hre.ethers.constants.AddressZero), + .removeObserver(ethers.constants.AddressZero), "Ownable: caller is not the owner" ); }) - .test("removeObserver() :: non-added observer", async (ctx) => { + .test("removeObserver() :: revert on removing non-added observer", async (ctx) => { const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); @@ -108,7 +108,7 @@ unit("TokenRateNotifier", ctxFactory) ); }) - .test("removeObserver() :: success", async (ctx) => { + .test("removeObserver() :: happy path of removing observer", async (ctx) => { const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; assert.equalBN(await tokenRateNotifier.observersLength(), 0); @@ -135,7 +135,7 @@ unit("TokenRateNotifier", ctxFactory) await assert.emits(tokenRateNotifier, tx, "PushTokenRateFailed", [observer.address, "0x332e27d2"]); }) - .test("handlePostTokenRebase() :: out of gas error", async (ctx) => { + .test("handlePostTokenRebase() :: revert when observer has out of gas error", async (ctx) => { const { tokenRateNotifier } = ctx.contracts; const { deployer } = ctx.accounts; @@ -148,7 +148,7 @@ unit("TokenRateNotifier", ctxFactory) ); }) - .test("handlePostTokenRebase() :: success", async (ctx) => { + .test("handlePostTokenRebase() :: happy path of handling token rebase", async (ctx) => { const { tokenRateNotifier, l1MessengerStub, @@ -162,7 +162,7 @@ unit("TokenRateNotifier", ctxFactory) await tokenRateNotifier.addObserver(opStackTokenRatePusher.address); let tx = await tokenRateNotifier.handlePostTokenRebase(1,2,3,4,5,6,7); - const provider = await hre.ethers.provider; + const provider = await ethers.provider; const blockNumber = await provider.getBlockNumber(); const blockTimestamp = (await provider.getBlock(blockNumber)).timestamp; @@ -184,7 +184,7 @@ unit("TokenRateNotifier", ctxFactory) .run(); async function ctxFactory() { - const [deployer, bridge, stranger, tokenRateOracle] = await hre.ethers.getSigners(); + const [deployer, bridge, stranger, tokenRateOracle] = await ethers.getSigners(); const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(); const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( @@ -219,7 +219,7 @@ async function ctxFactory() { } export function getInterfaceID(contractInterface: utils.Interface) { - let interfaceID = hre.ethers.constants.Zero; + 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])); From 13712caea7a1cc2b0e17dc69a7a96fbcce30cfda Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Wed, 27 Mar 2024 14:08:45 +0300 Subject: [PATCH 41/61] feat(steth): intermediate work on adding ERC-2612/EIP-1271 permit --- contracts/lib/SignatureChecker.sol | 77 +++++++ contracts/stubs/ERC1271PermitSignerMock.sol | 21 ++ contracts/token/ERC20RebasablePermit.sol | 159 +++++++++++++++ package.json | 2 + test/token/ERC20Permit.unit.test.ts | 214 ++++++++++++++++++++ utils/testing/permit-helpers.ts | 98 +++++++++ 6 files changed, 571 insertions(+) create mode 100644 contracts/lib/SignatureChecker.sol create mode 100644 contracts/stubs/ERC1271PermitSignerMock.sol create mode 100644 contracts/token/ERC20RebasablePermit.sol create mode 100644 test/token/ERC20Permit.unit.test.ts create mode 100644 utils/testing/permit-helpers.ts diff --git a/contracts/lib/SignatureChecker.sol b/contracts/lib/SignatureChecker.sol new file mode 100644 index 00000000..d42561c4 --- /dev/null +++ b/contracts/lib/SignatureChecker.sol @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 +// Writen based on (utils/cryptography/SignatureChecker.sol from d398d68 + +pragma solidity 0.8.10; + +import {ECDSA} from "@openzeppelin/contracts-v4.9/utils/cryptography/ECDSA.sol"; +import {IERC1271} from "@openzeppelin/contracts-v4.9/interfaces/IERC1271.sol"; + + + +/** + * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA + * signatures from externally owned accounts (EOAs) as well as ERC-1271 signatures from smart contract wallets like + * Argent and Safe Wallet (previously Gnosis Safe). + */ +library SignatureChecker { + /** + * @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the + * signature is validated against that smart contract using ERC-1271, otherwise it's validated using `ECDSA.recover`. + * + * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus + * change through time. It could return true at block N and false at block N+1 (or the opposite). + */ + function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + if (signer.code.length == 0) { + // return true; + (address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature); + return err == ECDSA.RecoverError.NoError && recovered == signer; + } else { + return isValidERC1271SignatureNow(signer, hash, signature); + } + } + + /** + * @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 isValidSignatureNow( + address signer, + bytes32 msgHash, + uint8 v, + bytes32 r, + bytes32 s + ) internal view returns (bool) { + if (signer.code.length == 0) { + (address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(msgHash, v, r, s); + return err == ECDSA.RecoverError.NoError && recovered == signer; + } else { + bytes memory signature = abi.encodePacked(r, s, v); + return isValidERC1271SignatureNow(signer, msgHash, signature); + } + } + + /** + * @dev Checks if a signature is valid for a given signer and data hash. The signature is validated + * against the signer smart contract using ERC-1271. + * + * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus + * change through time. It could return true at block N and false at block N+1 (or the opposite). + */ + function isValidERC1271SignatureNow( + address signer, + bytes32 hash, + bytes memory signature + ) internal view returns (bool) { + (bool success, bytes memory result) = signer.staticcall( + abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) + ); + return (success && + result.length >= 32 && + abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); + } +} diff --git a/contracts/stubs/ERC1271PermitSignerMock.sol b/contracts/stubs/ERC1271PermitSignerMock.sol new file mode 100644 index 00000000..c865691a --- /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; + + +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/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasablePermit.sol new file mode 100644 index 00000000..5a7843e2 --- /dev/null +++ b/contracts/token/ERC20RebasablePermit.sol @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +// import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; + +// import {SignatureUtils} from "../common/lib/SignatureUtils.sol"; +// import {IEIP712ERC20Rebasable} from "../lib/IEIP712ERC20Rebasable.sol"; + +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; +import {ERC20Rebasable} from "./ERC20Rebasable.sol"; +import {EIP712} from "@openzeppelin/contracts-v4.9/utils/cryptography/EIP712.sol"; +import {SignatureChecker} from "../lib/SignatureChecker.sol"; + + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC2612 { + /** + * @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; + + /** + * @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); + + /** + * @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); +} + + +contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { + using UnstructuredStorage for bytes32; + + /** + * @dev Nonces for ERC-2612 (Permit) + */ + mapping(address => uint256) internal noncesByAddress; + + /** + * @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; + + // TODO: outline structured storage used because at least EIP712 uses it + // TODO: use custom errors + + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param decimals_ The decimals places of the token + /// @param wrappedToken_ 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_, + uint8 decimals_, + address wrappedToken_, + address tokenRateOracle_, + address bridge_ + ) + ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) + EIP712("Liquid staked Ether 2.0", "1") + { + } + + /** + * @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.isValidSignatureNow(_owner, hash, _v, _r, _s)) { + revert ErrorInvalidSignature(); + } + _approve(_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; + } + + error ErrorInvalidSignature(); + error ErrorDeadlineExpired(); +} diff --git a/package.json b/package.json index 820a38ee..456c3985 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", @@ -74,6 +75,7 @@ "@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/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts new file mode 100644 index 00000000..4ec18250 --- /dev/null +++ b/test/token/ERC20Permit.unit.test.ts @@ -0,0 +1,214 @@ +import hre from "hardhat"; +import { assert } from "chai"; +import { unit } from "../../utils/testing"; +import { wei } from "../../utils/wei"; +import { makeDomainSeparator, signPermit } from "../../utils/testing/permit-helpers"; + +import { + ERC20Bridged__factory, + TokenRateOracle__factory, + OssifiableProxy__factory, + ERC20RebasablePermit__factory, + ERC1271PermitSignerMock__factory, +} from "../../typechain"; +import { BigNumber } from "ethers"; + + +const TOKEN_NAME = 'Liquid staked Ether 2.0' +const TOKEN_VERSION = '1' + +// 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', + }, +] + +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 } +} + +// const signPermit = async (owner, spender, value, nonce, domainSeparator, deadline, acct) => { +// const digest = calculatePermitDigest(owner, spender, value, nonce, domainSeparator, deadline) +// return await sign(digest, acct) +// } + + +unit("ERC20Permit", ctxFactory) + + .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { + const { rebasableProxied, wrappedToken } = ctx.contracts; + assert.equal(await rebasableProxied.WRAPPED_TOKEN(), wrappedToken.address) + }) + + .test('eip712Domain() is correct', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const [ fields, name, version, chainId, verifyingContract, salt, extensions ] = await token.eip712Domain() + + assert.equal(name, TOKEN_NAME) + assert.equal(version, TOKEN_VERSION) + assert.isDefined(hre.network.config.chainId) + assert.equal(chainId.toNumber(), hre.network.config.chainId as number) + assert.equal(verifyingContract, token.address) + + const domainSeparator = makeDomainSeparator(TOKEN_NAME, TOKEN_VERSION, chainId, token.address) + assert.equal(makeDomainSeparator(name, version, chainId, verifyingContract), domainSeparator) + }) + + .test('DOMAIN_SEPARATOR() is correct', async (ctx) => { + const token = ctx.contracts.rebasableProxied + const [ fields, name, version, chainId, verifyingContract, salt, extensions ] = await token.eip712Domain() + + assert.equal(name, TOKEN_NAME) + assert.equal(version, TOKEN_VERSION) + assert.isDefined(hre.network.config.chainId) + assert.equal(chainId.toNumber(), hre.network.config.chainId as number) + assert.equal(verifyingContract, token.address) + + const domainSeparator = makeDomainSeparator(TOKEN_NAME, TOKEN_VERSION, chainId, 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 + const charlieSigner = hre.ethers.provider.getSigner(charlie.address) + // const bobSigner = hre.ethers.provider.getSigner(BOB.address) + + const domainSeparator = makeDomainSeparator(TOKEN_NAME, TOKEN_VERSION, hre.network.config.chainId as number, token.address) + let { v, r, s } = await signPermit(owner, spender.address, value, nonce, deadline, 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(), domainSeparator) + + // a third-party, Charlie (not Alice) submits the permit + // TODO: handle unpredictable gas limit somehow better than setting it a random constant + const tx = await token.connect(charlieSigner) + .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, spender, value ]) + assert.equalBN(await token.nonces(owner.address), BigNumber.from(1)) + + // increment nonce + nonce = 1 + value = 4e5 + ;({ v, r, s } = await signPermit(owner, spender.address, value, nonce, deadline, domainSeparator)) + + // submit the permit + const tx2 = await token.connect(charlieSigner).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, BigNumber.from(value) ] ) + assert.equalBN(await token.nonces(owner.address), BigNumber.from(2)) + }) + + .run(); + +async function ctxFactory() { + // const name = "StETH Test Token"; + const name = TOKEN_NAME; + 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 [ + deployer, + owner, + recipient, + spender, + holder, + stranger, + user1, + user2, + ] = await hre.ethers.getSigners(); + + const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( + "WsETH Test Token", + "WsETH", + decimalsToSet, + owner.address + ); + const tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( + owner.address, + 86400 + ); + const rebasableTokenImpl = await new ERC20RebasablePermit__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 zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); + + const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( + rebasableTokenImpl.address, + deployer.address, + ERC20RebasablePermit__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const rebasableProxied = ERC20RebasablePermit__factory.connect( + l2TokensProxy.address, + holder + ); + + await tokenRateOracle.connect(owner).updateRate(rate, 1000); + await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); + // const { alice, bob } = await getAccountsEOA(); + const { alice, bob } = await getAccountsEIP1271(); + + const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + return { + accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, + constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate }, + contracts: { rebasableProxied, wrappedToken, tokenRateOracle }, + permitParams: { + owner: alice, + spender: bob, + value: 6e6, + nonce: 0, + deadline: MAX_UINT256, + } + }; +} diff --git a/utils/testing/permit-helpers.ts b/utils/testing/permit-helpers.ts new file mode 100644 index 00000000..d7151a30 --- /dev/null +++ b/utils/testing/permit-helpers.ts @@ -0,0 +1,98 @@ +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)' +) +console.log({ PERMIT_TYPE_HASH }) + +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) + console.log({ sig }) + return { v: sig.v, r: sig.r, s: sig.s } +} + + +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: ExternallyOwnedAccount | Eip1271Contract, spender: string, value: number, nonce: number, deadline: string, domainSeparator: string) { + const digest = calculatePermitDigest(owner.address, spender, value, nonce, deadline, domainSeparator) + if (owner.hasOwnProperty('sign')) { + return await signEIP1271(digest, owner as Eip1271Contract); + } else { + return await signEOA(digest, owner as ExternallyOwnedAccount); + } +} + +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] + ) +} + +function calculateEIP712Digest(domainSeparator: string, typeHash: string, types: string[], parameters: unknown[]) { + return streccak( + '0x1901' + + strip0x(domainSeparator) + + strip0x(keccak256(defaultAbiCoder.encode(['bytes32', ...types], [typeHash, ...parameters]))) + ) +} + +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') +} From 6a46704f8955446ddbf4e815c62870ca22bb5ead Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 29 Mar 2024 10:47:53 +0100 Subject: [PATCH 42/61] update optimism sdk version to fix integration tests --- package-lock.json | 19 +++++++++---------- package.json | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) 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..359469f0 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "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", From b9d5e5667323c22815998825535fa4de591457ad Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 29 Mar 2024 10:53:11 +0100 Subject: [PATCH 43/61] update addresses to run e2e tests --- utils/lido.ts | 6 +++--- utils/testing/e2e.ts | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) 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/testing/e2e.ts b/utils/testing/e2e.ts index 010596e7..b089a24a 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", + l1ERC20TokenBridge: "0x4Abf633d9c0F4aEebB4C2E3213c7aa1b8505D332", + aragonVoting: "0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A", + tokenManager: "0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66", + agent: "0x32A0E5828B62AAb932362a4816ae03b860b65e83", + l1CrossDomainMessenger: "0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef", }, l2: { - l2Token: "0x4c2ECf847C89d5De3187F1b0081E4dcdBe063C68", - l2ERC20TokenBridge: "0x0A5c6AB7B41E066b5C40907dd06063703be21B19", - govBridgeExecutor: "0x2365F00fFD70958EC2c20B601D501e4b75798D93", + l2Token: "0x24B47cd3A74f1799b32B2de11073764Cb1bb318B", + l2ERC20TokenBridge: "0xdBA2760246f315203F8B716b3a7590F0FFdc704a", + govBridgeExecutor: "0xf695357C66bA514150Da95b189acb37b46DDe602", }, }; From a3f4544e536adb93a9f63a517edaf0ba2c37748e Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 29 Mar 2024 10:54:20 +0100 Subject: [PATCH 44/61] fix integration test --- .../bridging-rebasable.integration.test.ts | 71 ------------------- 1 file changed, 71 deletions(-) diff --git a/test/optimism/bridging-rebasable.integration.test.ts b/test/optimism/bridging-rebasable.integration.test.ts index 3d5f62a8..68cbabee 100644 --- a/test/optimism/bridging-rebasable.integration.test.ts +++ b/test/optimism/bridging-rebasable.integration.test.ts @@ -109,75 +109,6 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); }) - .step("Push token rate to L2", async (ctx) => { - const { - l1Token, - l1TokenRebasable, - l1LidoTokensBridge, - l2TokenRebasable, - l1CrossDomainMessenger, - l2ERC20TokenBridge, - l1Provider - } = ctx; - - const { l1Stranger } = ctx.accounts; - - const tokenHolderStrangerBalanceBefore = await l1TokenRebasable.balanceOf( - l1Stranger.address - ); - - const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( - l1LidoTokensBridge.address - ); - - const tx = await l1LidoTokensBridge - .connect(l1Stranger) - .pushTokenRate(200_000); - - const dataToSend = await packedTokenRateAndTimestamp(l1Provider, l1Token); - - await assert.emits(l1LidoTokensBridge, tx, "ERC20DepositInitiated", [ - l1TokenRebasable.address, - l2TokenRebasable.address, - l1Stranger.address, - l2ERC20TokenBridge.address, - 0, - dataToSend, - ]); - - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( - "finalizeDeposit", - [ - l1TokenRebasable.address, - l2TokenRebasable.address, - l1Stranger.address, - l2ERC20TokenBridge.address, - 0, - dataToSend, - ] - ); - - const messageNonce = await l1CrossDomainMessenger.messageNonce(); - - await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, - l1LidoTokensBridge.address, - l2DepositCalldata, - messageNonce, - 200_000, - ]); - - assert.equalBN( - await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore - ); - - assert.equalBN( - await l1TokenRebasable.balanceOf(l1Stranger.address), - tokenHolderStrangerBalanceBefore - ); - }) - .step("L1 -> L2 deposit zero tokens via depositERC20() method", async (ctx) => { const { l1Token, @@ -889,8 +820,6 @@ async function ctxFactory() { l2Provider ); - await contracts.l1LidoTokensBridge.connect(l1ERC20TokenBridgeAdmin).pushTokenRate(1000000); - return { l1Provider, l2Provider, From e2c29e0f4fa2e5ef8cc121288fe1d55207fcfce3 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 1 Apr 2024 13:03:02 +0200 Subject: [PATCH 45/61] deployment scripts --- scripts/optimism/deploy-bridge.ts | 7 +- scripts/optimism/deploy-oracle.ts | 61 ++++ .../optimism.integration.test.ts | 54 +-- test/optimism/L2ERC20TokenBridge.unit.test.ts | 12 +- utils/deployment.ts | 2 + ... => deploymentBridgesAndRebasableToken.ts} | 99 ++++-- .../deploymentBridgesBothTokensAndOracle.ts | 318 ++++++++++++++++++ utils/optimism/deploymentOracle.ts | 103 ++++++ utils/optimism/testing.ts | 25 +- utils/optimism/upgradeOracle.ts | 79 +++++ 10 files changed, 686 insertions(+), 74 deletions(-) create mode 100644 scripts/optimism/deploy-oracle.ts rename utils/optimism/{deployment.ts => deploymentBridgesAndRebasableToken.ts} (70%) create mode 100644 utils/optimism/deploymentBridgesBothTokensAndOracle.ts create mode 100644 utils/optimism/deploymentOracle.ts create mode 100644 utils/optimism/upgradeOracle.ts diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 7680befb..3453071c 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -26,12 +26,14 @@ async function main() { .erc20TokenBridgeDeployScript( deploymentConfig.token, deploymentConfig.stETHToken, + deploymentConfig.l2TokenRateOracle, { deployer: ethDeployer, admins: { proxy: deploymentConfig.l1.proxyAdmin, - bridge: ethDeployer.address, + bridge: ethDeployer.address }, + contractsShift: 0 }, { deployer: optDeployer, @@ -39,6 +41,7 @@ async function main() { proxy: deploymentConfig.l2.proxyAdmin, bridge: optDeployer.address, }, + contractsShift: 0 } ); @@ -63,7 +66,7 @@ async function main() { { logger: console } ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 6; + const l2ERC20TokenBridgeProxyDeployStepIndex = 5; const l2BridgingManagement = new BridgingManagement( l2DeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), optDeployer, diff --git a/scripts/optimism/deploy-oracle.ts b/scripts/optimism/deploy-oracle.ts new file mode 100644 index 00000000..d5ef78d1 --- /dev/null +++ b/scripts/optimism/deploy-oracle.ts @@ -0,0 +1,61 @@ +import env from "../../utils/env"; +import prompt from "../../utils/prompt"; +import network from "../../utils/network"; +import optimism from "../../utils/optimism"; +import deploymentOracle from "../../utils/deployment"; + +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 = deploymentOracle.loadMultiChainDeploymentConfig(); + + const [l1DeployScript, l2DeployScript] = await optimism + .deploymentOracle(networkName, { logger: console }) + .oracleDeployScript( + deploymentConfig.token, + { + deployer: ethDeployer, + admins: { + proxy: deploymentConfig.l1.proxyAdmin, + bridge: ethDeployer.address, + }, + }, + { + deployer: optDeployer, + admins: { + proxy: deploymentConfig.l2.proxyAdmin, + bridge: optDeployer.address, + }, + } + ); + + await deploymentOracle.printMultiChainDeploymentConfig( + "Deploy Optimism Bridge", + 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/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index c4ad2883..77cac7f5 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -5,8 +5,6 @@ import { OssifiableProxy__factory, OptimismBridgeExecutor__factory, ERC20Bridged__factory, - ERC20Rebasable__factory, - TokenRateOracle__factory, ERC20WrapperStub__factory, } from "../../typechain"; import { wei } from "../../utils/wei"; @@ -17,6 +15,7 @@ import { BridgingManagerRole } from "../../utils/bridging-management"; import env from "../../utils/env"; import network from "../../utils/network"; import { getBridgeExecutorParams } from "../../utils/bridge-executor"; +import deploymentAll, { L1DeployAllScript, L2DeployAllScript } from "../../utils/optimism/deploymentBridgesBothTokensAndOracle"; scenario("Optimism :: Bridge Executor integration test", ctxFactory) .step("Activate L2 bridge", async (ctx) => { @@ -238,39 +237,42 @@ async function ctxFactory() { const l1EthGovExecutorAddress = await govBridgeExecutor.getEthereumGovernanceExecutor(); - const [, l2DeployScript] = await optimism - .deployment(networkName) - .erc20TokenBridgeDeployScript( - l1Token.address, - l1TokenRebasable.address, - { - deployer: l1Deployer, - admins: { - proxy: l1Deployer.address, - bridge: l1Deployer.address - }, - }, - { - deployer: l2Deployer, - admins: { - proxy: govBridgeExecutor.address, - bridge: govBridgeExecutor.address, - }, - } - ); - await l2DeployScript.run(); + const [, optDeployScript] = await deploymentAll( + networkName + ).erc20TokenBridgeDeployScript( + l1Token.address, + l1TokenRebasable.address, + { + deployer: l1Deployer, + admins: { + proxy: l1Deployer.address, + bridge: l1Deployer.address + }, + contractsShift: 0 + }, + { + deployer: l2Deployer, + admins: { + proxy: govBridgeExecutor.address, + bridge: govBridgeExecutor.address, + }, + contractsShift: 0 + } + ); + + await optDeployScript.run(); const l2Token = ERC20Bridged__factory.connect( - l2DeployScript.getContractAddress(1), + optDeployScript.tokenProxyAddress, l2Deployer ); const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( - l2DeployScript.getContractAddress(3), + optDeployScript.tokenBridgeProxyAddress, l2Deployer ); const l2ERC20TokenBridgeProxy = OssifiableProxy__factory.connect( - l2DeployScript.getContractAddress(3), + optDeployScript.tokenBridgeProxyAddress, l2Deployer ); diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index fc0a54db..1fd77dd3 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -686,12 +686,12 @@ async function ctxFactory() { const emptyContractEOA = await testing.impersonate(emptyContract.address); const [ - l1TokenRebasableAddress, - l1TokenNonRebasableAddress, - l2TokenNonRebasableAddress, - tokenRateOracleAddress, - l2TokenRebasableAddress, - l2TokenBridgeImplAddress, + , + , + , + , + , + , l2TokenBridgeProxyAddress ] = await predictAddresses(deployer, 7); diff --git a/utils/deployment.ts b/utils/deployment.ts index f18aa609..7612bdf7 100644 --- a/utils/deployment.ts +++ b/utils/deployment.ts @@ -12,6 +12,7 @@ interface ChainDeploymentConfig extends BridgingManagerSetupConfig { interface MultiChainDeploymentConfig { token: string; stETHToken: string; + l2TokenRateOracle: string; l1: ChainDeploymentConfig; l2: ChainDeploymentConfig; } @@ -20,6 +21,7 @@ export function loadMultiChainDeploymentConfig(): MultiChainDeploymentConfig { return { token: env.address("TOKEN"), stETHToken: env.address("STETH_TOKEN"), + l2TokenRateOracle: env.address("TOKEN_RATE_ORACLE"), l1: { proxyAdmin: env.address("L1_PROXY_ADMIN"), bridgeAdmin: env.address("L1_BRIDGE_ADMIN"), diff --git a/utils/optimism/deployment.ts b/utils/optimism/deploymentBridgesAndRebasableToken.ts similarity index 70% rename from utils/optimism/deployment.ts rename to utils/optimism/deploymentBridgesAndRebasableToken.ts index 6a64d757..c8fbd9d4 100644 --- a/utils/optimism/deployment.ts +++ b/utils/optimism/deploymentBridgesAndRebasableToken.ts @@ -1,23 +1,22 @@ import { assert } from "chai"; import { Overrides, Wallet } from "ethers"; -import { - ERC20Bridged__factory, - ERC20Rebasable__factory, - IERC20Metadata__factory, - L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, - OssifiableProxy__factory, - TokenRateOracle__factory, -} from "../../typechain"; - import addresses from "./addresses"; import { CommonOptions } from "./types"; import network, { NetworkName } from "../network"; import { DeployScript, Logger } from "../deployment/DeployScript"; +import { + ERC20Bridged__factory, + ERC20Rebasable__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20TokenBridge__factory, + OssifiableProxy__factory, + } from "../../typechain"; interface OptL1DeployScriptParams { deployer: Wallet; admins: { proxy: string; bridge: string }; + contractsShift: number; } interface OptL2DeployScriptParams extends OptL1DeployScriptParams { @@ -30,6 +29,54 @@ interface OptDeploymentOptions extends CommonOptions { 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 = {} @@ -39,6 +86,7 @@ export default function deployment( async erc20TokenBridgeDeployScript( l1Token: string, l1TokenRebasable: string, + l2TokenRateOracle: string, l1Params: OptL1DeployScriptParams, l2Params: OptL2DeployScriptParams, ) { @@ -46,20 +94,21 @@ export default function deployment( const [ expectedL1TokenBridgeImplAddress, expectedL1TokenBridgeProxyAddress, - ] = await network.predictAddresses(l1Params.deployer, 2); + ] = await network.predictAddresses(l1Params.deployer, l1Params.contractsShift + 2); const [ - expectedL2TokenRateOracleImplAddress, expectedL2TokenImplAddress, expectedL2TokenProxyAddress, expectedL2TokenRebasableImplAddress, expectedL2TokenRebasableProxyAddress, expectedL2TokenBridgeImplAddress, expectedL2TokenBridgeProxyAddress, - ] = await network.predictAddresses(l2Params.deployer, 7); + ] = await network.predictAddresses(l2Params.deployer, l2Params.contractsShift + 6); - const l1DeployScript = new DeployScript( + const l1DeployScript = new BridgeL1DeployScript( l1Params.deployer, + expectedL1TokenBridgeImplAddress, + expectedL1TokenBridgeProxyAddress, options?.logger ) .addStep({ @@ -108,21 +157,16 @@ export default function deployment( l2Params.l2TokenRebasable?.symbol ?? l1TokenRebasableInfo.symbol(), ]); - const l2DeployScript = new DeployScript( + const l2DeployScript = new BridgeL2DeployScript( l2Params.deployer, + expectedL2TokenImplAddress, + expectedL2TokenProxyAddress, + expectedL2TokenRebasableImplAddress, + expectedL2TokenRebasableProxyAddress, + expectedL2TokenBridgeImplAddress, + expectedL2TokenBridgeProxyAddress, options?.logger ) - .addStep({ - factory: TokenRateOracle__factory, - args: [ - expectedL2TokenBridgeProxyAddress, - 86400, - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL2TokenRateOracleImplAddress), - }) - .addStep({ factory: ERC20Bridged__factory, args: [ @@ -156,7 +200,7 @@ export default function deployment( l2TokenRebasableSymbol, decimals, expectedL2TokenProxyAddress, - expectedL2TokenRateOracleImplAddress, + l2TokenRateOracle, expectedL2TokenBridgeProxyAddress, options?.overrides, ], @@ -177,7 +221,6 @@ export default function deployment( afterDeploy: (c) => assert.equal(c.address, expectedL2TokenRebasableProxyAddress), }) - .addStep({ factory: L2ERC20TokenBridge__factory, args: [ diff --git a/utils/optimism/deploymentBridgesBothTokensAndOracle.ts b/utils/optimism/deploymentBridgesBothTokensAndOracle.ts new file mode 100644 index 00000000..f84be05a --- /dev/null +++ b/utils/optimism/deploymentBridgesBothTokensAndOracle.ts @@ -0,0 +1,318 @@ +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, + ERC20Rebasable__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20TokenBridge__factory, + OssifiableProxy__factory, + TokenRateOracle__factory, + TokenRateNotifier__factory, + OpStackTokenRatePusher__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 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 and Oracle +export default function deploymentAll( + networkName: NetworkName, + options: OptDeploymentOptions = {} +) { + const optAddresses = addresses(networkName, options); + return { + async erc20TokenBridgeDeployScript( + l1Token: string, + l1TokenRebasable: string, + l1Params: OptL1DeployScriptParams, + l2Params: OptL2DeployScriptParams, + ) { + + 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: [ + 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: ERC20Rebasable__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, + ERC20Rebasable__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ + factory: L2ERC20TokenBridge__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, + L2ERC20TokenBridge__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, l2DeployScript]; + }, + }; +} diff --git a/utils/optimism/deploymentOracle.ts b/utils/optimism/deploymentOracle.ts new file mode 100644 index 00000000..da5f25a4 --- /dev/null +++ b/utils/optimism/deploymentOracle.ts @@ -0,0 +1,103 @@ +import { assert } from "chai"; +import { Overrides, Wallet } from "ethers"; +import { ethers } from "hardhat"; +import addresses from "./addresses"; +import { CommonOptions } 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 { + deployer: Wallet; + admins: { proxy: string; bridge: string }; +} + +interface OptDeploymentOptions extends CommonOptions { + logger?: Logger; + overrides?: Overrides; +} + +export default function deploymentOracle( + networkName: NetworkName, + options: OptDeploymentOptions = {} + ) { + const optAddresses = addresses(networkName, options); + return { + async oracleDeployScript( + l1Token: string, + l1Params: OptDeployScriptParams, + l2Params: OptDeployScriptParams, + ) { + + const [ + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + ] = await network.predictAddresses(l1Params.deployer, 2); + + const [ + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress + ] = await network.predictAddresses(l2Params.deployer, 2); + + const l1DeployScript = new DeployScript( + l1Params.deployer, + options?.logger + ) + .addStep({ + factory: TokenRateNotifier__factory, + args: [ + 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 l2DeployScript = new DeployScript( + l2Params.deployer, + options?.logger + ) + .addStep({ + factory: TokenRateOracle__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + ethers.constants.AddressZero, + 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, l2DeployScript]; + }, + }; + } diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 5f36907c..65be4e31 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -1,4 +1,4 @@ -import { BigNumber, Signer } from "ethers"; +import { Signer } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { @@ -18,7 +18,7 @@ import { } from "../../typechain"; import addresses from "./addresses"; import contracts from "./contracts"; -import deployment from "./deployment"; +import deploymentAll, { L1DeployAllScript, L2DeployAllScript } from "./deploymentBridgesBothTokensAndOracle"; import testingUtils from "../testing"; import { BridgingManagement } from "../bridging-management"; import network, { NetworkName, SignerOrProvider } from "../network"; @@ -197,7 +197,7 @@ async function deployTestBridge( "TT" ); - const [ethDeployScript, optDeployScript] = await deployment( + const [ethDeployScript, optDeployScript] = await deploymentAll( networkName ).erc20TokenBridgeDeployScript( l1Token.address, @@ -205,25 +205,26 @@ async function deployTestBridge( { 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 l1LidoTokensBridgeProxyDeployStepIndex = 1; + const l1BridgingManagement = new BridgingManagement( - ethDeployScript.getContractAddress(l1LidoTokensBridgeProxyDeployStepIndex), + ethDeployScript.bridgeProxyAddress, ethDeployer ); - const l2ERC20TokenBridgeProxyDeployStepIndex = 6; const l2BridgingManagement = new BridgingManagement( - optDeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + optDeployScript.tokenBridgeProxyAddress, optDeployer ); @@ -244,11 +245,11 @@ async function deployTestBridge( l1TokenRebasable: l1TokenRebasable.connect(ethProvider), ...connectBridgeContracts( { - tokenRateOracle: optDeployScript.getContractAddress(0), - l2Token: optDeployScript.getContractAddress(2), - l2TokenRebasable: optDeployScript.getContractAddress(4), - l1LidoTokensBridge: ethDeployScript.getContractAddress(1), - l2ERC20TokenBridge: optDeployScript.getContractAddress(6) + tokenRateOracle: optDeployScript.tokenRateOracleProxyAddress, + l2Token: optDeployScript.tokenProxyAddress, + l2TokenRebasable: optDeployScript.tokenRebasableProxyAddress, + l1LidoTokensBridge: ethDeployScript.bridgeProxyAddress, + l2ERC20TokenBridge: optDeployScript.tokenBridgeProxyAddress }, ethProvider, optProvider diff --git a/utils/optimism/upgradeOracle.ts b/utils/optimism/upgradeOracle.ts new file mode 100644 index 00000000..17fd0b26 --- /dev/null +++ b/utils/optimism/upgradeOracle.ts @@ -0,0 +1,79 @@ +import { + OssifiableProxy__factory, + OptimismBridgeExecutor__factory +} from "../../typechain"; + +import network, { NetworkName } from "../network"; +import testingUtils from "../testing"; +import contracts from "./contracts"; +import testing from "../../utils/testing"; +import optimism from "../../utils/optimism"; +import { getBridgeExecutorParams } from "../../utils/bridge-executor"; + +export async function upgradeOracle( + networkName: NetworkName, + oracleProxyAddress: string, + newOracleAddress: string + ) { + const ethOptNetworks = network.multichain(["eth", "opt"], networkName); + const [ + ethProvider, + optProvider + ] = ethOptNetworks.getProviders({ forking: true }); + const ethDeployer = testing.accounts.deployer(ethProvider); + const optDeployer = testing.accounts.deployer(optProvider); + + + const optContracts = contracts(networkName, { forking: true }); + const l1CrossDomainMessengerAliased = await testingUtils.impersonate( + testingUtils.accounts.applyL1ToL2Alias(optContracts.L1CrossDomainMessenger.address), + optProvider + ); + const l2CrossDomainMessenger = await optContracts.L2CrossDomainMessenger.connect( + l1CrossDomainMessengerAliased + ); + + + 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(), + optProvider + ) + : await new OptimismBridgeExecutor__factory(optDeployer).deploy( + optAddresses.L2CrossDomainMessenger, + ethDeployer.address, + ...getBridgeExecutorParams(), + optDeployer.address + ); + + + const l1EthGovExecutorAddress = await govBridgeExecutor.getEthereumGovernanceExecutor(); + const bridgeExecutor = govBridgeExecutor.connect(optDeployer); + const l2OracleProxy = OssifiableProxy__factory.connect( + oracleProxyAddress, + optDeployer + ); + + await l2CrossDomainMessenger.relayMessage( + 0, + l1EthGovExecutorAddress, + bridgeExecutor.address, + 0, + 300_000, + bridgeExecutor.interface.encodeFunctionData("queue", [ + [oracleProxyAddress], + [0], + ["proxy__upgradeTo(address)"], + [ + "0x" + + l2OracleProxy.interface + .encodeFunctionData("proxy__upgradeTo", [newOracleAddress]) + .substring(10), + ], + [false], + ]), + { gasLimit: 5_000_000 } + ); +} From 0d2df1d450e304526d89ffe8c5e5b27c79b83252 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 1 Apr 2024 23:44:22 +0200 Subject: [PATCH 46/61] add token rate oracle integration tests --- .../optimism.integration.test.ts | 51 +++-- .../pushingTokenRate.integration.test.ts | 212 ++++++++++++++++++ ...othTokensAndOracle.ts => deploymentAll.ts} | 4 +- .../deploymentBridgesAndRebasableToken.ts | 4 +- utils/optimism/deploymentOracle.ts | 46 +++- utils/optimism/index.ts | 5 +- utils/optimism/testing.ts | 3 +- 7 files changed, 288 insertions(+), 37 deletions(-) create mode 100644 test/optimism/pushingTokenRate.integration.test.ts rename utils/optimism/{deploymentBridgesBothTokensAndOracle.ts => deploymentAll.ts} (98%) diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 77cac7f5..1cf1db45 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -15,7 +15,7 @@ import { BridgingManagerRole } from "../../utils/bridging-management"; import env from "../../utils/env"; import network from "../../utils/network"; import { getBridgeExecutorParams } from "../../utils/bridge-executor"; -import deploymentAll, { L1DeployAllScript, L2DeployAllScript } from "../../utils/optimism/deploymentBridgesBothTokensAndOracle"; +import deploymentAll from "../../utils/optimism/deploymentAll"; scenario("Optimism :: Bridge Executor integration test", ctxFactory) .step("Activate L2 bridge", async (ctx) => { @@ -202,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); @@ -221,6 +220,7 @@ async function ctxFactory() { ); const optAddresses = optimism.addresses(networkName); + const testingOnDeployedContracts = testing.env.USE_DEPLOYED_CONTRACTS(false); const govBridgeExecutor = testingOnDeployedContracts ? OptimismBridgeExecutor__factory.connect( @@ -237,31 +237,30 @@ async function ctxFactory() { const l1EthGovExecutorAddress = await govBridgeExecutor.getEthereumGovernanceExecutor(); + const [, optDeployScript] = await deploymentAll( + networkName + ).erc20TokenBridgeDeployScript( + l1Token.address, + l1TokenRebasable.address, + { + deployer: l1Deployer, + admins: { + proxy: l1Deployer.address, + bridge: l1Deployer.address + }, + contractsShift: 0 + }, + { + deployer: l2Deployer, + admins: { + proxy: govBridgeExecutor.address, + bridge: govBridgeExecutor.address, + }, + contractsShift: 0 + } + ); - const [, optDeployScript] = await deploymentAll( - networkName - ).erc20TokenBridgeDeployScript( - l1Token.address, - l1TokenRebasable.address, - { - deployer: l1Deployer, - admins: { - proxy: l1Deployer.address, - bridge: l1Deployer.address - }, - contractsShift: 0 - }, - { - deployer: l2Deployer, - admins: { - proxy: govBridgeExecutor.address, - bridge: govBridgeExecutor.address, - }, - contractsShift: 0 - } - ); - - await optDeployScript.run(); + await optDeployScript.run(); const l2Token = ERC20Bridged__factory.connect( optDeployScript.tokenProxyAddress, diff --git a/test/optimism/pushingTokenRate.integration.test.ts b/test/optimism/pushingTokenRate.integration.test.ts new file mode 100644 index 00000000..50f8211f --- /dev/null +++ b/test/optimism/pushingTokenRate.integration.test.ts @@ -0,0 +1,212 @@ +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 { + 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 [ + answer1, + answer2, + answer3, + answer4, + answer5 + ] = await tokenRateOracle.latestRoundData(); + + assert.equalBN(answer2, tokenRate); + assert.equalBN(answer4, 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, + { + deployer: l1Deployer, + admins: { + proxy: l1Deployer.address, + bridge: l1Deployer.address + }, + }, + { + deployer: l2Deployer, + admins: { + proxy: govBridgeExecutor.address, + bridge: govBridgeExecutor.address, + }, + } + ); + + 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/utils/optimism/deploymentBridgesBothTokensAndOracle.ts b/utils/optimism/deploymentAll.ts similarity index 98% rename from utils/optimism/deploymentBridgesBothTokensAndOracle.ts rename to utils/optimism/deploymentAll.ts index f84be05a..993b7a2c 100644 --- a/utils/optimism/deploymentBridgesBothTokensAndOracle.ts +++ b/utils/optimism/deploymentAll.ts @@ -102,7 +102,7 @@ export default function deploymentAll( l1TokenRebasable: string, l1Params: OptL1DeployScriptParams, l2Params: OptL2DeployScriptParams, - ) { + ): Promise<[L1DeployAllScript, L2DeployAllScript]> { const [ expectedL1TokenBridgeImplAddress, @@ -312,7 +312,7 @@ export default function deploymentAll( assert.equal(c.address, expectedL2TokenRateOracleProxyAddress), }); - return [l1DeployScript, l2DeployScript]; + return [l1DeployScript as L1DeployAllScript, l2DeployScript as L2DeployAllScript]; }, }; } diff --git a/utils/optimism/deploymentBridgesAndRebasableToken.ts b/utils/optimism/deploymentBridgesAndRebasableToken.ts index c8fbd9d4..1035b7a5 100644 --- a/utils/optimism/deploymentBridgesAndRebasableToken.ts +++ b/utils/optimism/deploymentBridgesAndRebasableToken.ts @@ -89,7 +89,7 @@ export default function deployment( l2TokenRateOracle: string, l1Params: OptL1DeployScriptParams, l2Params: OptL2DeployScriptParams, - ) { + ): Promise<[BridgeL1DeployScript, BridgeL2DeployScript]> { const [ expectedL1TokenBridgeImplAddress, @@ -248,7 +248,7 @@ export default function deployment( ], }); - return [l1DeployScript, l2DeployScript]; + return [l1DeployScript as BridgeL1DeployScript, l2DeployScript as BridgeL2DeployScript]; }, }; } diff --git a/utils/optimism/deploymentOracle.ts b/utils/optimism/deploymentOracle.ts index da5f25a4..ec28c820 100644 --- a/utils/optimism/deploymentOracle.ts +++ b/utils/optimism/deploymentOracle.ts @@ -22,6 +22,40 @@ interface OptDeploymentOptions extends CommonOptions { overrides?: Overrides; } +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 = {} @@ -32,7 +66,7 @@ export default function deploymentOracle( l1Token: string, l1Params: OptDeployScriptParams, l2Params: OptDeployScriptParams, - ) { + ): Promise<[OracleL1DeployScript, OracleL2DeployScript]> { const [ expectedL1TokenRateNotifierImplAddress, @@ -44,8 +78,10 @@ export default function deploymentOracle( expectedL2TokenRateOracleProxyAddress ] = await network.predictAddresses(l2Params.deployer, 2); - const l1DeployScript = new DeployScript( + const l1DeployScript = new OracleL1DeployScript( l1Params.deployer, + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, options?.logger ) .addStep({ @@ -69,8 +105,10 @@ export default function deploymentOracle( assert.equal(c.address, expectedL1OpStackTokenRatePusherImplAddress), }); - const l2DeployScript = new DeployScript( + const l2DeployScript = new OracleL2DeployScript( l2Params.deployer, + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress, options?.logger ) .addStep({ @@ -97,7 +135,7 @@ export default function deploymentOracle( assert.equal(c.address, expectedL2TokenRateOracleProxyAddress), }); - return [l1DeployScript, l2DeployScript]; + 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 65be4e31..a0cd5148 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -18,7 +18,7 @@ import { } from "../../typechain"; import addresses from "./addresses"; import contracts from "./contracts"; -import deploymentAll, { L1DeployAllScript, L2DeployAllScript } from "./deploymentBridgesBothTokensAndOracle"; +import deploymentAll from "./deploymentAll"; import testingUtils from "../testing"; import { BridgingManagement } from "../bridging-management"; import network, { NetworkName, SignerOrProvider } from "../network"; @@ -217,7 +217,6 @@ async function deployTestBridge( await ethDeployScript.run(); await optDeployScript.run(); - const l1BridgingManagement = new BridgingManagement( ethDeployScript.bridgeProxyAddress, ethDeployer From 8dc246e23fe977ca786afdc8f7798da1fc4b95e6 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Tue, 2 Apr 2024 10:33:09 +0300 Subject: [PATCH 47/61] fix happy path test for permit for rebasable --- contracts/token/ERC20RebasablePermit.sol | 3 +- test/token/ERC20Permit.unit.test.ts | 65 ++++++++++++------------ utils/testing/permit-helpers.ts | 14 ++--- utils/testing/unit.ts | 2 +- 4 files changed, 41 insertions(+), 43 deletions(-) diff --git a/contracts/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasablePermit.sol index 5a7843e2..6321672d 100644 --- a/contracts/token/ERC20RebasablePermit.sol +++ b/contracts/token/ERC20RebasablePermit.sol @@ -85,13 +85,14 @@ contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { constructor( string memory name_, string memory symbol_, + // TODO: pass signing domain version uint8 decimals_, address wrappedToken_, address tokenRateOracle_, address bridge_ ) ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) - EIP712("Liquid staked Ether 2.0", "1") + EIP712(name_, "2") { } diff --git a/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts index 4ec18250..a19bf940 100644 --- a/test/token/ERC20Permit.unit.test.ts +++ b/test/token/ERC20Permit.unit.test.ts @@ -1,6 +1,6 @@ import hre from "hardhat"; import { assert } from "chai"; -import { unit } from "../../utils/testing"; +import { unit, UnitTest } from "../../utils/testing"; import { wei } from "../../utils/wei"; import { makeDomainSeparator, signPermit } from "../../utils/testing/permit-helpers"; @@ -14,8 +14,10 @@ import { import { BigNumber } from "ethers"; +type ContextType = Awaited>> + const TOKEN_NAME = 'Liquid staked Ether 2.0' -const TOKEN_VERSION = '1' +const SIGNING_DOMAIN_VERSION = '2' // derived from mnemonic: want believe mosquito cat design route voice cause gold benefit gospel bulk often attitude rural const ACCOUNTS_AND_KEYS = [ @@ -29,6 +31,10 @@ const ACCOUNTS_AND_KEYS = [ }, ] +function getChainId() { + return hre.network.config.chainId as number; +} + const getAccountsEOA = async () => { return { alice: ACCOUNTS_AND_KEYS[0], @@ -43,13 +49,9 @@ const getAccountsEIP1271 = async () => { return { alice, bob } } -// const signPermit = async (owner, spender, value, nonce, domainSeparator, deadline, acct) => { -// const digest = calculatePermitDigest(owner, spender, value, nonce, domainSeparator, deadline) -// return await sign(digest, acct) -// } - - -unit("ERC20Permit", ctxFactory) +function permitTestsSuit(unitInstance: UnitTest) +{ + unitInstance .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { const { rebasableProxied, wrappedToken } = ctx.contracts; @@ -58,29 +60,22 @@ unit("ERC20Permit", ctxFactory) .test('eip712Domain() is correct', async (ctx) => { const token = ctx.contracts.rebasableProxied - const [ fields, name, version, chainId, verifyingContract, salt, extensions ] = await token.eip712Domain() + const [ , name, version, chainId, verifyingContract, , ] = await token.eip712Domain() assert.equal(name, TOKEN_NAME) - assert.equal(version, TOKEN_VERSION) + assert.equal(version, SIGNING_DOMAIN_VERSION) assert.isDefined(hre.network.config.chainId) - assert.equal(chainId.toNumber(), hre.network.config.chainId as number) + assert.equal(chainId.toNumber(), getChainId()) assert.equal(verifyingContract, token.address) - const domainSeparator = makeDomainSeparator(TOKEN_NAME, TOKEN_VERSION, chainId, token.address) + const domainSeparator = makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, chainId, token.address) assert.equal(makeDomainSeparator(name, version, chainId, verifyingContract), domainSeparator) }) .test('DOMAIN_SEPARATOR() is correct', async (ctx) => { const token = ctx.contracts.rebasableProxied - const [ fields, name, version, chainId, verifyingContract, salt, extensions ] = await token.eip712Domain() - assert.equal(name, TOKEN_NAME) - assert.equal(version, TOKEN_VERSION) - assert.isDefined(hre.network.config.chainId) - assert.equal(chainId.toNumber(), hre.network.config.chainId as number) - assert.equal(verifyingContract, token.address) - - const domainSeparator = makeDomainSeparator(TOKEN_NAME, TOKEN_VERSION, chainId, token.address) + const domainSeparator = makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, getChainId(), token.address) assert.equal(await ctx.contracts.rebasableProxied.DOMAIN_SEPARATOR(), domainSeparator) }) @@ -94,10 +89,9 @@ unit("ERC20Permit", ctxFactory) let nonce = 0 const charlie = ctx.accounts.user2 const charlieSigner = hre.ethers.provider.getSigner(charlie.address) - // const bobSigner = hre.ethers.provider.getSigner(BOB.address) - const domainSeparator = makeDomainSeparator(TOKEN_NAME, TOKEN_VERSION, hre.network.config.chainId as number, token.address) - let { v, r, s } = await signPermit(owner, spender.address, value, nonce, deadline, domainSeparator) + const domainSeparator = makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, getChainId(), token.address) + let { v, r, s } = await signPermit(owner, spender.address, value, deadline, nonce, domainSeparator) // check that the allowance is initially zero assert.equalBN(await token.allowance(owner.address, spender.address), BigNumber.from(0)) @@ -107,33 +101,35 @@ unit("ERC20Permit", ctxFactory) assert.equal(await token.DOMAIN_SEPARATOR(), domainSeparator) // a third-party, Charlie (not Alice) submits the permit - // TODO: handle unpredictable gas limit somehow better than setting it a random constant + // TODO: handle unpredictable gas limit somehow better than setting it to a random constant const tx = await token.connect(charlieSigner) .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, spender, 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, spender.address, value, nonce, deadline, domainSeparator)) + ;({ v, r, s } = await signPermit(owner, spender.address, value, deadline, nonce, domainSeparator)) // submit the permit const tx2 = await token.connect(charlieSigner).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, 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)) }) .run(); +} -async function ctxFactory() { - // const name = "StETH Test Token"; +function ctxFactoryFactory(signingAccountsFuncFactory: typeof getAccountsEIP1271 | typeof getAccountsEOA) { + return async () => { const name = TOKEN_NAME; const symbol = "StETH"; const decimalsToSet = 18; @@ -160,7 +156,9 @@ async function ctxFactory() { 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 ERC20RebasablePermit__factory(deployer).deploy( @@ -195,8 +193,7 @@ async function ctxFactory() { await tokenRateOracle.connect(owner).updateRate(rate, 1000); await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); - // const { alice, bob } = await getAccountsEOA(); - const { alice, bob } = await getAccountsEIP1271(); + const { alice, bob } = await signingAccountsFuncFactory(); const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' return { @@ -211,4 +208,8 @@ async function ctxFactory() { deadline: MAX_UINT256, } }; + } } + +permitTestsSuit(unit("ERC20Permit with EIP1271 (contract) signing", ctxFactoryFactory(getAccountsEIP1271))); +permitTestsSuit(unit("ERC20Permit with ECDSA (EOA) signing", ctxFactoryFactory(getAccountsEOA))); diff --git a/utils/testing/permit-helpers.ts b/utils/testing/permit-helpers.ts index d7151a30..06bd5617 100644 --- a/utils/testing/permit-helpers.ts +++ b/utils/testing/permit-helpers.ts @@ -7,7 +7,6 @@ import { ecsign as ecSignBuf } from "ethereumjs-util"; const PERMIT_TYPE_HASH = streccak( 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' ) -console.log({ PERMIT_TYPE_HASH }) interface Eip1271Contract { address: string; @@ -23,7 +22,6 @@ async function signEOA(digest: string, account: ExternallyOwnedAccount) { async function signEIP1271(digest: string, eip1271Contract: Eip1271Contract) { const sig = await eip1271Contract.sign(digest) - console.log({ sig }) return { v: sig.v, r: sig.r, s: sig.s } } @@ -43,7 +41,7 @@ export function makeDomainSeparator(name: string, version: string, chainId: BigN ) } -export async function signPermit(owner: ExternallyOwnedAccount | Eip1271Contract, spender: string, value: number, nonce: number, deadline: string, domainSeparator: string) { +export async function signPermit(owner: ExternallyOwnedAccount | Eip1271Contract, spender: string, value: number, deadline: string, nonce: number, domainSeparator: string) { const digest = calculatePermitDigest(owner.address, spender, value, nonce, deadline, domainSeparator) if (owner.hasOwnProperty('sign')) { return await signEIP1271(digest, owner as Eip1271Contract); @@ -52,7 +50,7 @@ export async function signPermit(owner: ExternallyOwnedAccount | Eip1271Contract } } -function calculatePermitDigest(owner: string, spender: string, value: number, nonce: number, deadline: string, domainSeparator: string) { +export function calculatePermitDigest(owner: string, spender: string, value: number, nonce: number, deadline: string, domainSeparator: string) { return calculateEIP712Digest( domainSeparator, PERMIT_TYPE_HASH, @@ -62,11 +60,9 @@ function calculatePermitDigest(owner: string, spender: string, value: number, no } function calculateEIP712Digest(domainSeparator: string, typeHash: string, types: string[], parameters: unknown[]) { - return streccak( - '0x1901' + - strip0x(domainSeparator) + - strip0x(keccak256(defaultAbiCoder.encode(['bytes32', ...types], [typeHash, ...parameters]))) - ) + 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) { 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; From ba0b41e8c0b388a673eedc236a20bade7bcf2e71 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Tue, 2 Apr 2024 14:49:55 +0300 Subject: [PATCH 48/61] test(rebasable permit): move the rest tests from core stethpermit.test.js --- contracts/token/ERC20RebasablePermit.sol | 59 +----- test/token/ERC20Permit.unit.test.ts | 224 +++++++++++++++++++++-- utils/testing/permit-helpers.ts | 38 +++- 3 files changed, 245 insertions(+), 76 deletions(-) diff --git a/contracts/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasablePermit.sol index 6321672d..22353a02 100644 --- a/contracts/token/ERC20RebasablePermit.sol +++ b/contracts/token/ERC20RebasablePermit.sol @@ -3,60 +3,13 @@ pragma solidity 0.8.10; -// import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; - -// import {SignatureUtils} from "../common/lib/SignatureUtils.sol"; -// import {IEIP712ERC20Rebasable} from "../lib/IEIP712ERC20Rebasable.sol"; - import {UnstructuredStorage} from "./UnstructuredStorage.sol"; import {ERC20Rebasable} from "./ERC20Rebasable.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"; -/** - * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by - * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC2612 { - /** - * @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; - - /** - * @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); - - /** - * @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); -} - - contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { using UnstructuredStorage for bytes32; @@ -65,6 +18,8 @@ contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { */ mapping(address => uint256) internal noncesByAddress; + // TODO: outline structured storage used because at least EIP712 uses it + /** * @dev Typehash constant for ERC-2612 (Permit) * @@ -73,11 +28,9 @@ contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { bytes32 internal constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; - // TODO: outline structured storage used because at least EIP712 uses it - // TODO: use custom errors - /// @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 wrappedToken_ address of the ERC20 token to wrap /// @param tokenRateOracle_ address of oracle that returns tokens rate @@ -85,14 +38,14 @@ contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { constructor( string memory name_, string memory symbol_, - // TODO: pass signing domain version + string memory version_, uint8 decimals_, address wrappedToken_, address tokenRateOracle_, address bridge_ ) ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) - EIP712(name_, "2") + EIP712(name_, version_) { } diff --git a/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts index a19bf940..9aedeada 100644 --- a/test/token/ERC20Permit.unit.test.ts +++ b/test/token/ERC20Permit.unit.test.ts @@ -2,7 +2,8 @@ import hre from "hardhat"; import { assert } from "chai"; import { unit, UnitTest } from "../../utils/testing"; import { wei } from "../../utils/wei"; -import { makeDomainSeparator, signPermit } from "../../utils/testing/permit-helpers"; +import { makeDomainSeparator, signPermit, calculateTransferAuthorizationDigest, signEOAorEIP1271 } from "../../utils/testing/permit-helpers"; +import testing from "../../utils/testing"; import { ERC20Bridged__factory, @@ -17,7 +18,8 @@ import { BigNumber } from "ethers"; type ContextType = Awaited>> const TOKEN_NAME = 'Liquid staked Ether 2.0' -const SIGNING_DOMAIN_VERSION = '2' +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 = [ @@ -67,14 +69,10 @@ function permitTestsSuit(unitInstance: UnitTest) assert.isDefined(hre.network.config.chainId) assert.equal(chainId.toNumber(), getChainId()) assert.equal(verifyingContract, token.address) - - const domainSeparator = makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, chainId, token.address) - assert.equal(makeDomainSeparator(name, version, chainId, verifyingContract), domainSeparator) }) .test('DOMAIN_SEPARATOR() is correct', async (ctx) => { const token = ctx.contracts.rebasableProxied - const domainSeparator = makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, getChainId(), token.address) assert.equal(await ctx.contracts.rebasableProxied.DOMAIN_SEPARATOR(), domainSeparator) }) @@ -88,21 +86,20 @@ function permitTestsSuit(unitInstance: UnitTest) // on behalf, and sign with Alice's key let nonce = 0 const charlie = ctx.accounts.user2 - const charlieSigner = hre.ethers.provider.getSigner(charlie.address) + // const charlieSigner = hre.ethers.provider.getSigner(charlie.address) - const domainSeparator = makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, getChainId(), token.address) - let { v, r, s } = await signPermit(owner, spender.address, value, deadline, nonce, domainSeparator) + 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(), domainSeparator) + 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(charlieSigner) + const tx = await token.connect(charlie) .permit(owner.address, spender.address, value, deadline, v, r, s, { gasLimit: '0xffffff' }) // check that allowance is updated @@ -114,10 +111,10 @@ function permitTestsSuit(unitInstance: UnitTest) // increment nonce nonce = 1 value = 4e5 - ;({ v, r, s } = await signPermit(owner, spender.address, value, deadline, nonce, domainSeparator)) + ;({ v, r, s } = await signPermit(owner.address, owner, spender.address, value, deadline, nonce, ctx.domainSeparator)) // submit the permit - const tx2 = await token.connect(charlieSigner).permit(owner.address, spender.address, value, deadline, v, r, s) + 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)) @@ -125,6 +122,202 @@ function permitTestsSuit(unitInstance: UnitTest) 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(); } @@ -164,6 +357,7 @@ function ctxFactoryFactory(signingAccountsFuncFactory: typeof getAccountsEIP1271 const rebasableTokenImpl = await new ERC20RebasablePermit__factory(deployer).deploy( name, symbol, + SIGNING_DOMAIN_VERSION, decimalsToSet, wrappedToken.address, tokenRateOracle.address, @@ -195,7 +389,6 @@ function ctxFactoryFactory(signingAccountsFuncFactory: typeof getAccountsEIP1271 await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); const { alice, bob } = await signingAccountsFuncFactory(); - const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' return { accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate }, @@ -206,7 +399,8 @@ function ctxFactoryFactory(signingAccountsFuncFactory: typeof getAccountsEIP1271 value: 6e6, nonce: 0, deadline: MAX_UINT256, - } + }, + domainSeparator: makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, getChainId(), rebasableProxied.address), }; } } diff --git a/utils/testing/permit-helpers.ts b/utils/testing/permit-helpers.ts index 06bd5617..16531c79 100644 --- a/utils/testing/permit-helpers.ts +++ b/utils/testing/permit-helpers.ts @@ -7,6 +7,9 @@ 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; @@ -19,12 +22,18 @@ 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( @@ -41,13 +50,17 @@ export function makeDomainSeparator(name: string, version: string, chainId: BigN ) } -export async function signPermit(owner: ExternallyOwnedAccount | Eip1271Contract, spender: string, value: number, deadline: string, nonce: number, domainSeparator: string) { - const digest = calculatePermitDigest(owner.address, spender, value, nonce, deadline, domainSeparator) - if (owner.hasOwnProperty('sign')) { - return await signEIP1271(digest, owner as Eip1271Contract); - } else { - return await signEOA(digest, owner as ExternallyOwnedAccount); - } +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) { @@ -59,6 +72,15 @@ export function calculatePermitDigest(owner: string, spender: string, value: num ) } +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) From 18a1dc5adbf969ba291f0be821dd76c265b55f2f Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 2 Apr 2024 15:37:03 +0200 Subject: [PATCH 49/61] add e2e tests for oracle --- .env.example | 1 + scripts/optimism/deploy-oracle.ts | 36 ++++--- test/optimism/pushingTokenRate.e2e.test.ts | 96 +++++++++++++++++++ .../pushingTokenRate.integration.test.ts | 17 ++-- utils/testing/env.ts | 3 + 5 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 test/optimism/pushingTokenRate.e2e.test.ts diff --git a/.env.example b/.env.example index fbf40032..4d4ffc94 100644 --- a/.env.example +++ b/.env.example @@ -77,6 +77,7 @@ 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 diff --git a/scripts/optimism/deploy-oracle.ts b/scripts/optimism/deploy-oracle.ts index d5ef78d1..bf5e38fb 100644 --- a/scripts/optimism/deploy-oracle.ts +++ b/scripts/optimism/deploy-oracle.ts @@ -3,6 +3,7 @@ import prompt from "../../utils/prompt"; import network from "../../utils/network"; import optimism from "../../utils/optimism"; import deploymentOracle from "../../utils/deployment"; +import { TokenRateNotifier__factory } from "../../typechain"; async function main() { const networkName = env.network(); @@ -18,41 +19,52 @@ async function main() { } ); - const deploymentConfig = deploymentOracle.loadMultiChainDeploymentConfig(); + const l1Token = env.address("TOKEN") + const l1Admin = env.address("L1_PROXY_ADMIN"); + const l2Admin = env.address("L2_PROXY_ADMIN"); const [l1DeployScript, l2DeployScript] = await optimism .deploymentOracle(networkName, { logger: console }) .oracleDeployScript( - deploymentConfig.token, + l1Token, { deployer: ethDeployer, admins: { - proxy: deploymentConfig.l1.proxyAdmin, + proxy: l1Admin, bridge: ethDeployer.address, }, }, { deployer: optDeployer, admins: { - proxy: deploymentConfig.l2.proxyAdmin, + proxy: l2Admin, bridge: optDeployer.address, }, } ); - await deploymentOracle.printMultiChainDeploymentConfig( - "Deploy Optimism Bridge", - ethDeployer, - optDeployer, - deploymentConfig, - l1DeployScript, - l2DeployScript - ); +// await deploymentOracle.printMultiChainDeploymentConfig( +// "Deploy Token Rate Oracle", +// ethDeployer, +// optDeployer, +// deploymentConfig, +// l1DeployScript, +// l2DeployScript +// ); await prompt.proceed(); await l1DeployScript.run(); await l2DeployScript.run(); + + /// setup, add observer + const tokenRateNotifier = TokenRateNotifier__factory.connect( + l1DeployScript.tokenRateNotifierImplAddress, + ethDeployer + ); + await tokenRateNotifier + .connect(ethDeployer) + .addObserver(l1DeployScript.opStackTokenRatePusherImplAddress); } main().catch((error) => { 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 index 50f8211f..36f93e83 100644 --- a/test/optimism/pushingTokenRate.integration.test.ts +++ b/test/optimism/pushingTokenRate.integration.test.ts @@ -8,6 +8,7 @@ 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, @@ -55,7 +56,7 @@ scenario("Optimism :: Token Rate Oracle integration test", ctxFactory) ]); }) - .step("finalize pushing rate", async (ctx) => { + .step("Finalize pushing rate", async (ctx) => { const { opTokenRatePusher, tokenRateOracle, @@ -91,15 +92,15 @@ scenario("Optimism :: Token Rate Oracle integration test", ctxFactory) assert.equalBN(answer, tokenRate); const [ - answer1, - answer2, - answer3, - answer4, - answer5 + , + tokenRateAnswer, + , + updatedAt, + ] = await tokenRateOracle.latestRoundData(); - assert.equalBN(answer2, tokenRate); - assert.equalBN(answer4, blockTimestampStr); + assert.equalBN(tokenRateAnswer, tokenRate); + assert.equalBN(updatedAt, blockTimestampStr); }) .run(); diff --git a/utils/testing/env.ts b/utils/testing/env.ts index 1a00941e..0f61419e 100644 --- a/utils/testing/env.ts +++ b/utils/testing/env.ts @@ -33,6 +33,9 @@ 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"); }, From 5f86091943e1dbd3df9cb1eb92b1ecd060296641 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 3 Apr 2024 09:53:59 +0200 Subject: [PATCH 50/61] remove increaseAllowance and decreaseAllowance from tokens --- contracts/arbitrum/README.md | 30 ----- contracts/optimism/README.md | 30 ----- contracts/token/ERC20Core.sol | 32 ----- contracts/token/ERC20Rebasable.sol | 32 ----- test/token/ERC20Bridged.unit.test.ts | 169 ------------------------- test/token/ERC20Rebasable.unit.test.ts | 169 ------------------------- 6 files changed, 462 deletions(-) diff --git a/contracts/arbitrum/README.md b/contracts/arbitrum/README.md index cf76c5c1..26f3c4fb 100644 --- a/contracts/arbitrum/README.md +++ b/contracts/arbitrum/README.md @@ -695,36 +695,6 @@ 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) diff --git a/contracts/optimism/README.md b/contracts/optimism/README.md index 598f7b99..954556d6 100644 --- a/contracts/optimism/README.md +++ b/contracts/optimism/README.md @@ -512,36 +512,6 @@ 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) 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/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 81c4eb09..ba69ba13 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -161,38 +161,6 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta 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_, - _getTokenAllowance()[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 = _getTokenAllowance()[msg.sender][spender_]; - if (currentAllowance < subtractedValue_) { - revert ErrorDecreasedAllowanceBelowZero(); - } - unchecked { - _approve(msg.sender, spender_, currentAllowance - subtractedValue_); - } - return true; - } - function _getTokenAllowance() internal pure returns (mapping(address => mapping(address => uint256)) storage) { return TOKEN_ALLOWANCE_POSITION.storageMapAddressMapAddressUint256(); } 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/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index b8cb3e47..201a1551 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -706,175 +706,6 @@ unit("ERC20Rebasable", ctxFactory) ); }) - .test("increaseAllowance() :: initial allowance is zero", async (ctx) => { - const { rebasableProxied } = ctx.contracts; - const { holder, spender } = ctx.accounts; - - // validate allowance before increasing - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - "0" - ); - - const allowanceIncrease = wei`1 ether`; - - // increase allowance - const tx = await rebasableProxied.increaseAllowance( - spender.address, - allowanceIncrease - ); - - // validate Approval event was emitted - await assert.emits(rebasableProxied, tx, "Approval", [ - holder.address, - spender.address, - allowanceIncrease, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - allowanceIncrease - ); - }) - - .test("increaseAllowance() :: initial allowance is not zero", async (ctx) => { - const { rebasableProxied } = ctx.contracts; - const { holder, spender } = ctx.accounts; - - const initialAllowance = wei`2 ether`; - - // set initial allowance - await rebasableProxied.approve(spender.address, initialAllowance); - - // validate allowance before increasing - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - initialAllowance - ); - - const allowanceIncrease = wei`1 ether`; - - // increase allowance - const tx = await rebasableProxied.increaseAllowance( - spender.address, - allowanceIncrease - ); - - const expectedAllowance = wei - .toBigNumber(initialAllowance) - .add(allowanceIncrease); - - // validate Approval event was emitted - await assert.emits(rebasableProxied, tx, "Approval", [ - holder.address, - spender.address, - expectedAllowance, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - expectedAllowance - ); - }) - - .test("increaseAllowance() :: the increase is not zero", async (ctx) => { - const { rebasableProxied } = ctx.contracts; - const { holder, spender } = ctx.accounts; - - const initialAllowance = wei`2 ether`; - - // set initial allowance - await rebasableProxied.approve(spender.address, initialAllowance); - - // validate allowance before increasing - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - initialAllowance - ); - - // increase allowance - const tx = await rebasableProxied.increaseAllowance(spender.address, "0"); - - // validate Approval event was emitted - await assert.emits(rebasableProxied, tx, "Approval", [ - holder.address, - spender.address, - initialAllowance, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - initialAllowance - ); - }) - - .test( - "decreaseAllowance() :: decrease is greater than current allowance", - async (ctx) => { - const { rebasableProxied } = ctx.contracts; - const { holder, spender } = ctx.accounts; - - // validate allowance before increasing - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - "0" - ); - - const allowanceDecrease = wei`1 ether`; - - // decrease allowance - await assert.revertsWith( - rebasableProxied.decreaseAllowance(spender.address, allowanceDecrease), - "ErrorDecreasedAllowanceBelowZero()" - ); - } - ) - - .group([wei`1 ether`, "0"], (allowanceDecrease) => [ - `decreaseAllowance() :: the decrease is ${allowanceDecrease} wei`, - async (ctx) => { - const { rebasableProxied } = ctx.contracts; - const { holder, spender } = ctx.accounts; - - const initialAllowance = wei`2 ether`; - - // set initial allowance - await rebasableProxied.approve(spender.address, initialAllowance); - - // validate allowance before increasing - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - initialAllowance - ); - - // decrease allowance - const tx = await rebasableProxied.decreaseAllowance( - spender.address, - allowanceDecrease - ); - - const expectedAllowance = wei - .toBigNumber(initialAllowance) - .sub(allowanceDecrease); - - // validate Approval event was emitted - await assert.emits(rebasableProxied, tx, "Approval", [ - holder.address, - spender.address, - expectedAllowance, - ]); - - // validate allowance was updated correctly - assert.equalBN( - await rebasableProxied.allowance(holder.address, spender.address), - expectedAllowance - ); - }, - ]) - .test("bridgeMint() :: not owner", async (ctx) => { const { rebasableProxied } = ctx.contracts; const { stranger } = ctx.accounts; From cc868ef046985ed7b8e225bfe99f8f93a09e9fc5 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 3 Apr 2024 10:01:18 +0200 Subject: [PATCH 51/61] change function names --- contracts/optimism/L2ERC20TokenBridge.sol | 4 +-- contracts/token/ERC20Rebasable.sol | 4 +-- .../token/interfaces/IERC20BridgedShares.sol | 4 +-- test/token/ERC20Permit.unit.test.ts | 2 +- test/token/ERC20Rebasable.unit.test.ts | 28 +++++++++---------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20TokenBridge.sol index 2c7b54b4..dd01601e 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20TokenBridge.sol @@ -98,7 +98,7 @@ contract L2ERC20TokenBridge is ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); - ERC20Rebasable(L2_TOKEN_REBASABLE).mintShares(to_, amount_); + ERC20Rebasable(L2_TOKEN_REBASABLE).bridgeMintShares(to_, amount_); uint256 rebasableTokenAmount = ERC20Rebasable(L2_TOKEN_REBASABLE).getTokensByShares(amount_); emit DepositFinalized( @@ -131,7 +131,7 @@ contract L2ERC20TokenBridge is ) internal { if (l2Token_ == L2_TOKEN_REBASABLE) { uint256 shares = ERC20Rebasable(L2_TOKEN_REBASABLE).getSharesByTokens(amount_); - ERC20Rebasable(L2_TOKEN_REBASABLE).burnShares(msg.sender, shares); + ERC20Rebasable(L2_TOKEN_REBASABLE).bridgeBurnShares(msg.sender, shares); _initiateWithdrawal( L1_TOKEN_REBASABLE, diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index ba69ba13..eb7f8751 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -85,12 +85,12 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta } /// @inheritdoc IERC20BridgedShares - function mintShares(address account_, uint256 amount_) external onlyBridge { + function bridgeMintShares(address account_, uint256 amount_) external onlyBridge { _mintShares(account_, amount_); } /// @inheritdoc IERC20BridgedShares - function burnShares(address account_, uint256 amount_) external onlyBridge { + function bridgeBurnShares(address account_, uint256 amount_) external onlyBridge { _burnShares(account_, amount_); } diff --git a/contracts/token/interfaces/IERC20BridgedShares.sol b/contracts/token/interfaces/IERC20BridgedShares.sol index df7a0d9e..8ae4be6d 100644 --- a/contracts/token/interfaces/IERC20BridgedShares.sol +++ b/contracts/token/interfaces/IERC20BridgedShares.sol @@ -14,10 +14,10 @@ interface IERC20BridgedShares is IERC20 { /// @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 mintShares(address account_, uint256 amount_) external; + 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 burnShares(address account_, uint256 amount_) external; + function bridgeBurnShares(address account_, uint256 amount_) external; } diff --git a/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts index 9aedeada..81a0e20f 100644 --- a/test/token/ERC20Permit.unit.test.ts +++ b/test/token/ERC20Permit.unit.test.ts @@ -386,7 +386,7 @@ function ctxFactoryFactory(signingAccountsFuncFactory: typeof getAccountsEIP1271 ); await tokenRateOracle.connect(owner).updateRate(rate, 1000); - await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); + await rebasableProxied.connect(owner).bridgeMintShares(holder.address, premintShares); const { alice, bob } = await signingAccountsFuncFactory(); return { diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 201a1551..e763ec09 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -342,7 +342,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).unwrap(wei`4 ether`), "ErrorNotEnoughBalance()"); }) - .test("mintShares() :: happy path", async (ctx) => { + .test("bridgeMintShares() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const {user1, user2, owner } = ctx.accounts; @@ -358,7 +358,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); - const tx0 = await rebasableProxied.connect(owner).mintShares(user1.address, user1SharesToMint); + const tx0 = await rebasableProxied.connect(owner).bridgeMintShares(user1.address, user1SharesToMint); assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1SharesToMint); assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1TokensMinted); @@ -374,7 +374,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - const tx1 = await rebasableProxied.connect(owner).mintShares(user2.address, user2SharesToMint); + const tx1 = await rebasableProxied.connect(owner).bridgeMintShares(user2.address, user2SharesToMint); assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2SharesToMint); assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2TokensMinted); @@ -384,7 +384,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.totalSupply(), premintTokens.add(user1TokensMinted).add(user2TokensMinted)); }) - .test("burnShares() :: happy path", async (ctx) => { + .test("bridgeBurnShares() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; const {user1, user2, owner } = ctx.accounts; @@ -406,11 +406,11 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.sharesOf(user1.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user1.address), 0); - await rebasableProxied.connect(owner).mintShares(user1.address, user1SharesToMint); + 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).burnShares(user1.address, user1SharesToBurn); + await rebasableProxied.connect(owner).bridgeBurnShares(user1.address, user1SharesToBurn); assert.equalBN(await rebasableProxied.sharesOf(user1.address), user1Shares); assert.equalBN(await rebasableProxied.balanceOf(user1.address), user1Tokens); @@ -431,10 +431,10 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.sharesOf(user2.address), 0); assert.equalBN(await rebasableProxied.balanceOf(user2.address), 0); - await rebasableProxied.connect(owner).mintShares(user2.address, user2SharesToMint); + 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).burnShares(user2.address, user2SharesToBurn); + await rebasableProxied.connect(owner).bridgeBurnShares(user2.address, user2SharesToBurn); assert.equalBN(await rebasableProxied.sharesOf(user2.address), user2Shares); assert.equalBN(await rebasableProxied.balanceOf(user2.address), user2Tokens); @@ -713,7 +713,7 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith( rebasableProxied .connect(stranger) - .mintShares(stranger.address, wei`1000 ether`), + .bridgeMintShares(stranger.address, wei`1000 ether`), "ErrorNotBridge()" ); }) @@ -734,7 +734,7 @@ unit("ERC20Rebasable", ctxFactory) // mint tokens const tx = await rebasableProxied .connect(owner) - .mintShares(recipient.address, mintAmount); + .bridgeMintShares(recipient.address, mintAmount); // validate Transfer event was emitted await assert.emits(rebasableProxied, tx, "Transfer", [ @@ -762,7 +762,7 @@ unit("ERC20Rebasable", ctxFactory) const { holder, stranger } = ctx.accounts; await assert.revertsWith( - rebasableProxied.connect(stranger).burnShares(holder.address, wei`100 ether`), + rebasableProxied.connect(stranger).bridgeBurnShares(holder.address, wei`100 ether`), "ErrorNotBridge()" ); }) @@ -775,7 +775,7 @@ unit("ERC20Rebasable", ctxFactory) assert.equalBN(await rebasableProxied.balanceOf(stranger.address), 0); await assert.revertsWith( - rebasableProxied.connect(owner).burnShares(stranger.address, wei`100 ether`), + rebasableProxied.connect(owner).bridgeBurnShares(stranger.address, wei`100 ether`), "ErrorNotEnoughBalance()" ); }) @@ -796,7 +796,7 @@ unit("ERC20Rebasable", ctxFactory) // burn tokens const tx = await rebasableProxied .connect(owner) - .burnShares(holder.address, burnAmount); + .bridgeBurnShares(holder.address, burnAmount); // validate Transfer event was emitted await assert.emits(rebasableProxied, tx, "Transfer", [ @@ -886,7 +886,7 @@ async function ctxFactory() { ); await tokenRateOracle.connect(owner).updateRate(rate, 1000); - await rebasableProxied.connect(owner).mintShares(holder.address, premintShares); + await rebasableProxied.connect(owner).bridgeMintShares(holder.address, premintShares); return { accounts: { deployer, owner, recipient, spender, holder, stranger, zero, user1, user2 }, From 06d00c7c4433c4ab70378083e01df462ec712a9f Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 3 Apr 2024 10:10:05 +0200 Subject: [PATCH 52/61] rename stETH to rebasable token in env --- .env.example | 2 +- .env.wsteth.opt_mainnet | 2 +- README.md | 4 ++-- contracts/optimism/L1LidoTokensBridge.sol | 2 +- scripts/optimism/deploy-bridge.ts | 2 +- utils/deployment.ts | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 4d4ffc94..d3ecb36a 100644 --- a/.env.example +++ b/.env.example @@ -29,7 +29,7 @@ ETHERSCAN_API_KEY_OPT= TOKEN= # Address of the rebasable token to deploy the bridge/gateway for -STETH_TOKEN= +REBASABLE_TOKEN= # Name of the network environments used by deployment scripts. # Might be one of: "mainnet", "goerli". diff --git a/.env.wsteth.opt_mainnet b/.env.wsteth.opt_mainnet index ccb7f7d6..86ebcad0 100644 --- a/.env.wsteth.opt_mainnet +++ b/.env.wsteth.opt_mainnet @@ -22,7 +22,7 @@ ETHERSCAN_API_KEY_OPT= TOKEN=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 # Address of the rebasable token to deploy the bridge/gateway for -STETH_TOKEN= +REBASABLE_TOKEN= # Name of the network environments used by deployment scripts. # Might be one of: "mainnet", "goerli". diff --git a/README.md b/README.md index 97b1a883..7781dbe3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ 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 non-rebasable token to deploy a new bridge on the Ethereum chain. -- [`STETH_TOKEN`] (#STETH_TOKEN) - address of the rebasable token to deploy new bridge on the Ethereum chain. +- [`REBASABLE_TOKEN`] (#REBASABLE_TOKEN) - address of the rebasable token to deploy new bridge on the Ethereum chain. - [`NETWORK`](#NETWORK) - name of the network environments used by deployment scripts. Allowed values: `mainnet`, `goerli`. - [`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. @@ -317,7 +317,7 @@ Below variables used in the Arbitrum/Optimism bridge deployment process. Address of the non-rebasable token to deploy a new bridge on the Ethereum chain. -#### `STETH_TOKEN` +#### `REBASABLE_TOKEN` Address of the rebasable token to deploy new bridge on the Ethereum chain. diff --git a/contracts/optimism/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol index b78e223e..cd144d16 100644 --- a/contracts/optimism/L1LidoTokensBridge.sol +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -7,7 +7,7 @@ import {L1ERC20TokenBridge} from "./L1ERC20TokenBridge.sol"; import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; /// @author kovalgek -/// @notice Hides wstETH concept from other contracts to save level of abstraction. +/// @notice Hides wstETH concept from other contracts to keep `L1ERC20TokenBridge` reusable. contract L1LidoTokensBridge is L1ERC20TokenBridge { constructor( diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 3453071c..682eeb4a 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -25,7 +25,7 @@ async function main() { .deployment(networkName, { logger: console }) .erc20TokenBridgeDeployScript( deploymentConfig.token, - deploymentConfig.stETHToken, + deploymentConfig.rebasableToken, deploymentConfig.l2TokenRateOracle, { deployer: ethDeployer, diff --git a/utils/deployment.ts b/utils/deployment.ts index 7612bdf7..eba5c079 100644 --- a/utils/deployment.ts +++ b/utils/deployment.ts @@ -11,7 +11,7 @@ interface ChainDeploymentConfig extends BridgingManagerSetupConfig { interface MultiChainDeploymentConfig { token: string; - stETHToken: string; + rebasableToken: string; l2TokenRateOracle: string; l1: ChainDeploymentConfig; l2: ChainDeploymentConfig; @@ -20,7 +20,7 @@ interface MultiChainDeploymentConfig { export function loadMultiChainDeploymentConfig(): MultiChainDeploymentConfig { return { token: env.address("TOKEN"), - stETHToken: env.address("STETH_TOKEN"), + rebasableToken: env.address("REBASABLE_TOKEN"), l2TokenRateOracle: env.address("TOKEN_RATE_ORACLE"), l1: { proxyAdmin: env.address("L1_PROXY_ADMIN"), From 0b22b0f3652f410e370e5eea3e40d2da8128de67 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 8 Apr 2024 12:57:28 +0200 Subject: [PATCH 53/61] deploy all script, refactor other deploy scripts --- .env.example | 27 ++ scripts/optimism/deploy-bridge.ts | 4 +- scripts/optimism/deploy-new-impls.ts | 73 ++++ scripts/optimism/deploy-oracle.ts | 112 +++--- .../optimism.integration.test.ts | 4 +- utils/deployment.ts | 28 +- utils/optimism/deploymentAll.ts | 318 ----------------- utils/optimism/deploymentAllFromScratch.ts | 320 ++++++++++++++++++ .../optimism/deploymentNewImplementations.ts | 229 +++++++++++++ utils/optimism/deploymentOracle.ts | 167 +++++---- utils/optimism/testing.ts | 4 +- utils/optimism/types.ts | 17 + utils/optimism/upgradeOracle.ts | 79 ----- 13 files changed, 829 insertions(+), 553 deletions(-) create mode 100644 scripts/optimism/deploy-new-impls.ts delete mode 100644 utils/optimism/deploymentAll.ts create mode 100644 utils/optimism/deploymentAllFromScratch.ts create mode 100644 utils/optimism/deploymentNewImplementations.ts delete mode 100644 utils/optimism/upgradeOracle.ts diff --git a/.env.example b/.env.example index d3ecb36a..db673518 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,33 @@ TOKEN= # Address of the rebasable token to deploy the bridge/gateway for REBASABLE_TOKEN= +# Address of token rate notifier. Connects Lido core protocol. +TOKEN_RATE_NOTIFIER= + +# Address of token rate pusher +L1_OP_STACK_TOKEN_RATE_PUSHER= + +# Gas limit required to complete pushing token rate on L2. +L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE= + +# A time period when token rate can be considered outdated. +RATE_OUTDATED_DELAY= + +# 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". NETWORK=mainnet diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 682eeb4a..2e4f272b 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -24,8 +24,8 @@ async function main() { const [l1DeployScript, l2DeployScript] = await optimism .deployment(networkName, { logger: console }) .erc20TokenBridgeDeployScript( - deploymentConfig.token, - deploymentConfig.rebasableToken, + deploymentConfig.l1Token, + deploymentConfig.l1RebasableToken, deploymentConfig.l2TokenRateOracle, { deployer: ethDeployer, diff --git a/scripts/optimism/deploy-new-impls.ts b/scripts/optimism/deploy-new-impls.ts new file mode 100644 index 00000000..2026c9c5 --- /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.rateOutdatedDelay, + } + ); + + 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 index bf5e38fb..7d75e0f3 100644 --- a/scripts/optimism/deploy-oracle.ts +++ b/scripts/optimism/deploy-oracle.ts @@ -2,72 +2,74 @@ import env from "../../utils/env"; import prompt from "../../utils/prompt"; import network from "../../utils/network"; import optimism from "../../utils/optimism"; -import deploymentOracle from "../../utils/deployment"; +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 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 [ethDeployer] = ethOptNetwork.getSigners(env.privateKey(), { + forking: env.forking(), + }); + const [, optDeployer] = ethOptNetwork.getSigners( + env.string("OPT_DEPLOYER_PRIVATE_KEY"), + { + forking: env.forking(), + } + ); - const l1Token = env.address("TOKEN") - const l1Admin = env.address("L1_PROXY_ADMIN"); - const l2Admin = env.address("L2_PROXY_ADMIN"); + const deploymentConfig = deployment.loadMultiChainDeploymentConfig(); - const [l1DeployScript, l2DeployScript] = await optimism - .deploymentOracle(networkName, { logger: console }) - .oracleDeployScript( - l1Token, - { - deployer: ethDeployer, - admins: { - proxy: l1Admin, - bridge: ethDeployer.address, - }, - }, - { - deployer: optDeployer, - admins: { - proxy: l2Admin, - bridge: optDeployer.address, - }, - } - ); + const [l1DeployScript, l2DeployScript] = await optimism + .deploymentOracle(networkName, { logger: console }) + .oracleDeployScript( + deploymentConfig.l1Token, + deploymentConfig.l2GasLimitForPushingTokenRate, + deploymentConfig.rateOutdatedDelay, + { + 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 deploymentOracle.printMultiChainDeploymentConfig( -// "Deploy Token Rate Oracle", -// ethDeployer, -// optDeployer, -// deploymentConfig, -// l1DeployScript, -// l2DeployScript -// ); + await deployment.printMultiChainDeploymentConfig( + "Deploy Token Rate Oracle", + ethDeployer, + optDeployer, + deploymentConfig, + l1DeployScript, + l2DeployScript + ); - await prompt.proceed(); + await prompt.proceed(); - await l1DeployScript.run(); - await l2DeployScript.run(); + await l1DeployScript.run(); + await l2DeployScript.run(); - /// setup, add observer - const tokenRateNotifier = TokenRateNotifier__factory.connect( - l1DeployScript.tokenRateNotifierImplAddress, - ethDeployer - ); - await tokenRateNotifier - .connect(ethDeployer) - .addObserver(l1DeployScript.opStackTokenRatePusherImplAddress); + /// 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; + console.error(error); + process.exitCode = 1; }); diff --git a/test/bridge-executor/optimism.integration.test.ts b/test/bridge-executor/optimism.integration.test.ts index 1cf1db45..bc8fcc13 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -15,7 +15,7 @@ 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/deploymentAll"; +import deploymentAll from "../../utils/optimism/deploymentAllFromScratch"; scenario("Optimism :: Bridge Executor integration test", ctxFactory) .step("Activate L2 bridge", async (ctx) => { @@ -239,7 +239,7 @@ async function ctxFactory() { const [, optDeployScript] = await deploymentAll( networkName - ).erc20TokenBridgeDeployScript( + ).deployAllScript( l1Token.address, l1TokenRebasable.address, { diff --git a/utils/deployment.ts b/utils/deployment.ts index eba5c079..e820bd8f 100644 --- a/utils/deployment.ts +++ b/utils/deployment.ts @@ -10,18 +10,32 @@ interface ChainDeploymentConfig extends BridgingManagerSetupConfig { } interface MultiChainDeploymentConfig { - token: string; - rebasableToken: string; + l1Token: string; + l1RebasableToken: string; + l1OpStackTokenRatePusher: string; + l2GasLimitForPushingTokenRate: number; + rateOutdatedDelay: number; + l1TokenBridge: string; + l2TokenBridge: string; + l2Token: string; l2TokenRateOracle: string; + govBridgeExecutor: string; l1: ChainDeploymentConfig; l2: ChainDeploymentConfig; } export function loadMultiChainDeploymentConfig(): MultiChainDeploymentConfig { return { - token: env.address("TOKEN"), - rebasableToken: env.address("REBASABLE_TOKEN"), - l2TokenRateOracle: env.address("TOKEN_RATE_ORACLE"), + 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")), + rateOutdatedDelay: Number(env.string("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"), @@ -53,8 +67,8 @@ export async function printMultiChainDeploymentConfig( l1DeployScript: DeployScript, l2DeployScript: DeployScript ) { - const { token, stETHToken, l1, l2 } = deploymentParams; - console.log(chalk.bold(`${title} :: ${chalk.underline(token)} :: ${chalk.underline(stETHToken)}\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/optimism/deploymentAll.ts b/utils/optimism/deploymentAll.ts deleted file mode 100644 index 993b7a2c..00000000 --- a/utils/optimism/deploymentAll.ts +++ /dev/null @@ -1,318 +0,0 @@ -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, - ERC20Rebasable__factory, - IERC20Metadata__factory, - L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, - OssifiableProxy__factory, - TokenRateOracle__factory, - TokenRateNotifier__factory, - OpStackTokenRatePusher__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 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 and Oracle -export default function deploymentAll( - networkName: NetworkName, - options: OptDeploymentOptions = {} -) { - const optAddresses = addresses(networkName, options); - return { - async erc20TokenBridgeDeployScript( - 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: [ - 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: ERC20Rebasable__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, - ERC20Rebasable__factory.createInterface().encodeFunctionData( - "initialize", - [l2TokenRebasableName, l2TokenRebasableSymbol] - ), - options?.overrides, - ], - afterDeploy: (c) => - assert.equal(c.address, expectedL2TokenRebasableProxyAddress), - }) - .addStep({ - factory: L2ERC20TokenBridge__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, - L2ERC20TokenBridge__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/deploymentAllFromScratch.ts b/utils/optimism/deploymentAllFromScratch.ts new file mode 100644 index 00000000..6b95f1b1 --- /dev/null +++ b/utils/optimism/deploymentAllFromScratch.ts @@ -0,0 +1,320 @@ +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, + ERC20Rebasable__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20TokenBridge__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: [ + 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: ERC20Rebasable__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, + ERC20Rebasable__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ + factory: L2ERC20TokenBridge__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, + L2ERC20TokenBridge__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/deploymentNewImplementations.ts b/utils/optimism/deploymentNewImplementations.ts new file mode 100644 index 00000000..706dbf41 --- /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, + ERC20Rebasable__factory, + IERC20Metadata__factory, + L1LidoTokensBridge__factory, + L2ERC20TokenBridge__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: ERC20Rebasable__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, + ERC20Rebasable__factory.createInterface().encodeFunctionData( + "initialize", + [l2TokenRebasableName, l2TokenRebasableSymbol] + ), + options?.overrides, + ], + afterDeploy: (c) => + assert.equal(c.address, expectedL2TokenRebasableProxyAddress), + }) + .addStep({ + factory: L2ERC20TokenBridge__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 index ec28c820..002a9230 100644 --- a/utils/optimism/deploymentOracle.ts +++ b/utils/optimism/deploymentOracle.ts @@ -1,8 +1,8 @@ import { assert } from "chai"; -import { Overrides, Wallet } from "ethers"; +import { Wallet } from "ethers"; import { ethers } from "hardhat"; import addresses from "./addresses"; -import { CommonOptions } from "./types"; +import { DeployScriptParams, OptDeploymentOptions } from "./types"; import network, { NetworkName } from "../network"; import { DeployScript, Logger } from "../deployment/DeployScript"; import { @@ -10,20 +10,10 @@ import { TokenRateOracle__factory, TokenRateNotifier__factory, OpStackTokenRatePusher__factory - } from "../../typechain"; - -interface OptDeployScriptParams { - deployer: Wallet; - admins: { proxy: string; bridge: string }; -} - -interface OptDeploymentOptions extends CommonOptions { - logger?: Logger; - overrides?: Overrides; -} +} from "../../typechain"; +interface OptDeployScriptParams extends DeployScriptParams {} export class OracleL1DeployScript extends DeployScript { - constructor( deployer: Wallet, tokenRateNotifierImplAddress: string, @@ -40,7 +30,6 @@ export class OracleL1DeployScript extends DeployScript { } export class OracleL2DeployScript extends DeployScript { - constructor( deployer: Wallet, tokenRateOracleImplAddress: string, @@ -50,7 +39,7 @@ export class OracleL2DeployScript extends DeployScript { super(deployer, logger); this.tokenRateOracleImplAddress = tokenRateOracleImplAddress; this.tokenRateOracleProxyAddress = tokenRateOracleProxyAddress; - } + } public tokenRateOracleImplAddress: string; public tokenRateOracleProxyAddress: string; @@ -59,83 +48,85 @@ export class OracleL2DeployScript extends DeployScript { export default function deploymentOracle( networkName: NetworkName, options: OptDeploymentOptions = {} - ) { +) { const optAddresses = addresses(networkName, options); return { - async oracleDeployScript( - l1Token: string, - l1Params: OptDeployScriptParams, - l2Params: OptDeployScriptParams, - ): Promise<[OracleL1DeployScript, OracleL2DeployScript]> { + async oracleDeployScript( + l1Token: string, + l2GasLimitForPushingTokenRate: number, + rateOutdatedDelay: number, + l1Params: OptDeployScriptParams, + l2Params: OptDeployScriptParams, + ): Promise<[OracleL1DeployScript, OracleL2DeployScript]> { - const [ - expectedL1TokenRateNotifierImplAddress, - expectedL1OpStackTokenRatePusherImplAddress, - ] = await network.predictAddresses(l1Params.deployer, 2); + const [ + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + ] = await network.predictAddresses(l1Params.deployer, 2); - const [ - expectedL2TokenRateOracleImplAddress, - expectedL2TokenRateOracleProxyAddress - ] = await network.predictAddresses(l2Params.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: [ - 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 l1DeployScript = new OracleL1DeployScript( + l1Params.deployer, + expectedL1TokenRateNotifierImplAddress, + expectedL1OpStackTokenRatePusherImplAddress, + options?.logger + ) + .addStep({ + factory: TokenRateNotifier__factory, + args: [ + 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, - 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), - }); + const l2DeployScript = new OracleL2DeployScript( + l2Params.deployer, + expectedL2TokenRateOracleImplAddress, + expectedL2TokenRateOracleProxyAddress, + options?.logger + ) + .addStep({ + factory: TokenRateOracle__factory, + args: [ + optAddresses.L2CrossDomainMessenger, + ethers.constants.AddressZero, + expectedL1OpStackTokenRatePusherImplAddress, + rateOutdatedDelay, + 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]; - }, + return [l1DeployScript as OracleL1DeployScript, l2DeployScript as OracleL2DeployScript]; + }, }; - } +} diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index a0cd5148..e7848efe 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -18,7 +18,7 @@ import { } from "../../typechain"; import addresses from "./addresses"; import contracts from "./contracts"; -import deploymentAll from "./deploymentAll"; +import deploymentAll from "./deploymentAllFromScratch"; import testingUtils from "../testing"; import { BridgingManagement } from "../bridging-management"; import network, { NetworkName, SignerOrProvider } from "../network"; @@ -199,7 +199,7 @@ async function deployTestBridge( const [ethDeployScript, optDeployScript] = await deploymentAll( networkName - ).erc20TokenBridgeDeployScript( + ).deployAllScript( l1Token.address, l1TokenRebasable.address, { 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/optimism/upgradeOracle.ts b/utils/optimism/upgradeOracle.ts deleted file mode 100644 index 17fd0b26..00000000 --- a/utils/optimism/upgradeOracle.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - OssifiableProxy__factory, - OptimismBridgeExecutor__factory -} from "../../typechain"; - -import network, { NetworkName } from "../network"; -import testingUtils from "../testing"; -import contracts from "./contracts"; -import testing from "../../utils/testing"; -import optimism from "../../utils/optimism"; -import { getBridgeExecutorParams } from "../../utils/bridge-executor"; - -export async function upgradeOracle( - networkName: NetworkName, - oracleProxyAddress: string, - newOracleAddress: string - ) { - const ethOptNetworks = network.multichain(["eth", "opt"], networkName); - const [ - ethProvider, - optProvider - ] = ethOptNetworks.getProviders({ forking: true }); - const ethDeployer = testing.accounts.deployer(ethProvider); - const optDeployer = testing.accounts.deployer(optProvider); - - - const optContracts = contracts(networkName, { forking: true }); - const l1CrossDomainMessengerAliased = await testingUtils.impersonate( - testingUtils.accounts.applyL1ToL2Alias(optContracts.L1CrossDomainMessenger.address), - optProvider - ); - const l2CrossDomainMessenger = await optContracts.L2CrossDomainMessenger.connect( - l1CrossDomainMessengerAliased - ); - - - 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(), - optProvider - ) - : await new OptimismBridgeExecutor__factory(optDeployer).deploy( - optAddresses.L2CrossDomainMessenger, - ethDeployer.address, - ...getBridgeExecutorParams(), - optDeployer.address - ); - - - const l1EthGovExecutorAddress = await govBridgeExecutor.getEthereumGovernanceExecutor(); - const bridgeExecutor = govBridgeExecutor.connect(optDeployer); - const l2OracleProxy = OssifiableProxy__factory.connect( - oracleProxyAddress, - optDeployer - ); - - await l2CrossDomainMessenger.relayMessage( - 0, - l1EthGovExecutorAddress, - bridgeExecutor.address, - 0, - 300_000, - bridgeExecutor.interface.encodeFunctionData("queue", [ - [oracleProxyAddress], - [0], - ["proxy__upgradeTo(address)"], - [ - "0x" + - l2OracleProxy.interface - .encodeFunctionData("proxy__upgradeTo", [newOracleAddress]) - .substring(10), - ], - [false], - ]), - { gasLimit: 5_000_000 } - ); -} From f563fab9045c620d6fbbefbbb9ffef6b51d19b9e Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 9 Apr 2024 23:02:58 +0200 Subject: [PATCH 54/61] add permit to non-rebasable token --- contracts/token/ERC20BridgedPermit.sol | 35 ++++++++ contracts/token/ERC20Permit.sol | 107 +++++++++++++++++++++++ contracts/token/ERC20Rebasable.sol | 24 ++++- contracts/token/ERC20RebasablePermit.sol | 92 ++----------------- test/token/ERC20Rebasable.unit.test.ts | 6 ++ 5 files changed, 179 insertions(+), 85 deletions(-) create mode 100644 contracts/token/ERC20BridgedPermit.sol create mode 100644 contracts/token/ERC20Permit.sol diff --git a/contracts/token/ERC20BridgedPermit.sol b/contracts/token/ERC20BridgedPermit.sol new file mode 100644 index 00000000..d936ae37 --- /dev/null +++ b/contracts/token/ERC20BridgedPermit.sol @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ERC20Bridged} from "./ERC20Bridged.sol"; +import {ERC20Permit} from "./ERC20Permit.sol"; + +contract ERC20BridgedPermit is ERC20Bridged, ERC20Permit { + + /// @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_) + ERC20Permit(name_, version_) + { + } + + function _permitAccepted( + address owner_, + address spender_, + uint256 amount_ + ) internal override { + _approve(owner_, spender_, amount_); + } +} diff --git a/contracts/token/ERC20Permit.sol b/contracts/token/ERC20Permit.sol new file mode 100644 index 00000000..634731c7 --- /dev/null +++ b/contracts/token/ERC20Permit.sol @@ -0,0 +1,107 @@ +// 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"; + +contract ERC20Permit 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.isValidSignatureNow(_owner, hash, _v, _r, _s)) { + revert ErrorInvalidSignature(); + } + + _permitAccepted(_owner, _spender, _value); + } + + function _permitAccepted( + address owner_, + address spender_, + uint256 amount_ + ) internal virtual { + } + + /** + * @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; + } + + error ErrorInvalidSignature(); + error ErrorDeadlineExpired(); +} diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index eb7f8751..49939548 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -191,7 +191,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ) internal onlyNonZeroAccount(from_) onlyNonZeroAccount(to_) { uint256 sharesToTransfer = _getSharesByTokens(amount_); _transferShares(from_, to_, sharesToTransfer); - emit Transfer(from_, to_, amount_); + _emitTransferEvents(from_, to_, amount_ ,sharesToTransfer); } /// @dev Updates owner_'s allowance for spender_ based on spent amount_. Does not update @@ -271,7 +271,8 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ) internal onlyNonZeroAccount(recipient_) { _setTotalShares(_getTotalShares() + amount_); _getShares()[recipient_] = _getShares()[recipient_] + amount_; - emit Transfer(address(0), recipient_, amount_); + uint256 tokensAmount = _getTokensByShares(amount_); + _emitTransferEvents(address(0), recipient_, tokensAmount ,amount_); } /// @dev Destroys amount_ shares from account_, reducing the total shares supply. @@ -307,6 +308,17 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta _getShares()[recipient_] = _getShares()[recipient_] + sharesAmount_; } + /// @dev Emits `Transfer` and `TransferShares` events + function _emitTransferEvents( + address _from, + address _to, + uint _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)) { @@ -323,6 +335,14 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta _; } + /// @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(); diff --git a/contracts/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasablePermit.sol index 22353a02..57606446 100644 --- a/contracts/token/ERC20RebasablePermit.sol +++ b/contracts/token/ERC20RebasablePermit.sol @@ -3,30 +3,10 @@ pragma solidity 0.8.10; -import {UnstructuredStorage} from "./UnstructuredStorage.sol"; import {ERC20Rebasable} from "./ERC20Rebasable.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"; +import {ERC20Permit} from "./ERC20Permit.sol"; - -contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, 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; +contract ERC20RebasablePermit is ERC20Rebasable, ERC20Permit { /// @param name_ The name of the token /// @param symbol_ The symbol of the token @@ -45,69 +25,15 @@ contract ERC20RebasablePermit is IERC2612, ERC20Rebasable, EIP712 { address bridge_ ) ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) - EIP712(name_, version_) + ERC20Permit(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.isValidSignatureNow(_owner, hash, _v, _r, _s)) { - revert ErrorInvalidSignature(); - } - _approve(_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(); + function _permitAccepted( + address owner_, + address spender_, + uint256 amount_ + ) internal override { + _approve(owner_, spender_, amount_); } - - /** - * @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; - } - - error ErrorInvalidSignature(); - error ErrorDeadlineExpired(); } diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index e763ec09..84f7c223 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -737,7 +737,13 @@ unit("ERC20Rebasable", ctxFactory) .bridgeMintShares(recipient.address, mintAmount); // validate Transfer event was emitted + const mintAmountInTokens = await rebasableProxied.getTokensByShares(mintAmount); await assert.emits(rebasableProxied, tx, "Transfer", [ + hre.ethers.constants.AddressZero, + recipient.address, + mintAmountInTokens, + ]); + await assert.emits(rebasableProxied, tx, "TransferShares", [ hre.ethers.constants.AddressZero, recipient.address, mintAmount, From 3332e3069b25a0aa51c839a888f6fbf26ff238d9 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 10 Apr 2024 13:37:40 +0200 Subject: [PATCH 55/61] add unit tests for non-rebasable token --- test/token/ERC20Permit.unit.test.ts | 818 ++++++++++++++++------------ 1 file changed, 457 insertions(+), 361 deletions(-) diff --git a/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts index 81a0e20f..35ce44b3 100644 --- a/test/token/ERC20Permit.unit.test.ts +++ b/test/token/ERC20Permit.unit.test.ts @@ -1,409 +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 { - ERC20Bridged__factory, TokenRateOracle__factory, OssifiableProxy__factory, ERC20RebasablePermit__factory, ERC1271PermitSignerMock__factory, + ERC20BridgedPermit__factory, } from "../../typechain"; -import { BigNumber } from "ethers"; type ContextType = Awaited>> -const TOKEN_NAME = 'Liquid staked Ether 2.0' 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', - }, + { + address: '0xF4C028683CAd61ff284d265bC0F77EDd67B4e65A', + privateKey: '0x5f7edf5892efb4a5cd75dedd496598f48e579b562a70eb1360474cc83a982987', + }, + { + address: '0x7F94c1F9e4BfFccc8Cd79195554E0d83a0a5c5f2', + privateKey: '0x3fe2f6bd9dbc7d507a6cb95ec36a36787706617e34385292b66c74cd39874605', + }, ] function getChainId() { - return hre.network.config.chainId as number; + return hre.network.config.chainId as number; } const getAccountsEOA = async () => { - return { - alice: ACCOUNTS_AND_KEYS[0], - bob: ACCOUNTS_AND_KEYS[1], - } + 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 } + 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.WRAPPED_TOKEN(), wrappedToken.address) - }) - - .test('eip712Domain() is correct', async (ctx) => { - const token = ctx.contracts.rebasableProxied - const [ , name, version, chainId, verifyingContract, , ] = await token.eip712Domain() - - assert.equal(name, TOKEN_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(TOKEN_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 - // const charlieSigner = hre.ethers.provider.getSigner(charlie.address) - - 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) +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.WRAPPED_TOKEN(), 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(); +} - assert.equalBN(await token.nonces(owner.address), BigNumber.from(1)) - assert.emits(token, tx, 'Approval', [ owner, spender, BigNumber.from(value) ]) +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), + }; } - }) - - .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(signingAccountsFuncFactory: typeof getAccountsEIP1271 | typeof getAccountsEOA) { - return async () => { - const name = TOKEN_NAME; - 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 [ - deployer, - owner, - recipient, - spender, - holder, - stranger, - user1, - user2, - ] = await hre.ethers.getSigners(); - - const wrappedToken = await new ERC20Bridged__factory(deployer).deploy( - "WsETH Test Token", - "WsETH", +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 ERC20RebasablePermit__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, + ERC20RebasablePermit__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) + ); + + const rebasableProxied = ERC20RebasablePermit__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 tokenRateOracle = await new TokenRateOracle__factory(deployer).deploy( - hre.ethers.constants.AddressZero, - owner.address, - hre.ethers.constants.AddressZero, - 86400 - ); - const rebasableTokenImpl = await new ERC20RebasablePermit__factory(deployer).deploy( - name, - symbol, - SIGNING_DOMAIN_VERSION, - decimalsToSet, - wrappedToken.address, - tokenRateOracle.address, - owner.address - ); - - await hre.network.provider.request({ - method: "hardhat_impersonateAccount", - params: [hre.ethers.constants.AddressZero], - }); - - const zero = await hre.ethers.getSigner(hre.ethers.constants.AddressZero); const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( - rebasableTokenImpl.address, - deployer.address, - ERC20RebasablePermit__factory.createInterface().encodeFunctionData("initialize", [ - name, - symbol, - ]) + wrappedToken.address, + deployer.address, + ERC20BridgedPermit__factory.createInterface().encodeFunctionData("initialize", [ + name, + symbol, + ]) ); - const rebasableProxied = ERC20RebasablePermit__factory.connect( - l2TokensProxy.address, - holder + const nonRebasableProxied = ERC20BridgedPermit__factory.connect( + l2TokensProxy.address, + holder ); - await tokenRateOracle.connect(owner).updateRate(rate, 1000); - await rebasableProxied.connect(owner).bridgeMintShares(holder.address, premintShares); - 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, wrappedToken, tokenRateOracle }, - permitParams: { - owner: alice, - spender: bob, - value: 6e6, - nonce: 0, - deadline: MAX_UINT256, - }, - domainSeparator: makeDomainSeparator(TOKEN_NAME, SIGNING_DOMAIN_VERSION, getChainId(), rebasableProxied.address), - }; - } + return nonRebasableProxied; } -permitTestsSuit(unit("ERC20Permit with EIP1271 (contract) signing", ctxFactoryFactory(getAccountsEIP1271))); -permitTestsSuit(unit("ERC20Permit with ECDSA (EOA) signing", ctxFactoryFactory(getAccountsEOA))); +permitTestsSuit( + unit("ERC20RebasablePermit with EIP1271 (contract) signing", + ctxFactoryFactory( + "Liquid staked Ether 2.0", + "stETH", + true, + getAccountsEIP1271 + ) + ) +); + +permitTestsSuit( + unit("ERC20RebasablePermit 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 + ) + ) +); From 0545e33eaec087aecce4948853c9a19c8bedcb78 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 10 Apr 2024 22:34:35 +0200 Subject: [PATCH 56/61] PR fixes: fix comments, readme, rename bridges, notifier refactoring --- .env.example | 13 +- README.md | 21 +- contracts/lib/SignatureChecker.sol | 3 +- contracts/lido/TokenRateNotifier.sol | 48 ++- .../interfaces/IPostTokenRebaseReceiver.sol | 20 - .../lido/interfaces/ITokenRatePusher.sol | 4 +- ...ckTokenRatePusherWithOutOfGasErrorStub.sol | 6 +- contracts/optimism/DepositDataCodec.sol | 8 +- ...ge.sol => L1ERC20ExtendedTokensBridge.sol} | 9 +- contracts/optimism/L1LidoTokensBridge.sol | 8 +- ...ge.sol => L2ERC20ExtendedTokensBridge.sol} | 10 +- contracts/optimism/README.md | 12 +- .../token/interfaces/IERC20TokenRate.sol | 2 +- contracts/token/interfaces/IERC20Wrapper.sol | 2 +- .../token/interfaces/ITokenRateOracle.sol | 8 +- scripts/optimism/deploy-bridge.ts | 8 +- test/arbitrum/_launch.test.ts | 4 +- .../optimism.integration.test.ts | 58 +-- test/optimism/L1ERC20TokenBridge.unit.test.ts | 10 +- test/optimism/L2ERC20TokenBridge.unit.test.ts | 8 +- test/optimism/TokenRateNotifier.unit.test.ts | 381 +++++++++--------- .../bridging-rebasable-to.e2e.test.ts | 6 +- test/optimism/bridging-rebasable.e2e.test.ts | 2 +- .../bridging-rebasable.integration.test.ts | 126 +++--- test/optimism/bridging-to.e2e.test.ts | 6 +- test/optimism/bridging.e2e.test.ts | 2 +- test/optimism/bridging.integration.test.ts | 102 ++--- test/optimism/deployment.acceptance.test.ts | 34 +- test/optimism/deposit-gas-estimation.test.ts | 57 +-- test/optimism/managing-deposits.e2e.test.ts | 22 +- test/optimism/managing-executor.e2e.test.ts | 6 +- test/optimism/managing-proxy.e2e.test.ts | 10 +- .../pushingTokenRate.integration.test.ts | 4 + utils/arbitrum/testing.ts | 8 +- utils/optimism/deploymentAllFromScratch.ts | 7 +- .../deploymentBridgesAndRebasableToken.ts | 6 +- .../optimism/deploymentNewImplementations.ts | 4 +- utils/optimism/deploymentOracle.ts | 1 + utils/optimism/testing.ts | 38 +- utils/testing/e2e.ts | 4 +- 40 files changed, 568 insertions(+), 520 deletions(-) delete mode 100644 contracts/lido/interfaces/IPostTokenRebaseReceiver.sol rename contracts/optimism/{L1ERC20TokenBridge.sol => L1ERC20ExtendedTokensBridge.sol} (98%) rename contracts/optimism/{L2ERC20TokenBridge.sol => L2ERC20ExtendedTokensBridge.sol} (97%) diff --git a/.env.example b/.env.example index db673518..b683fcb0 100644 --- a/.env.example +++ b/.env.example @@ -25,23 +25,20 @@ 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 to deploy the bridge/gateway for +# Address of the rebasable token on L1 to deploy the bridge/gateway for REBASABLE_TOKEN= -# Address of token rate notifier. Connects Lido core protocol. -TOKEN_RATE_NOTIFIER= - -# Address of token rate pusher +# 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. L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE= # A time period when token rate can be considered outdated. -RATE_OUTDATED_DELAY= +RATE_OUTDATED_DELAY=86400 # default is 24 hours # Address of L1 token bridge proxy. L1_TOKEN_BRIDGE= @@ -59,7 +56,7 @@ L2_TOKEN_RATE_ORACLE= 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 diff --git a/README.md b/README.md index 7781dbe3..caf32a0d 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 @@ -44,7 +45,15 @@ The configuration of the deployment scripts happens via the ENV variables. The f - [`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. -- [`NETWORK`](#NETWORK) - name of the network environments used by deployment scripts. Allowed values: `mainnet`, `goerli`. +- [`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. +- [`RATE_OUTDATED_DELAY`](#RATE_OUTDATED_DELAY) - a time period when token rate can be considered outdated. Default is 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. @@ -315,17 +324,17 @@ Below variables used in the Arbitrum/Optimism bridge deployment process. #### `TOKEN` -Address of the non-rebasable 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 rebasable token to deploy new bridge on the Ethereum chain. +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` @@ -447,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` @@ -487,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/contracts/lib/SignatureChecker.sol b/contracts/lib/SignatureChecker.sol index d42561c4..e4e3ab59 100644 --- a/contracts/lib/SignatureChecker.sol +++ b/contracts/lib/SignatureChecker.sol @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido // SPDX-License-Identifier: GPL-3.0 -// Writen based on (utils/cryptography/SignatureChecker.sol from d398d68 +// Written based on (utils/cryptography/SignatureChecker.sol from d398d68 pragma solidity 0.8.10; @@ -24,7 +24,6 @@ library SignatureChecker { */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { if (signer.code.length == 0) { - // return true; (address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature); return err == ECDSA.RecoverError.NoError && recovered == signer; } else { diff --git a/contracts/lido/TokenRateNotifier.sol b/contracts/lido/TokenRateNotifier.sol index b9e9e17c..28cf18f0 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -3,21 +3,32 @@ pragma solidity 0.8.10; -import {IPostTokenRebaseReceiver} from "./interfaces/IPostTokenRebaseReceiver.sol"; 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 = 16; - - /// @notice Invalid interface id. - bytes4 public constant INVALID_INTERFACE_ID = 0xffffffff; + 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; @@ -28,6 +39,11 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { /// @notice All observers. address[] public observers; + /// @param initialOwner_ initial owner + constructor(address initialOwner_) { + _transferOwnership(initialOwner_); + } + /// @notice Add a `observer_` to the back of array /// @param observer_ observer address function addObserver(address observer_) external onlyOwner { @@ -55,24 +71,22 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { revert ErrorNoObserverToRemove(); } - for (uint256 obIndex = observerIndexToRemove; obIndex < observers.length - 1; obIndex++) { - observers[obIndex] = observers[obIndex + 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, - uint256, - uint256, - uint256, - uint256, - uint256, - uint256 + uint256, /* reportTimestamp */ + uint256, /* timeElapsed */ + uint256, /* preTotalShares */ + uint256, /* preTotalEther */ + uint256, /* postTotalShares */ + uint256, /* postTotalEther */ + uint256 /* sharesMintedAsFees */ ) external { for (uint256 obIndex = 0; obIndex < observers.length; obIndex++) { try ITokenRatePusher(observers[obIndex]).pushTokenRate() {} @@ -93,7 +107,7 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { /// @notice Observer length /// @return Added observers count - function observersLength() public view returns (uint256) { + function observersLength() external view returns (uint256) { return observers.length; } diff --git a/contracts/lido/interfaces/IPostTokenRebaseReceiver.sol b/contracts/lido/interfaces/IPostTokenRebaseReceiver.sol deleted file mode 100644 index 65b1fe90..00000000 --- a/contracts/lido/interfaces/IPostTokenRebaseReceiver.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @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; -} diff --git a/contracts/lido/interfaces/ITokenRatePusher.sol b/contracts/lido/interfaces/ITokenRatePusher.sol index 9492a240..9d157c3c 100644 --- a/contracts/lido/interfaces/ITokenRatePusher.sol +++ b/contracts/lido/interfaces/ITokenRatePusher.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.10; /// @author kovalgek -/// @notice An interface for entity that pushes rate. +/// @notice An interface for entity that pushes token rate. interface ITokenRatePusher { - /// @notice Pushes token rate to L2 by depositing zero tokens. + /// @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 index 818b4229..cb8d1c26 100644 --- a/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol @@ -8,10 +8,12 @@ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; contract OpStackTokenRatePusherWithOutOfGasErrorStub is ERC165, ITokenRatePusher { - mapping (uint256 => uint256) data; + uint256 public constant OUT_OF_GAS_INCURRING_MAX = 1000000000000; + + mapping (uint256 => uint256) public data; function pushTokenRate() external { - for (uint256 i = 0; i < 1000000000000; ++i) { + for (uint256 i = 0; i < OUT_OF_GAS_INCURRING_MAX; ++i) { data[i] = i; } } diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/optimism/DepositDataCodec.sol index 68ada77b..55178758 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/optimism/DepositDataCodec.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; @@ -9,7 +9,7 @@ contract DepositDataCodec { uint8 internal constant RATE_FIELD_SIZE = 12; uint8 internal constant TIMESTAMP_FIELD_SIZE = 5; - + struct DepositData { uint96 rate; uint40 timestamp; @@ -26,11 +26,11 @@ contract DepositDataCodec { } 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])), diff --git a/contracts/optimism/L1ERC20TokenBridge.sol b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol similarity index 98% rename from contracts/optimism/L1ERC20TokenBridge.sol rename to contracts/optimism/L1ERC20ExtendedTokensBridge.sol index 087df07f..7e303a97 100644 --- a/contracts/optimism/L1ERC20TokenBridge.sol +++ b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol @@ -20,7 +20,7 @@ import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; /// @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 L1ERC20TokenBridge is +abstract contract L1ERC20ExtendedTokensBridge is IL1ERC20Bridge, BridgingManager, RebasableAndNonRebasableTokens, @@ -44,7 +44,12 @@ abstract contract L1ERC20TokenBridge is address l1TokenRebasable_, address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens( + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { L2_TOKEN_BRIDGE = l2TokenBridge_; } diff --git a/contracts/optimism/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol index cd144d16..491ff28b 100644 --- a/contracts/optimism/L1LidoTokensBridge.sol +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.10; -import {L1ERC20TokenBridge} from "./L1ERC20TokenBridge.sol"; +import {L1ERC20ExtendedTokensBridge} from "./L1ERC20ExtendedTokensBridge.sol"; import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; /// @author kovalgek -/// @notice Hides wstETH concept from other contracts to keep `L1ERC20TokenBridge` reusable. -contract L1LidoTokensBridge is L1ERC20TokenBridge { +/// @notice Hides wstETH concept from other contracts to keep `L1ERC20ExtendedTokensBridge` reusable. +contract L1LidoTokensBridge is L1ERC20ExtendedTokensBridge { constructor( address messenger_, @@ -17,7 +17,7 @@ contract L1LidoTokensBridge is L1ERC20TokenBridge { address l1TokenRebasable_, address l2TokenNonRebasable_, address l2TokenRebasable_ - ) L1ERC20TokenBridge( + ) L1ERC20ExtendedTokensBridge( messenger_, l2TokenBridge_, l1TokenNonRebasable_, diff --git a/contracts/optimism/L2ERC20TokenBridge.sol b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol similarity index 97% rename from contracts/optimism/L2ERC20TokenBridge.sol rename to contracts/optimism/L2ERC20ExtendedTokensBridge.sol index dd01601e..f2b630be 100644 --- a/contracts/optimism/L2ERC20TokenBridge.sol +++ b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol @@ -24,7 +24,7 @@ import {DepositDataCodec} from "./DepositDataCodec.sol"; /// 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 +contract L2ERC20ExtendedTokensBridge is IL2ERC20Bridge, BridgingManager, RebasableAndNonRebasableTokens, @@ -48,7 +48,12 @@ contract L2ERC20TokenBridge is address l1TokenRebasable_, address l2TokenNonRebasable_, address l2TokenRebasable_ - ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens(l1TokenNonRebasable_, l1TokenRebasable_, l2TokenNonRebasable_, l2TokenRebasable_) { + ) CrossDomainEnabled(messenger_) RebasableAndNonRebasableTokens( + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ) { L1_TOKEN_BRIDGE = l1TokenBridge_; } @@ -95,6 +100,7 @@ contract L2ERC20TokenBridge is { if (_isRebasableTokenFlow(l1Token_, l2Token_)) { DepositData memory depositData = decodeDepositData(data_); + ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); diff --git a/contracts/optimism/README.md b/contracts/optimism/README.md index 954556d6..c493349d 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) @@ -661,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; @@ -676,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/token/interfaces/IERC20TokenRate.sol b/contracts/token/interfaces/IERC20TokenRate.sol index 16c51870..0b57716e 100644 --- a/contracts/token/interfaces/IERC20TokenRate.sol +++ b/contracts/token/interfaces/IERC20TokenRate.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; diff --git a/contracts/token/interfaces/IERC20Wrapper.sol b/contracts/token/interfaces/IERC20Wrapper.sol index 6b3125d4..c443f3cd 100644 --- a/contracts/token/interfaces/IERC20Wrapper.sol +++ b/contracts/token/interfaces/IERC20Wrapper.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/token/interfaces/ITokenRateOracle.sol index 2f2d32f6..ce057ac8 100644 --- a/contracts/token/interfaces/ITokenRateOracle.sol +++ b/contracts/token/interfaces/ITokenRateOracle.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; @@ -10,7 +10,7 @@ interface ITokenRateOracle { /// @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. - /// @return startedAt_ is time when rate was pushed on L1 side. + /// @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() @@ -34,6 +34,6 @@ interface ITokenRateOracle { /// @notice Updates token rate. /// @param tokenRate_ wstETH/stETH token rate. - /// @param rateL1Timestamp_ L1 time when rate was pushed on L1 side. + /// @param rateL1Timestamp_ L1 time when rate was pushed on L1 side. function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external; -} \ No newline at end of file +} diff --git a/scripts/optimism/deploy-bridge.ts b/scripts/optimism/deploy-bridge.ts index 2e4f272b..57538f49 100644 --- a/scripts/optimism/deploy-bridge.ts +++ b/scripts/optimism/deploy-bridge.ts @@ -59,16 +59,16 @@ 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 = 5; + const l2ERC20ExtendedTokensBridgeProxyDeployStepIndex = 5; const l2BridgingManagement = new BridgingManagement( - l2DeployScript.getContractAddress(l2ERC20TokenBridgeProxyDeployStepIndex), + l2DeployScript.getContractAddress(l2ERC20ExtendedTokensBridgeProxyDeployStepIndex), optDeployer, { logger: console } ); 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 bc8fcc13..ee290cb6 100644 --- a/test/bridge-executor/optimism.integration.test.ts +++ b/test/bridge-executor/optimism.integration.test.ts @@ -1,7 +1,7 @@ import { assert } from "chai"; import { ERC20BridgedStub__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, OptimismBridgeExecutor__factory, ERC20Bridged__factory, @@ -19,23 +19,23 @@ 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(); @@ -46,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)", @@ -56,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), ], @@ -91,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, @@ -126,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), ], @@ -145,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); @@ -154,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, @@ -170,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), ], @@ -188,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); @@ -266,11 +266,11 @@ async function ctxFactory() { optDeployScript.tokenProxyAddress, l2Deployer ); - const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( + const l2ERC20ExtendedTokensBridge = L2ERC20ExtendedTokensBridge__factory.connect( optDeployScript.tokenBridgeProxyAddress, l2Deployer ); - const l2ERC20TokenBridgeProxy = OssifiableProxy__factory.connect( + const l2ERC20ExtendedTokensBridgeProxy = OssifiableProxy__factory.connect( optDeployScript.tokenBridgeProxyAddress, l2Deployer ); @@ -305,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 index efafeb57..d70705db 100644 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L1ERC20TokenBridge.unit.test.ts @@ -4,7 +4,7 @@ import { ERC20BridgedStub__factory, ERC20WrapperStub__factory, L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, EmptyContractStub__factory, } from "../../typechain"; @@ -160,7 +160,7 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) await assert.emits(l1Messenger, tx, "SentMessage", [ l2TokenBridgeEOA.address, l1TokenBridge.address, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( "finalizeDeposit", [ l1TokenNonRebasable.address, @@ -229,7 +229,7 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) await assert.emits(l1Messenger, tx, "SentMessage", [ l2TokenBridgeEOA.address, l1TokenBridge.address, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( "finalizeDeposit", [ l1TokenRebasable.address, @@ -425,7 +425,7 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) await assert.emits(l1Messenger, tx, "SentMessage", [ l2TokenBridgeEOA.address, l1TokenBridge.address, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( "finalizeDeposit", [ l1TokenNonRebasable.address, @@ -497,7 +497,7 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) await assert.emits(l1Messenger, tx, "SentMessage", [ l2TokenBridgeEOA.address, l1TokenBridge.address, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( "finalizeDeposit", [ l1TokenRebasable.address, diff --git a/test/optimism/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20TokenBridge.unit.test.ts index 1fd77dd3..5ea2dc66 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20TokenBridge.unit.test.ts @@ -5,7 +5,7 @@ import { TokenRateOracle__factory, ERC20Rebasable__factory, L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, EmptyContractStub__factory, CrossDomainMessengerStub__factory, @@ -18,7 +18,7 @@ import { getContractAddress } from "ethers/lib/utils"; import { JsonRpcProvider } from "@ethersproject/providers"; import { BigNumber } from "ethers"; -unit("Optimism:: L2ERC20TokenBridge", ctxFactory) +unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) .test("l1TokenBridge()", async (ctx) => { assert.equal( await ctx.l2TokenBridge.l1TokenBridge(), @@ -727,7 +727,7 @@ async function ctxFactory() { l2TokenBridgeProxyAddress ); - const l2TokenBridgeImpl = await new L2ERC20TokenBridge__factory( + const l2TokenBridgeImpl = await new L2ERC20ExtendedTokensBridge__factory( deployer ).deploy( l2MessengerStub.address, @@ -748,7 +748,7 @@ async function ctxFactory() { ]) ); - const l2TokenBridge = L2ERC20TokenBridge__factory.connect( + const l2TokenBridge = L2ERC20ExtendedTokensBridge__factory.connect( l2TokenBridgeProxy.address, deployer ); diff --git a/test/optimism/TokenRateNotifier.unit.test.ts b/test/optimism/TokenRateNotifier.unit.test.ts index 14997721..d8ca1bc0 100644 --- a/test/optimism/TokenRateNotifier.unit.test.ts +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -17,204 +17,225 @@ import { unit("TokenRateNotifier", ctxFactory) - .test("initial state", async (ctx) => { - const { tokenRateNotifier } = ctx.contracts; - - assert.equalBN(await tokenRateNotifier.MAX_OBSERVERS_COUNT(), 16); - assert.equal(await tokenRateNotifier.INVALID_INTERFACE_ID(), "0xffffffff"); - 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.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(); - await assert.revertsWith( - tokenRateNotifier.addObserver(observer.address), - "ErrorBadObserverInterface()" - ); - }) - - .test("addObserver() :: revert on adding too many observers", async (ctx) => { - const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; - - assert.equalBN(await tokenRateNotifier.observersLength(), 0); - const maxObservers = await tokenRateNotifier.MAX_OBSERVERS_COUNT(); - for (let i = 0; i < maxObservers.toNumber(); i++) { - await tokenRateNotifier.addObserver(opStackTokenRatePusher.address); - } - assert.equalBN(await tokenRateNotifier.observersLength(), maxObservers); - - await assert.revertsWith( - tokenRateNotifier.addObserver(opStackTokenRatePusher.address), - "ErrorMaxObserversCountExceeded()" - ); - }) - - .test("addObserver() :: happy path of adding observer", async (ctx) => { - const { tokenRateNotifier, opStackTokenRatePusher } = ctx.contracts; - - assert.equalBN(await tokenRateNotifier.observersLength(), 0); - const tx = await tokenRateNotifier.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.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.addObserver(opStackTokenRatePusher.address); - - assert.equalBN(await tokenRateNotifier.observersLength(), 1); - - const tx = await tokenRateNotifier.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.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.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.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(); + .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; + + assert.equalBN(await tokenRateNotifier.observersLength(), 0); + const maxObservers = await tokenRateNotifier.MAX_OBSERVERS_COUNT(); + for (let i = 0; i < maxObservers.toNumber(); i++) { + 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() :: 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 ctxFactory() { - const [deployer, bridge, stranger, tokenRateOracle] = await ethers.getSigners(); - const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(); + const [deployer, owner, stranger, tokenRateOracle] = await ethers.getSigners(); + const tokenRateNotifier = await new TokenRateNotifier__factory(deployer).deploy(owner.address); const l1TokenRebasableStub = await new ERC20BridgedStub__factory(deployer).deploy( - "L1 Token Rebasable", - "L1R" + "L1 Token Rebasable", + "L1R" ); const l1TokenNonRebasableStub = await new ERC20WrapperStub__factory(deployer).deploy( - l1TokenRebasableStub.address, - "L1 Token Non Rebasable", - "L1NR" + l1TokenRebasableStub.address, + "L1 Token Non Rebasable", + "L1NR" ); const l1MessengerStub = await new CrossDomainMessengerStub__factory( - deployer + deployer ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); const l2GasLimitForPushingTokenRate = 123; const opStackTokenRatePusher = await new OpStackTokenRatePusher__factory(deployer).deploy( - l1MessengerStub.address, - l1TokenNonRebasableStub.address, - tokenRateOracle.address, - l2GasLimitForPushingTokenRate + l1MessengerStub.address, + l1TokenNonRebasableStub.address, + tokenRateOracle.address, + l2GasLimitForPushingTokenRate ); return { - accounts: { deployer, bridge, stranger, tokenRateOracle }, - contracts: { tokenRateNotifier, opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub }, - constants: { l2GasLimitForPushingTokenRate } + accounts: { deployer, owner, stranger, tokenRateOracle }, + contracts: { tokenRateNotifier, opStackTokenRatePusher, l1MessengerStub, l1TokenNonRebasableStub }, + constants: { l2GasLimitForPushingTokenRate } }; } diff --git a/test/optimism/bridging-rebasable-to.e2e.test.ts b/test/optimism/bridging-rebasable-to.e2e.test.ts index ca582770..6dc3d5ac 100644 --- a/test/optimism/bridging-rebasable-to.e2e.test.ts +++ b/test/optimism/bridging-rebasable-to.e2e.test.ts @@ -80,7 +80,7 @@ import { }) .step("Withdraw tokens from L2 via withdrawERC20To()", async (ctx) => { - withdrawTokensTxResponse = await ctx.l2ERC20TokenBridge + withdrawTokensTxResponse = await ctx.l2ERC20ExtendedTokensBridge .connect(ctx.l2Tester) .withdrawTo( ctx.l2TokenRebasable.address, @@ -147,7 +147,7 @@ import { l1TokenRebasable: testingSetup.l1TokenRebasable, l2TokenRebasable: testingSetup.l2TokenRebasable, l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, - l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge: testingSetup.l2ERC20ExtendedTokensBridge, crossChainMessenger: new CrossChainMessenger({ l2ChainId: network.chainId("opt", networkName), l1ChainId: network.chainId("eth", networkName), @@ -157,7 +157,7 @@ import { LidoBridge: { Adapter: LidoBridgeAdapter, l1Bridge: testingSetup.l1LidoTokensBridge.address, - l2Bridge: testingSetup.l2ERC20TokenBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, }, }, }), diff --git a/test/optimism/bridging-rebasable.e2e.test.ts b/test/optimism/bridging-rebasable.e2e.test.ts index 868f0a68..27aaa65d 100644 --- a/test/optimism/bridging-rebasable.e2e.test.ts +++ b/test/optimism/bridging-rebasable.e2e.test.ts @@ -144,7 +144,7 @@ import { LidoBridge: { Adapter: LidoBridgeAdapter, l1Bridge: testingSetup.l1LidoTokensBridge.address, - l2Bridge: testingSetup.l2ERC20TokenBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, }, }, }), diff --git a/test/optimism/bridging-rebasable.integration.test.ts b/test/optimism/bridging-rebasable.integration.test.ts index 68cbabee..fbe208ae 100644 --- a/test/optimism/bridging-rebasable.integration.test.ts +++ b/test/optimism/bridging-rebasable.integration.test.ts @@ -17,13 +17,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Activate bridging on L1", async (ctx) => { const { l1LidoTokensBridge } = ctx; - const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + const { l1ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { await l1LidoTokensBridge - .connect(l1ERC20TokenBridgeAdmin) + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableDeposits(); } else { console.log("L1 deposits already enabled"); @@ -34,7 +34,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) if (!isWithdrawalsEnabled) { await l1LidoTokensBridge - .connect(l1ERC20TokenBridgeAdmin) + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableWithdrawals(); } else { console.log("L1 withdrawals already enabled"); @@ -45,32 +45,32 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .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("Set up Token Rate Oracle by pushing first rate", async (ctx) => { @@ -81,7 +81,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1LidoTokensBridge, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l2Provider } = ctx; @@ -94,10 +94,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .relayMessage( 1, l1LidoTokensBridge.address, - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -116,7 +116,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l1Provider } = ctx; @@ -130,7 +130,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1TokenRebasable.balanceOf( l1LidoTokensBridge.address ); @@ -155,7 +155,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) dataToSend, ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1TokenRebasable.address, @@ -170,7 +170,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, @@ -179,7 +179,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore + l1ERC20ExtendedTokensBridgeBalanceBefore ); assert.equalBN( @@ -195,7 +195,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1LidoTokensBridge, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l2Provider } = ctx; @@ -215,10 +215,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .relayMessage( 1, l1LidoTokensBridge.address, - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -229,7 +229,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -255,7 +255,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l1Provider } = ctx; const { accountA: tokenHolderA } = ctx.accounts; @@ -269,7 +269,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1TokenRebasable.balanceOf( l1LidoTokensBridge.address ); @@ -294,7 +294,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) dataToSend, ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1TokenRebasable.address, @@ -309,7 +309,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, @@ -318,7 +318,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmountNonRebasable) ); assert.equalBN( @@ -334,7 +334,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2TokenRebasable, l1LidoTokensBridge, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l2Provider } = ctx; const { depositAmountNonRebasable, depositAmountRebasable } = ctx.common; @@ -354,10 +354,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .relayMessage( 1, l1LidoTokensBridge.address, - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -368,7 +368,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -393,7 +393,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const { l1TokenRebasable, l2TokenRebasable, - l2ERC20TokenBridge + l2ERC20ExtendedTokensBridge } = ctx; const tokenHolderABalanceBefore = await l2TokenRebasable.balanceOf( @@ -401,7 +401,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); const l2TotalSupplyBefore = await l2TokenRebasable.totalSupply(); - const tx = await l2ERC20TokenBridge + const tx = await l2ERC20ExtendedTokensBridge .connect(tokenHolderA.l2Signer) .withdraw( l2TokenRebasable.address, @@ -410,7 +410,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "WithdrawalInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -437,7 +437,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2CrossDomainMessenger, l2TokenRebasable, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; const { withdrawalAmountNonRebasable, withdrawalAmountRebasable } = ctx.common; @@ -445,13 +445,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); await l1CrossDomainMessenger .connect(l1Stranger) - .setXDomainMessageSender(l2ERC20TokenBridge.address); + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); const tx = await l1CrossDomainMessenger .connect(l1Stranger) @@ -483,7 +483,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) ); assert.equalBN( @@ -501,7 +501,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2TokenRebasable, l1CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l1Provider } = ctx; const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; @@ -518,7 +518,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); @@ -544,7 +544,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) dataToSend, ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1TokenRebasable.address, @@ -559,7 +559,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, @@ -568,7 +568,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmountNonRebasable) + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmountNonRebasable) ); assert.equalBN( @@ -584,7 +584,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2TokenRebasable, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l2Provider } = ctx; @@ -612,10 +612,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .relayMessage( 1, l1LidoTokensBridge.address, - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -626,7 +626,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderA.address, @@ -647,7 +647,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .step("L2 -> L1 withdrawal via withdrawTo()", async (ctx) => { - const { l1TokenRebasable, l2TokenRebasable, l2ERC20TokenBridge } = ctx; + const { l1TokenRebasable, l2TokenRebasable, l2ERC20ExtendedTokensBridge } = ctx; const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; const { exchangeRate } = ctx.common; @@ -659,7 +659,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) ); const l2TotalSupplyBefore = await l2TokenRebasable.totalSupply(); - const tx = await l2ERC20TokenBridge + const tx = await l2ERC20ExtendedTokensBridge .connect(tokenHolderB.l2Signer) .withdrawTo( l2TokenRebasable.address, @@ -669,7 +669,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); - await assert.emits(l2ERC20TokenBridge, tx, "WithdrawalInitiated", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "WithdrawalInitiated", [ l1TokenRebasable.address, l2TokenRebasable.address, tokenHolderB.address, @@ -697,7 +697,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2CrossDomainMessenger, l2TokenRebasable, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, @@ -712,13 +712,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); await l1CrossDomainMessenger .connect(l1Stranger) - .setXDomainMessageSender(l2ERC20TokenBridge.address); + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); const tx = await l1CrossDomainMessenger .connect(l1Stranger) @@ -750,7 +750,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmountNonRebasable) ); assert.equalBN( @@ -767,8 +767,8 @@ async function ctxFactory() { const { l1Provider, l2Provider, - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, ...contracts } = await optimism.testing(networkName).getIntegrationTestSetup(); @@ -794,13 +794,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 ); @@ -828,8 +828,8 @@ async function ctxFactory() { accountA, accountB, l1Stranger: testing.accounts.stranger(l1Provider), - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, l1CrossDomainMessengerAliased, }, common: { diff --git a/test/optimism/bridging-to.e2e.test.ts b/test/optimism/bridging-to.e2e.test.ts index 5c9788ed..13d9833f 100644 --- a/test/optimism/bridging-to.e2e.test.ts +++ b/test/optimism/bridging-to.e2e.test.ts @@ -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, @@ -146,7 +146,7 @@ async function ctxFactory() { l1Token: testingSetup.l1Token, l2Token: testingSetup.l2Token, l1LidoTokensBridge: testingSetup.l1LidoTokensBridge, - l2ERC20TokenBridge: testingSetup.l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge: testingSetup.l2ERC20ExtendedTokensBridge, crossChainMessenger: new CrossChainMessenger({ l2ChainId: network.chainId("opt", networkName), l1ChainId: network.chainId("eth", networkName), @@ -156,7 +156,7 @@ async function ctxFactory() { LidoBridge: { Adapter: DAIBridgeAdapter, l1Bridge: testingSetup.l1LidoTokensBridge.address, - l2Bridge: testingSetup.l2ERC20TokenBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, }, }, }), diff --git a/test/optimism/bridging.e2e.test.ts b/test/optimism/bridging.e2e.test.ts index ea346bdd..c045267e 100644 --- a/test/optimism/bridging.e2e.test.ts +++ b/test/optimism/bridging.e2e.test.ts @@ -144,7 +144,7 @@ async function ctxFactory() { LidoBridge: { Adapter: LidoBridgeAdapter, l1Bridge: testingSetup.l1LidoTokensBridge.address, - l2Bridge: testingSetup.l2ERC20TokenBridge.address, + l2Bridge: testingSetup.l2ERC20ExtendedTokensBridge.address, }, }, }), diff --git a/test/optimism/bridging.integration.test.ts b/test/optimism/bridging.integration.test.ts index 57d9e5b3..6cc2a509 100644 --- a/test/optimism/bridging.integration.test.ts +++ b/test/optimism/bridging.integration.test.ts @@ -13,13 +13,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Activate bridging on L1", async (ctx) => { const { l1LidoTokensBridge } = ctx; - const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + const { l1ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { await l1LidoTokensBridge - .connect(l1ERC20TokenBridgeAdmin) + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableDeposits(); } else { console.log("L1 deposits already enabled"); @@ -30,7 +30,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) if (!isWithdrawalsEnabled) { await l1LidoTokensBridge - .connect(l1ERC20TokenBridgeAdmin) + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableWithdrawals(); } else { console.log("L1 withdrawals already enabled"); @@ -41,32 +41,32 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .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) => { @@ -75,7 +75,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2Token, l1CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA } = ctx.accounts; const { depositAmount } = ctx.common; @@ -87,7 +87,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); @@ -110,7 +110,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x", ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1Token.address, @@ -125,7 +125,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, @@ -134,7 +134,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmount) ); assert.equalBN( @@ -149,7 +149,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l2Token, l1LidoTokensBridge, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { depositAmount } = ctx.common; const { accountA: tokenHolderA, l1CrossDomainMessengerAliased } = @@ -165,10 +165,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .relayMessage( 1, l1LidoTokensBridge.address, - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -179,7 +179,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -200,18 +200,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, @@ -236,7 +236,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2CrossDomainMessenger, l2Token, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, l1Stranger } = ctx.accounts; const { withdrawalAmount } = ctx.common; @@ -244,13 +244,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); await l1CrossDomainMessenger .connect(l1Stranger) - .setXDomainMessageSender(l2ERC20TokenBridge.address); + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); const tx = await l1CrossDomainMessenger .connect(l1Stranger) @@ -282,7 +282,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmount) ); assert.equalBN( @@ -296,7 +296,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1Token, l2Token, l1LidoTokensBridge, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, l1CrossDomainMessenger, } = ctx; const { accountA: tokenHolderA, accountB: tokenHolderB } = ctx.accounts; @@ -311,7 +311,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); @@ -335,7 +335,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x", ]); - const l2DepositCalldata = l2ERC20TokenBridge.interface.encodeFunctionData( + const l2DepositCalldata = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( "finalizeDeposit", [ l1Token.address, @@ -350,7 +350,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const messageNonce = await l1CrossDomainMessenger.messageNonce(); await assert.emits(l1CrossDomainMessenger, tx, "SentMessage", [ - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, l1LidoTokensBridge.address, l2DepositCalldata, messageNonce, @@ -359,7 +359,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.add(depositAmount) + l1ERC20ExtendedTokensBridgeBalanceBefore.add(depositAmount) ); assert.equalBN( @@ -374,7 +374,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2Token, l2CrossDomainMessenger, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, @@ -393,10 +393,10 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .relayMessage( 1, l1LidoTokensBridge.address, - l2ERC20TokenBridge.address, + l2ERC20ExtendedTokensBridge.address, 0, 300_000, - l2ERC20TokenBridge.interface.encodeFunctionData("finalizeDeposit", [ + l2ERC20ExtendedTokensBridge.interface.encodeFunctionData("finalizeDeposit", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -407,7 +407,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) { gasLimit: 5_000_000 } ); - await assert.emits(l2ERC20TokenBridge, tx, "DepositFinalized", [ + await assert.emits(l2ERC20ExtendedTokensBridge, tx, "DepositFinalized", [ l1Token.address, l2Token.address, tokenHolderA.address, @@ -427,7 +427,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 +436,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 +446,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, @@ -473,7 +473,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) l1LidoTokensBridge, l2CrossDomainMessenger, l2Token, - l2ERC20TokenBridge, + l2ERC20ExtendedTokensBridge, } = ctx; const { accountA: tokenHolderA, @@ -485,13 +485,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); - const l1ERC20TokenBridgeBalanceBefore = await l1Token.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1Token.balanceOf( l1LidoTokensBridge.address ); await l1CrossDomainMessenger .connect(l1Stranger) - .setXDomainMessageSender(l2ERC20TokenBridge.address); + .setXDomainMessageSender(l2ERC20ExtendedTokensBridge.address); const tx = await l1CrossDomainMessenger .connect(l1Stranger) @@ -523,7 +523,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) assert.equalBN( await l1Token.balanceOf(l1LidoTokensBridge.address), - l1ERC20TokenBridgeBalanceBefore.sub(withdrawalAmount) + l1ERC20ExtendedTokensBridgeBalanceBefore.sub(withdrawalAmount) ); assert.equalBN( @@ -540,8 +540,8 @@ async function ctxFactory() { const { l1Provider, l2Provider, - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, ...contracts } = await optimism.testing(networkName).getIntegrationTestSetup(); @@ -563,13 +563,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 +597,8 @@ async function ctxFactory() { accountA, accountB, l1Stranger: testing.accounts.stranger(l1Provider), - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, l1CrossDomainMessengerAliased, }, common: { diff --git a/test/optimism/deployment.acceptance.test.ts b/test/optimism/deployment.acceptance.test.ts index b5a7a766..ec30d93f 100644 --- a/test/optimism/deployment.acceptance.test.ts +++ b/test/optimism/deployment.acceptance.test.ts @@ -44,7 +44,7 @@ scenario("Optimism Bridge :: deployment acceptance test", ctxFactory) .step("L1 bridge :: L2 token bridge", async (ctx) => { assert.equal( await ctx.l1LidoTokensBridge.l2TokenBridge(), - ctx.l2ERC20TokenBridge.address + ctx.l2ERC20ExtendedTokensBridge.address ); }) .step("L1 Bridge :: is deposits enabled", async (ctx) => { @@ -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.L1_TOKEN_NON_REBASABLE(), 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.L2_TOKEN_NON_REBASABLE(), + await ctx.l2ERC20ExtendedTokensBridge.L2_TOKEN_NON_REBASABLE(), ctx.erc20Bridged.address ); }) .step("L2 bridge :: L1 token bridge", async (ctx) => { assert.equal( - await ctx.l2ERC20TokenBridge.l1TokenBridge(), + 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 ); }) @@ -287,9 +287,9 @@ async function ctxFactory() { 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 index ea3ae063..e6d4857e 100644 --- a/test/optimism/deposit-gas-estimation.test.ts +++ b/test/optimism/deposit-gas-estimation.test.ts @@ -13,13 +13,13 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .step("Activate bridging on L1", async (ctx) => { const { l1LidoTokensBridge } = ctx; - const { l1ERC20TokenBridgeAdmin } = ctx.accounts; + const { l1ERC20ExtendedTokensBridgeAdmin } = ctx.accounts; const isDepositsEnabled = await l1LidoTokensBridge.isDepositsEnabled(); if (!isDepositsEnabled) { await l1LidoTokensBridge - .connect(l1ERC20TokenBridgeAdmin) + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableDeposits(); } else { console.log("L1 deposits already enabled"); @@ -30,7 +30,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) if (!isWithdrawalsEnabled) { await l1LidoTokensBridge - .connect(l1ERC20TokenBridgeAdmin) + .connect(l1ERC20ExtendedTokensBridgeAdmin) .enableWithdrawals(); } else { console.log("L1 withdrawals already enabled"); @@ -41,32 +41,32 @@ scenario("Optimism :: Bridging integration test", ctxFactory) }) .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 zero tokens via depositERC20() method", async (ctx) => { @@ -83,13 +83,18 @@ scenario("Optimism :: Bridging integration test", ctxFactory) await l1TokenRebasable .connect(tokenHolderA.l1Signer) - .approve(l1LidoTokensBridge.address, 0); + .approve(l1LidoTokensBridge.address, 10); - const tokenHolderABalanceBefore = await l1TokenRebasable.balanceOf( + await l1Token + .connect(tokenHolderA.l1Signer) + .approve(l1LidoTokensBridge.address, 10); + + const tokenHolderABalanceBefore = await l1Token.balanceOf( tokenHolderA.address ); + console.log("tokenHolderABalanceBefore=",tokenHolderABalanceBefore); - const l1ERC20TokenBridgeBalanceBefore = await l1TokenRebasable.balanceOf( + const l1ERC20ExtendedTokensBridgeBalanceBefore = await l1TokenRebasable.balanceOf( l1LidoTokensBridge.address ); @@ -98,7 +103,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .depositERC20( l1Token.address, l2Token.address, - 0, + 10, 200_000, "0x" ); @@ -111,7 +116,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) .depositERC20( l1TokenRebasable.address, l2TokenRebasable.address, - 0, + 10, 200_000, "0x" ); @@ -134,8 +139,8 @@ async function ctxFactory() { const { l1Provider, l2Provider, - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, ...contracts } = await optimism.testing(networkName).getIntegrationTestSetup(); @@ -155,17 +160,21 @@ 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 ); + 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)); @@ -192,8 +201,8 @@ async function ctxFactory() { accountA, accountB, l1Stranger: testing.accounts.stranger(l1Provider), - l1ERC20TokenBridgeAdmin, - l2ERC20TokenBridgeAdmin, + l1ERC20ExtendedTokensBridgeAdmin, + l2ERC20ExtendedTokensBridgeAdmin, l1CrossDomainMessengerAliased, }, common: { 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.integration.test.ts b/test/optimism/pushingTokenRate.integration.test.ts index 36f93e83..f5d9606c 100644 --- a/test/optimism/pushingTokenRate.integration.test.ts +++ b/test/optimism/pushingTokenRate.integration.test.ts @@ -143,12 +143,15 @@ async function ctxFactory() { networkName ).oracleDeployScript( l1Token.address, + 1000, + 86400, { deployer: l1Deployer, admins: { proxy: l1Deployer.address, bridge: l1Deployer.address }, + contractsShift: 0 }, { deployer: l2Deployer, @@ -156,6 +159,7 @@ async function ctxFactory() { proxy: govBridgeExecutor.address, bridge: govBridgeExecutor.address, }, + contractsShift: 0 } ); 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/optimism/deploymentAllFromScratch.ts b/utils/optimism/deploymentAllFromScratch.ts index 6b95f1b1..8d59c629 100644 --- a/utils/optimism/deploymentAllFromScratch.ts +++ b/utils/optimism/deploymentAllFromScratch.ts @@ -9,7 +9,7 @@ import { ERC20Rebasable__factory, IERC20Metadata__factory, L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, TokenRateOracle__factory, TokenRateNotifier__factory, @@ -163,6 +163,7 @@ export default function deploymentAll( .addStep({ factory: TokenRateNotifier__factory, args: [ + l1Params.deployer.address, options?.overrides, ], afterDeploy: (c) => @@ -265,7 +266,7 @@ export default function deploymentAll( assert.equal(c.address, expectedL2TokenRebasableProxyAddress), }) .addStep({ - factory: L2ERC20TokenBridge__factory, + factory: L2ERC20ExtendedTokensBridge__factory, args: [ optAddresses.L2CrossDomainMessenger, expectedL1TokenBridgeProxyAddress, @@ -283,7 +284,7 @@ export default function deploymentAll( args: [ expectedL2TokenBridgeImplAddress, l2Params.admins.proxy, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( "initialize", [l2Params.admins.bridge] ), diff --git a/utils/optimism/deploymentBridgesAndRebasableToken.ts b/utils/optimism/deploymentBridgesAndRebasableToken.ts index 1035b7a5..1241ab24 100644 --- a/utils/optimism/deploymentBridgesAndRebasableToken.ts +++ b/utils/optimism/deploymentBridgesAndRebasableToken.ts @@ -9,7 +9,7 @@ import { ERC20Rebasable__factory, IERC20Metadata__factory, L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, } from "../../typechain"; @@ -222,7 +222,7 @@ export default function deployment( assert.equal(c.address, expectedL2TokenRebasableProxyAddress), }) .addStep({ - factory: L2ERC20TokenBridge__factory, + factory: L2ERC20ExtendedTokensBridge__factory, args: [ optAddresses.L2CrossDomainMessenger, expectedL1TokenBridgeProxyAddress, @@ -240,7 +240,7 @@ export default function deployment( args: [ expectedL2TokenBridgeImplAddress, l2Params.admins.proxy, - L2ERC20TokenBridge__factory.createInterface().encodeFunctionData( + L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( "initialize", [l2Params.admins.bridge] ), diff --git a/utils/optimism/deploymentNewImplementations.ts b/utils/optimism/deploymentNewImplementations.ts index 706dbf41..60398029 100644 --- a/utils/optimism/deploymentNewImplementations.ts +++ b/utils/optimism/deploymentNewImplementations.ts @@ -9,7 +9,7 @@ import { ERC20Rebasable__factory, IERC20Metadata__factory, L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, OssifiableProxy__factory, TokenRateOracle__factory } from "../../typechain"; @@ -197,7 +197,7 @@ export default function deploymentNewImplementations( assert.equal(c.address, expectedL2TokenRebasableProxyAddress), }) .addStep({ - factory: L2ERC20TokenBridge__factory, + factory: L2ERC20ExtendedTokensBridge__factory, args: [ optAddresses.L2CrossDomainMessenger, l1Params.tokenBridgeProxyAddress, diff --git a/utils/optimism/deploymentOracle.ts b/utils/optimism/deploymentOracle.ts index 002a9230..2d43f6b9 100644 --- a/utils/optimism/deploymentOracle.ts +++ b/utils/optimism/deploymentOracle.ts @@ -78,6 +78,7 @@ export default function deploymentOracle( .addStep({ factory: TokenRateNotifier__factory, args: [ + l1Params.deployer.address, options?.overrides, ], afterDeploy: (c) => diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index e7848efe..97de55c4 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -6,13 +6,13 @@ import { ERC20Bridged, IERC20__factory, L1LidoTokensBridge, - L2ERC20TokenBridge, + L2ERC20ExtendedTokensBridge, ERC20Bridged__factory, ERC20BridgedStub__factory, ERC20WrapperStub__factory, TokenRateOracle__factory, L1LidoTokensBridge__factory, - L2ERC20TokenBridge__factory, + L2ERC20ExtendedTokensBridge__factory, CrossDomainMessengerStub__factory, ERC20Rebasable__factory, } from "../../typechain"; @@ -58,11 +58,11 @@ export default function testing(networkName: NetworkName) { ? await loadDeployedBridges(ethProvider, optProvider) : await deployTestBridge(networkName, ethProvider, optProvider); - const [l1ERC20TokenBridgeAdminAddress] = + 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( @@ -82,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", ]); @@ -101,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 ) }; @@ -170,7 +170,7 @@ async function loadDeployedBridges( l2Token: testingUtils.env.OPT_L2_TOKEN(), l2TokenRebasable: testingUtils.env.OPT_L2_REBASABLE_TOKEN(), l1LidoTokensBridge: testingUtils.env.OPT_L1_ERC20_TOKEN_BRIDGE(), - l2ERC20TokenBridge: testingUtils.env.OPT_L2_ERC20_TOKEN_BRIDGE(), + l2ERC20ExtendedTokensBridge: testingUtils.env.OPT_L2_ERC20_TOKEN_BRIDGE(), }, l1SignerOrProvider, l2SignerOrProvider @@ -248,7 +248,7 @@ async function deployTestBridge( l2Token: optDeployScript.tokenProxyAddress, l2TokenRebasable: optDeployScript.tokenRebasableProxyAddress, l1LidoTokensBridge: ethDeployScript.bridgeProxyAddress, - l2ERC20TokenBridge: optDeployScript.tokenBridgeProxyAddress + l2ERC20ExtendedTokensBridge: optDeployScript.tokenBridgeProxyAddress }, ethProvider, optProvider @@ -262,7 +262,7 @@ function connectBridgeContracts( l2Token: string; l2TokenRebasable: string; l1LidoTokensBridge: string; - l2ERC20TokenBridge: string; + l2ERC20ExtendedTokensBridge: string; }, ethSignerOrProvider: SignerOrProvider, optSignerOrProvider: SignerOrProvider @@ -272,8 +272,8 @@ function connectBridgeContracts( addresses.l1LidoTokensBridge, ethSignerOrProvider ); - const l2ERC20TokenBridge = L2ERC20TokenBridge__factory.connect( - addresses.l2ERC20TokenBridge, + const l2ERC20ExtendedTokensBridge = L2ERC20ExtendedTokensBridge__factory.connect( + addresses.l2ERC20ExtendedTokensBridge, optSignerOrProvider ); const l2Token = ERC20Bridged__factory.connect( @@ -293,7 +293,7 @@ function connectBridgeContracts( l2Token, l2TokenRebasable, l1LidoTokensBridge, - l2ERC20TokenBridge + l2ERC20ExtendedTokensBridge }; } @@ -303,7 +303,7 @@ async function printLoadedTestConfig( l1Token: IERC20; l2Token: ERC20Bridged; l1LidoTokensBridge: L1LidoTokensBridge; - l2ERC20TokenBridge: L2ERC20TokenBridge; + l2ERC20ExtendedTokensBridge: L2ERC20ExtendedTokensBridge; }, l1TokensHolder?: Signer ) { @@ -326,7 +326,7 @@ async function printLoadedTestConfig( ` · 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/testing/e2e.ts b/utils/testing/e2e.ts index b089a24a..203633e5 100644 --- a/utils/testing/e2e.ts +++ b/utils/testing/e2e.ts @@ -8,7 +8,7 @@ export const E2E_TEST_CONTRACTS_OPTIMISM = { l1: { l1Token: "0xB82381A3fBD3FaFA77B3a7bE693342618240067b", l1LDOToken: "0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB", - l1ERC20TokenBridge: "0x4Abf633d9c0F4aEebB4C2E3213c7aa1b8505D332", + l1ERC20ExtendedTokensBridge: "0x4Abf633d9c0F4aEebB4C2E3213c7aa1b8505D332", aragonVoting: "0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A", tokenManager: "0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66", agent: "0x32A0E5828B62AAb932362a4816ae03b860b65e83", @@ -16,7 +16,7 @@ export const E2E_TEST_CONTRACTS_OPTIMISM = { }, l2: { l2Token: "0x24B47cd3A74f1799b32B2de11073764Cb1bb318B", - l2ERC20TokenBridge: "0xdBA2760246f315203F8B716b3a7590F0FFdc704a", + l2ERC20ExtendedTokensBridge: "0xdBA2760246f315203F8B716b3a7590F0FFdc704a", govBridgeExecutor: "0xf695357C66bA514150Da95b189acb37b46DDe602", }, }; From a55b5f030592edd35c253316fcfbe6d29380724f Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 15 Apr 2024 08:24:52 +0200 Subject: [PATCH 57/61] use mapping for tokens in bridges --- .../optimism/L1ERC20ExtendedTokensBridge.sol | 215 +++++++----------- contracts/optimism/L1LidoTokensBridge.sol | 4 +- .../optimism/L2ERC20ExtendedTokensBridge.sol | 156 +++++-------- .../RebasableAndNonRebasableTokens.sol | 126 ++++++++-- 4 files changed, 242 insertions(+), 259 deletions(-) diff --git a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol index 7e303a97..97046a94 100644 --- a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; @@ -6,15 +6,13 @@ 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 "./DepositDataCodec.sol"; -import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.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 @@ -24,12 +22,11 @@ abstract contract L1ERC20ExtendedTokensBridge is IL1ERC20Bridge, BridgingManager, RebasableAndNonRebasableTokens, - CrossDomainEnabled, - DepositDataCodec + CrossDomainEnabled { using SafeERC20 for IERC20; - address public immutable L2_TOKEN_BRIDGE; + 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 @@ -53,8 +50,24 @@ abstract contract L1ERC20ExtendedTokensBridge is L2_TOKEN_BRIDGE = l2TokenBridge_; } + function initialize2( + address admin_, + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) external { + BridgingManager.initialize(admin_); + RebasableAndNonRebasableTokens.initialize( + l1TokenNonRebasable_, + l1TokenRebasable_, + l2TokenNonRebasable_, + l2TokenRebasable_ + ); + } + /// @notice required to abstact a way token rate is requested. - function tokenRate() virtual internal view returns (uint256); + function tokenRate(address l1NonRebasableToken) virtual internal view returns (uint256); /// @inheritdoc IL1ERC20Bridge function l2TokenBridge() external view returns (address) { @@ -71,14 +84,19 @@ abstract contract L1ERC20ExtendedTokensBridge is ) external whenDepositsEnabled - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) + onlySupportedL1L2TokensPair(l1Token_, l2Token_) { if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } - - _depositERC20To(l1Token_, l2Token_, msg.sender, amount_, l2Gas_, data_); + uint256 rate = tokenRate(_l1NonRebasableToken(l1Token_)); + bytes memory encodedDepositData = DepositDataCodec.encodeDepositData(DepositDataCodec.DepositData({ + rate: uint96(rate), + timestamp: uint40(block.timestamp), + data: data_ + })); + _depositERC20To(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, data_); + emit ERC20DepositInitiated(l1Token_, l2Token_, msg.sender, msg.sender, amount_, encodedDepositData); } /// @inheritdoc IL1ERC20Bridge @@ -93,10 +111,18 @@ abstract contract L1ERC20ExtendedTokensBridge is external whenDepositsEnabled onlyNonZeroAccount(to_) - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) + // onlySupportedL1Token(l1Token_) + // onlySupportedL2Token(l2Token_) + onlySupportedL1L2TokensPair(l1Token_, l2Token_) { - _depositERC20To(l1Token_, l2Token_, to_, amount_, l2Gas_, data_); + uint256 rate = tokenRate(_l1NonRebasableToken(l1Token_)); + bytes memory encodedDepositData = DepositDataCodec.encodeDepositData(DepositDataCodec.DepositData({ + rate: uint96(rate), + 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 @@ -110,154 +136,71 @@ abstract contract L1ERC20ExtendedTokensBridge is ) external whenWithdrawalsEnabled - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) + onlySupportedL1L2TokensPair(l1Token_, l2Token_) onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) { - if (_isRebasableTokenFlow(l1Token_, l2Token_)) { - uint256 rebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); - IERC20(L1_TOKEN_REBASABLE).safeTransfer(to_, rebasableTokenAmount); - - emit ERC20WithdrawalFinalized( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - from_, - to_, - rebasableTokenAmount, - data_ - ); - } else if (_isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(L1_TOKEN_NON_REBASABLE).safeTransfer(to_, amount_); - - emit ERC20WithdrawalFinalized( - L1_TOKEN_NON_REBASABLE, - L2_TOKEN_NON_REBASABLE, - from_, - to_, - amount_, - data_ - ); - } - } - - function _depositERC20To( - address l1Token_, - address l2Token_, - address to_, - uint256 amount_, - uint32 l2Gas_, - bytes memory data_ - ) internal { - if (_isRebasableTokenFlow(l1Token_, l2Token_)) { - DepositData memory depositData = DepositData({ - rate: uint96(tokenRate()), - timestamp: uint40(block.timestamp), - data: data_ - }); - bytes memory encodedDepositData = encodeDepositData(depositData); - - if (amount_ == 0) { - _initiateERC20Deposit( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - msg.sender, - to_, - 0, - l2Gas_, - encodedDepositData - ); - - emit ERC20DepositInitiated( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - msg.sender, - to_, - 0, - encodedDepositData - ); - - return; - } - - IERC20(L1_TOKEN_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); - if(!IERC20(L1_TOKEN_REBASABLE).approve(L1_TOKEN_NON_REBASABLE, amount_)) { - revert ErrorRebasableTokenApprove(); - } - uint256 nonRebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).wrap(amount_); - - _initiateERC20Deposit( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - msg.sender, - to_, - nonRebasableTokenAmount, - l2Gas_, - encodedDepositData - ); - - emit ERC20DepositInitiated( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - msg.sender, - to_, - amount_, - encodedDepositData - ); - } else if (_isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20(L1_TOKEN_NON_REBASABLE).safeTransferFrom(msg.sender, address(this), amount_); - - _initiateERC20Deposit( - L1_TOKEN_NON_REBASABLE, - L2_TOKEN_NON_REBASABLE, - msg.sender, - to_, - amount_, - l2Gas_, - data_ - ); - - emit ERC20DepositInitiated( - L1_TOKEN_NON_REBASABLE, - L2_TOKEN_NON_REBASABLE, - msg.sender, - to_, - amount_, - data_ - ); + if(_isRebasable(l1Token_)) { + address l1NonRebasableToken = _getRebasableTokens()[l1Token_].pairedToken; + uint256 rebasableTokenAmount = IERC20Wrapper(l1NonRebasableToken).unwrap(amount_); + IERC20(l1Token_).safeTransfer(to_, rebasableTokenAmount); + emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, rebasableTokenAmount, data_); + } else { + 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 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 data_ Optional data to forward to L2. This data is provided + + /// @param encodedDepositData_ 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( + function _depositERC20To( address l1Token_, address l2Token_, address from_, address to_, uint256 amount_, uint32 l2Gas_, - bytes memory data_ + bytes memory encodedDepositData_ ) internal { + uint256 amountToDeposit = _transferToBridge(l1Token_, from_, amount_); + bytes memory message = abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, - l1Token_, - l2Token_, - from_, - to_, - amount_, - data_ + l1Token_, l2Token_, from_, to_, amountToDeposit, encodedDepositData_ ); sendCrossDomainMessage(L2_TOKEN_BRIDGE, l2Gas_, message); } + function _transferToBridge( + address l1Token_, + address from_, + uint256 amount_ + ) internal returns (uint256) { + + if (amount_ == 0) { + return amount_; + } + + IERC20(l1Token_).safeTransferFrom(from_, address(this), amount_); + if(_isRebasable(l1Token_)) { + address l1NonRebasableToken = _getRebasableTokens()[l1Token_].pairedToken; + if(!IERC20(l1Token_).approve(l1NonRebasableToken, amount_)) revert ErrorRebasableTokenApprove(); + return IERC20Wrapper(l1NonRebasableToken).wrap(amount_); + } + return amount_; + } + error ErrorSenderNotEOA(); error ErrorRebasableTokenApprove(); } diff --git a/contracts/optimism/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol index 491ff28b..85809780 100644 --- a/contracts/optimism/L1LidoTokensBridge.sol +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -27,7 +27,7 @@ contract L1LidoTokensBridge is L1ERC20ExtendedTokensBridge { ) { } - function tokenRate() override internal view returns (uint256) { - return IERC20WstETH(L1_TOKEN_NON_REBASABLE).stEthPerToken(); + function tokenRate(address l1NonRebasableToken) override internal view returns (uint256) { + return IERC20WstETH(l1NonRebasableToken).stEthPerToken(); } } diff --git a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol index f2b630be..88caab8d 100644 --- a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol @@ -5,18 +5,16 @@ 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 {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; import {IL2ERC20Bridge} from "./interfaces/IL2ERC20Bridge.sol"; import {IERC20Bridged} from "../token/interfaces/IERC20Bridged.sol"; import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; - import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; -import {DepositDataCodec} from "./DepositDataCodec.sol"; +import {DepositDataCodec} from "../lib/DepositDataCodec.sol"; /// @author psirex /// @notice The L2 token bridge works with the L1 token bridge to enable ERC20 token bridging @@ -28,8 +26,7 @@ contract L2ERC20ExtendedTokensBridge is IL2ERC20Bridge, BridgingManager, RebasableAndNonRebasableTokens, - CrossDomainEnabled, - DepositDataCodec + CrossDomainEnabled { using SafeERC20 for IERC20; @@ -68,8 +65,12 @@ contract L2ERC20ExtendedTokensBridge is uint256 amount_, uint32 l1Gas_, bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _withdrawTo(l2Token_, msg.sender, amount_, l1Gas_, data_); + ) external + whenWithdrawalsEnabled + onlySupportedL2Token(l2Token_) + { + _withdrawTo(l2Token_, msg.sender, msg.sender, amount_, l1Gas_, data_); + emit WithdrawalInitiated(_l1Token(l2Token_), l2Token_, msg.sender, msg.sender, amount_, data_); } /// @inheritdoc IL2ERC20Bridge @@ -79,8 +80,12 @@ contract L2ERC20ExtendedTokensBridge is uint256 amount_, uint32 l1Gas_, bytes calldata data_ - ) external whenWithdrawalsEnabled onlySupportedL2Token(l2Token_) { - _withdrawTo(l2Token_, to_, amount_, l1Gas_, 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 @@ -93,90 +98,16 @@ contract L2ERC20ExtendedTokensBridge is bytes calldata data_ ) external - whenDepositsEnabled - onlySupportedL1Token(l1Token_) - onlySupportedL2Token(l2Token_) + whenDepositsEnabled() + onlySupportedL1L2TokensPair(l1Token_, l2Token_) onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) { - if (_isRebasableTokenFlow(l1Token_, l2Token_)) { - DepositData memory depositData = decodeDepositData(data_); - - ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); - tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); - - ERC20Rebasable(L2_TOKEN_REBASABLE).bridgeMintShares(to_, amount_); - - uint256 rebasableTokenAmount = ERC20Rebasable(L2_TOKEN_REBASABLE).getTokensByShares(amount_); - emit DepositFinalized( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - from_, - to_, - rebasableTokenAmount, - depositData.data - ); - } else if (_isNonRebasableTokenFlow(l1Token_, l2Token_)) { - IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeMint(to_, amount_); - emit DepositFinalized( - L1_TOKEN_NON_REBASABLE, - L2_TOKEN_NON_REBASABLE, - from_, - to_, - amount_, - data_ - ); - } - } + DepositDataCodec.DepositData memory depositData = DepositDataCodec.decodeDepositData(data_); + ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2Token_).TOKEN_RATE_ORACLE(); + tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); - function _withdrawTo( - address l2Token_, - address to_, - uint256 amount_, - uint32 l1Gas_, - bytes calldata data_ - ) internal { - if (l2Token_ == L2_TOKEN_REBASABLE) { - uint256 shares = ERC20Rebasable(L2_TOKEN_REBASABLE).getSharesByTokens(amount_); - ERC20Rebasable(L2_TOKEN_REBASABLE).bridgeBurnShares(msg.sender, shares); - - _initiateWithdrawal( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - msg.sender, - to_, - shares, - l1Gas_, - data_ - ); - emit WithdrawalInitiated( - L1_TOKEN_REBASABLE, - L2_TOKEN_REBASABLE, - msg.sender, - to_, - amount_, - data_ - ); - } else if (l2Token_ == L2_TOKEN_NON_REBASABLE) { - IERC20Bridged(L2_TOKEN_NON_REBASABLE).bridgeBurn(msg.sender, amount_); - - _initiateWithdrawal( - L1_TOKEN_NON_REBASABLE, - L2_TOKEN_NON_REBASABLE, - msg.sender, - to_, - amount_, - l1Gas_, - data_ - ); - emit WithdrawalInitiated( - L1_TOKEN_NON_REBASABLE, - L2_TOKEN_NON_REBASABLE, - msg.sender, - to_, - amount_, - data_ - ); - } + 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 @@ -188,25 +119,50 @@ contract L2ERC20ExtendedTokensBridge is /// @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 l1Token_, + function _withdrawTo( address l2Token_, address from_, address to_, uint256 amount_, uint32 l1Gas_, - bytes memory data_ + bytes calldata data_ ) internal { + uint256 amountToWithdraw = _burnTokens(l2Token_, from_, amount_); + bytes memory message = abi.encodeWithSelector( IL1ERC20Bridge.finalizeERC20Withdrawal.selector, - l1Token_, - l2Token_, - from_, - to_, - amount_, - data_ + _l1Token(l2Token_), l2Token_, from_, to_, amountToWithdraw, data_ ); - sendCrossDomainMessage(L1_TOKEN_BRIDGE, l1Gas_, message); } + + function _mintTokens( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_ + ) internal returns (uint256) { + if(_isRebasable(l1Token_)) { + ERC20Rebasable(l2Token_).bridgeMintShares(to_, amount_); + return ERC20Rebasable(l2Token_).getTokensByShares(amount_); + } + + IERC20Bridged(l2Token_).bridgeMint(to_, amount_); + return amount_; + } + + function _burnTokens( + address l2Token_, + address from_, + uint256 amount_ + ) internal returns (uint256) { + if(_isRebasable(l2Token_)) { + uint256 shares = ERC20Rebasable(l2Token_).getSharesByTokens(amount_); + ERC20Rebasable(l2Token_).bridgeBurnShares(from_, shares); + return shares; + } + + IERC20Bridged(l2Token_).bridgeBurn(from_, amount_); + return amount_; + } } diff --git a/contracts/optimism/RebasableAndNonRebasableTokens.sol b/contracts/optimism/RebasableAndNonRebasableTokens.sol index bffff151..45313fc6 100644 --- a/contracts/optimism/RebasableAndNonRebasableTokens.sol +++ b/contracts/optimism/RebasableAndNonRebasableTokens.sol @@ -1,37 +1,113 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; -/// @author psirex +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; + using UnstructuredRefStorage for bytes32; + + /// @dev Servers for pairing tokens by one-layer and wrapping. + /// @param `oppositeLayerToken` token representation on opposite layer. + /// @param `pairedToken` paired token address on the same domain. + struct TokenInfo { + address oppositeLayerToken; + address pairedToken; + } - /// @notice Address of the bridged rebasable token in the L1 chain - address public immutable L1_TOKEN_REBASABLE; + bytes32 internal constant REBASABLE_TOKENS_POSITION = keccak256("RebasableAndNonRebasableTokens.REBASABLE_TOKENS_POSITION"); + bytes32 internal constant NON_REBASABLE_TOKENS_POSITION = keccak256("RebasableAndNonRebasableTokens.NON_REBASABLE_TOKENS_POSITION"); + + function _getRebasableTokens() internal pure returns (mapping(address => TokenInfo) storage) { + return _storageMapAddressTokenInfo(REBASABLE_TOKENS_POSITION); + } - /// @notice Address of the non rebasable token minted on the L2 chain when token bridged - address public immutable L2_TOKEN_NON_REBASABLE; + function _getNonRebasableTokens() internal pure returns (mapping(address => TokenInfo) storage) { + return _storageMapAddressTokenInfo(REBASABLE_TOKENS_POSITION); + } - /// @notice Address of the rebasable token minted on the L2 chain when token bridged - address public immutable L2_TOKEN_REBASABLE; + function _storageMapAddressTokenInfo(bytes32 _position) internal pure returns ( + mapping(address => TokenInfo) storage result + ) { + assembly { result.slot := _position } + } /// @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_; + constructor( + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) { + _getRebasableTokens()[l1TokenRebasable_] = TokenInfo({ + oppositeLayerToken: l2TokenRebasable_, + pairedToken: l1TokenNonRebasable_ + }); + _getRebasableTokens()[l2TokenRebasable_] = TokenInfo({ + oppositeLayerToken: l1TokenRebasable_, + pairedToken: l2TokenNonRebasable_ + }); + _getNonRebasableTokens()[l1TokenNonRebasable_] = TokenInfo({ + oppositeLayerToken: l2TokenNonRebasable_, + pairedToken: l1TokenRebasable_ + }); + _getNonRebasableTokens()[l2TokenNonRebasable_] = TokenInfo({ + oppositeLayerToken: l1TokenNonRebasable_, + pairedToken: l2TokenRebasable_ + }); + } + + function initialize( + address l1TokenNonRebasable_, + address l1TokenRebasable_, + address l2TokenNonRebasable_, + address l2TokenRebasable_ + ) public { + _getRebasableTokens()[l1TokenRebasable_] = TokenInfo({ + oppositeLayerToken: l2TokenRebasable_, + pairedToken: l1TokenNonRebasable_ + }); + _getRebasableTokens()[l2TokenRebasable_] = TokenInfo({ + oppositeLayerToken: l1TokenRebasable_, + pairedToken: l2TokenNonRebasable_ + }); + _getNonRebasableTokens()[l1TokenNonRebasable_] = TokenInfo({ + oppositeLayerToken: l2TokenNonRebasable_, + pairedToken: l1TokenRebasable_ + }); + _getNonRebasableTokens()[l2TokenNonRebasable_] = TokenInfo({ + oppositeLayerToken: l1TokenNonRebasable_, + pairedToken: l2TokenRebasable_ + }); + } + + /// @dev Validates that passed l1Token_ and l2Token_ tokens pair is supported by the bridge. + modifier onlySupportedL1L2TokensPair(address l1Token_, address l2Token_) { + if (_getRebasableTokens()[l1Token_].oppositeLayerToken == address(0) && + _getNonRebasableTokens()[l1Token_].oppositeLayerToken == address(0)) { + revert ErrorUnsupportedL1Token(); + } + if (_getRebasableTokens()[l2Token_].oppositeLayerToken == address(0) && + _getNonRebasableTokens()[l2Token_].oppositeLayerToken == address(0)) { + revert ErrorUnsupportedL2Token(); + } + if (_getRebasableTokens()[l1Token_].oppositeLayerToken != l2Token_ && + _getNonRebasableTokens()[l2Token_].oppositeLayerToken != l1Token_) { + revert ErrorUnsupportedL1L2TokensPair(); + } + _; } /// @dev Validates that passed l1Token_ is supported by the bridge modifier onlySupportedL1Token(address l1Token_) { - if (l1Token_ != L1_TOKEN_NON_REBASABLE && l1Token_ != L1_TOKEN_REBASABLE) { + if (_getRebasableTokens()[l1Token_].oppositeLayerToken == address(0) && + _getNonRebasableTokens()[l1Token_].oppositeLayerToken == address(0)) { revert ErrorUnsupportedL1Token(); } _; @@ -39,7 +115,8 @@ contract RebasableAndNonRebasableTokens { /// @dev Validates that passed l2Token_ is supported by the bridge modifier onlySupportedL2Token(address l2Token_) { - if (l2Token_ != L2_TOKEN_NON_REBASABLE && l2Token_ != L2_TOKEN_REBASABLE) { + if (_getRebasableTokens()[l2Token_].oppositeLayerToken == address(0) && + _getNonRebasableTokens()[l2Token_].oppositeLayerToken == address(0)) { revert ErrorUnsupportedL2Token(); } _; @@ -53,15 +130,22 @@ contract RebasableAndNonRebasableTokens { _; } - function _isRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { - return l1Token_ == L1_TOKEN_REBASABLE && l2Token_ == L2_TOKEN_REBASABLE; + function _isRebasable(address token_) internal view returns (bool) { + return _getRebasableTokens()[token_].oppositeLayerToken != address(0); + } + + function _l1Token(address l2Token_) internal view returns (address) { + return _isRebasable(l2Token_) ? + _getRebasableTokens()[l2Token_].oppositeLayerToken : + _getNonRebasableTokens()[l2Token_].oppositeLayerToken; } - function _isNonRebasableTokenFlow(address l1Token_, address l2Token_) internal view returns (bool) { - return l1Token_ == L1_TOKEN_NON_REBASABLE && l2Token_ == L2_TOKEN_NON_REBASABLE; + function _l1NonRebasableToken(address l1Token_) internal view returns (address) { + return _isRebasable(l1Token_) ? _getRebasableTokens()[l1Token_].pairedToken : l1Token_; } error ErrorUnsupportedL1Token(); error ErrorUnsupportedL2Token(); + error ErrorUnsupportedL1L2TokensPair(); error ErrorAccountIsZeroAddress(); } From f54eecdf85908d1e2ab6a37e6e71c238ae965a99 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 15 Apr 2024 15:46:28 +0200 Subject: [PATCH 58/61] fix bridge unit and integration tests, remove mapping in storing tokens in bridge --- .../optimism/L1ERC20ExtendedTokensBridge.sol | 51 +- contracts/optimism/L1LidoTokensBridge.sol | 6 +- .../optimism/L2ERC20ExtendedTokensBridge.sol | 6 +- test/optimism/L1ERC20TokenBridge.unit.test.ts | 918 --------------- test/optimism/L1LidoTokensBridge.unit.test.ts | 1014 +++++++++++++++++ ... L2ERC20ExtendedTokensBridge.unit.test.ts} | 58 +- .../bridging-rebasable.integration.test.ts | 3 +- test/optimism/bridging.integration.test.ts | 33 +- 8 files changed, 1107 insertions(+), 982 deletions(-) delete mode 100644 test/optimism/L1ERC20TokenBridge.unit.test.ts create mode 100644 test/optimism/L1LidoTokensBridge.unit.test.ts rename test/optimism/{L2ERC20TokenBridge.unit.test.ts => L2ERC20ExtendedTokensBridge.unit.test.ts} (91%) diff --git a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol index 97046a94..6a16129d 100644 --- a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol @@ -50,24 +50,8 @@ abstract contract L1ERC20ExtendedTokensBridge is L2_TOKEN_BRIDGE = l2TokenBridge_; } - function initialize2( - address admin_, - address l1TokenNonRebasable_, - address l1TokenRebasable_, - address l2TokenNonRebasable_, - address l2TokenRebasable_ - ) external { - BridgingManager.initialize(admin_); - RebasableAndNonRebasableTokens.initialize( - l1TokenNonRebasable_, - l1TokenRebasable_, - l2TokenNonRebasable_, - l2TokenRebasable_ - ); - } - /// @notice required to abstact a way token rate is requested. - function tokenRate(address l1NonRebasableToken) virtual internal view returns (uint256); + function tokenRate() virtual internal view returns (uint256); /// @inheritdoc IL1ERC20Bridge function l2TokenBridge() external view returns (address) { @@ -89,13 +73,12 @@ abstract contract L1ERC20ExtendedTokensBridge is if (Address.isContract(msg.sender)) { revert ErrorSenderNotEOA(); } - uint256 rate = tokenRate(_l1NonRebasableToken(l1Token_)); bytes memory encodedDepositData = DepositDataCodec.encodeDepositData(DepositDataCodec.DepositData({ - rate: uint96(rate), + rate: uint96(tokenRate()), timestamp: uint40(block.timestamp), data: data_ })); - _depositERC20To(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, data_); + _depositERC20To(l1Token_, l2Token_, msg.sender, msg.sender, amount_, l2Gas_, encodedDepositData); emit ERC20DepositInitiated(l1Token_, l2Token_, msg.sender, msg.sender, amount_, encodedDepositData); } @@ -111,13 +94,10 @@ abstract contract L1ERC20ExtendedTokensBridge is external whenDepositsEnabled onlyNonZeroAccount(to_) - // onlySupportedL1Token(l1Token_) - // onlySupportedL2Token(l2Token_) onlySupportedL1L2TokensPair(l1Token_, l2Token_) { - uint256 rate = tokenRate(_l1NonRebasableToken(l1Token_)); bytes memory encodedDepositData = DepositDataCodec.encodeDepositData(DepositDataCodec.DepositData({ - rate: uint96(rate), + rate: uint96(tokenRate()), timestamp: uint40(block.timestamp), data: data_ })); @@ -136,12 +116,11 @@ abstract contract L1ERC20ExtendedTokensBridge is ) external whenWithdrawalsEnabled - onlySupportedL1L2TokensPair(l1Token_, l2Token_) onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) + onlySupportedL1L2TokensPair(l1Token_, l2Token_) { if(_isRebasable(l1Token_)) { - address l1NonRebasableToken = _getRebasableTokens()[l1Token_].pairedToken; - uint256 rebasableTokenAmount = IERC20Wrapper(l1NonRebasableToken).unwrap(amount_); + uint256 rebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); IERC20(l1Token_).safeTransfer(to_, rebasableTokenAmount); emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, rebasableTokenAmount, data_); } else { @@ -152,14 +131,12 @@ abstract contract L1ERC20ExtendedTokensBridge is /// @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_ 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. @@ -187,16 +164,12 @@ abstract contract L1ERC20ExtendedTokensBridge is address from_, uint256 amount_ ) internal returns (uint256) { - - if (amount_ == 0) { - return amount_; - } - - IERC20(l1Token_).safeTransferFrom(from_, address(this), amount_); - if(_isRebasable(l1Token_)) { - address l1NonRebasableToken = _getRebasableTokens()[l1Token_].pairedToken; - if(!IERC20(l1Token_).approve(l1NonRebasableToken, amount_)) revert ErrorRebasableTokenApprove(); - return IERC20Wrapper(l1NonRebasableToken).wrap(amount_); + 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_; } diff --git a/contracts/optimism/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol index 85809780..c28ed325 100644 --- a/contracts/optimism/L1LidoTokensBridge.sol +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; @@ -27,7 +27,7 @@ contract L1LidoTokensBridge is L1ERC20ExtendedTokensBridge { ) { } - function tokenRate(address l1NonRebasableToken) override internal view returns (uint256) { - return IERC20WstETH(l1NonRebasableToken).stEthPerToken(); + 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 index 88caab8d..21bc55e7 100644 --- a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; @@ -16,7 +16,7 @@ import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.s import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {DepositDataCodec} from "../lib/DepositDataCodec.sol"; -/// @author psirex +/// @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 @@ -103,7 +103,7 @@ contract L2ERC20ExtendedTokensBridge is onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) { DepositDataCodec.DepositData memory depositData = DepositDataCodec.decodeDepositData(data_); - ITokenRateOracle tokenRateOracle = ERC20Rebasable(l2Token_).TOKEN_RATE_ORACLE(); + ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); uint256 depositedAmount = _mintTokens(l1Token_, l2Token_, to_, amount_); diff --git a/test/optimism/L1ERC20TokenBridge.unit.test.ts b/test/optimism/L1ERC20TokenBridge.unit.test.ts deleted file mode 100644 index d70705db..00000000 --- a/test/optimism/L1ERC20TokenBridge.unit.test.ts +++ /dev/null @@ -1,918 +0,0 @@ -import { assert } from "chai"; -import hre, { ethers } from "hardhat"; -import { - ERC20BridgedStub__factory, - ERC20WrapperStub__factory, - L1LidoTokensBridge__factory, - L2ERC20ExtendedTokensBridge__factory, - OssifiableProxy__factory, - EmptyContractStub__factory, -} 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"; -import { BigNumber } from "ethers"; -import { ERC20WrapperStub } from "../../typechain"; - -unit("Optimism :: L1LidoTokensBridge", 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.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("depositsERC20() :: 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("depositsERC20() :: 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() :: 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 - ); - - await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ - l1TokenNonRebasable.address, - l2TokenNonRebasable.address, - deployer.address, - deployer.address, - amount, - data, - ]); - - await assert.emits(l1Messenger, tx, "SentMessage", [ - l2TokenBridgeEOA.address, - l1TokenBridge.address, - L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( - "finalizeDeposit", - [ - l1TokenNonRebasable.address, - l2TokenNonRebasable.address, - deployer.address, - deployer.address, - amount, - data, - ] - ), - 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("depositsERC20To() :: wrong l1Token address", async (ctx) => { - const { - l1TokenBridge, - stubs: { l2TokenNonRebasable, l2TokenRebasable }, - accounts: { recipient, stranger }, - } = ctx; - await l1TokenBridge.disableDeposits(); - - assert.isFalse(await l1TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - l1TokenBridge.depositERC20To( - stranger.address, - l2TokenNonRebasable.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - await assert.revertsWith( - l1TokenBridge.depositERC20To( - stranger.address, - l2TokenRebasable.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("depositsERC20To() :: wrong l2Token address", async (ctx) => { - const { - l1TokenBridge, - stubs: { l1TokenNonRebasable, l1TokenRebasable }, - accounts: { recipient, stranger }, - } = ctx; - await l1TokenBridge.disableDeposits(); - - assert.isFalse(await l1TokenBridge.isDepositsEnabled()); - - await assert.revertsWith( - l1TokenBridge.depositERC20To( - l1TokenNonRebasable.address, - stranger.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - await assert.revertsWith( - l1TokenBridge.depositERC20To( - l1TokenRebasable.address, - stranger.address, - recipient.address, - wei`1 ether`, - wei`1 gwei`, - "0x" - ), - "ErrorDepositsDisabled()" - ); - }) - - .test("depositsERC20To() :: 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 - ); - - await assert.emits(l1TokenBridge, tx, "ERC20DepositInitiated", [ - l1TokenNonRebasable.address, - l2TokenNonRebasable.address, - deployer.address, - recipient.address, - amount, - data, - ]); - - await assert.emits(l1Messenger, tx, "SentMessage", [ - l2TokenBridgeEOA.address, - l1TokenBridge.address, - L2ERC20ExtendedTokensBridge__factory.createInterface().encodeFunctionData( - "finalizeDeposit", - [ - l1TokenNonRebasable.address, - l2TokenNonRebasable.address, - deployer.address, - recipient.address, - amount, - data, - ] - ), - 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 }, - accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, - } = ctx; - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .finalizeERC20Withdrawal( - stranger.address, - l2TokenNonRebasable.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL1Token()" - ); - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .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 }, - accounts: { deployer, recipient, l2TokenBridgeEOA, stranger }, - } = ctx; - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .finalizeERC20Withdrawal( - l1TokenNonRebasable.address, - stranger.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL2Token()" - ); - - await assert.revertsWith( - l1TokenBridge - .connect(l2TokenBridgeEOA) - .finalizeERC20Withdrawal( - l1TokenRebasable.address, - stranger.address, - deployer.address, - recipient.address, - wei`1 ether`, - "0x" - ), - "ErrorUnsupportedL2Token()" - ); - }) - - .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 bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); - - const amount = wei`1 ether`; - const data = "0xdeadbeaf"; - - 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; - - 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 deployerBalanceBefore = await l1TokenRebasable.balanceOf(deployer.address); - - await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); - - await l1TokenRebasable.transfer(l1TokenNonRebasable.address, wei`100 ether`); - - 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) - ); - }) - - .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/L1LidoTokensBridge.unit.test.ts b/test/optimism/L1LidoTokensBridge.unit.test.ts new file mode 100644 index 00000000..15642120 --- /dev/null +++ b/test/optimism/L1LidoTokensBridge.unit.test.ts @@ -0,0 +1,1014 @@ +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 bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); + + const amount = wei`1 ether`; + const data = "0xdeadbeaf"; + + 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; + + 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 deployerBalanceBefore = await l1TokenRebasable.balanceOf(deployer.address); + + await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); + + await l1TokenRebasable.transfer(l1TokenNonRebasable.address, wei`100 ether`); + + 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) + ); + }) + + .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/L2ERC20TokenBridge.unit.test.ts b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts similarity index 91% rename from test/optimism/L2ERC20TokenBridge.unit.test.ts rename to test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts index 5ea2dc66..d84ff111 100644 --- a/test/optimism/L2ERC20TokenBridge.unit.test.ts +++ b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts @@ -19,11 +19,13 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { BigNumber } from "ethers"; unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) - .test("l1TokenBridge()", async (ctx) => { - assert.equal( - await ctx.l2TokenBridge.l1TokenBridge(), - ctx.accounts.l1TokenBridgeEOA.address - ); + .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) => { @@ -68,7 +70,7 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) ); }) - .test("withdraw() :: non rebasable token flow", async (ctx) => { + .test("withdraw() :: non-rebasable token flow", async (ctx) => { const { l2TokenBridge, accounts: { deployer, l1TokenBridgeEOA }, @@ -137,8 +139,6 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) accounts: { deployer, l1TokenBridgeEOA, l2MessengerStubEOA, recipient }, stubs: { l2Messenger, - l1TokenNonRebasable, - l2TokenNonRebasable, l1TokenRebasable, l2TokenRebasable }, @@ -514,6 +514,41 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) ); }) + .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, @@ -587,7 +622,7 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) ); }) - .test("finalizeDeposit() :: non rebasable token flow", async (ctx) => { + .test("finalizeDeposit() :: non-rebasable token flow", async (ctx) => { const { l2TokenBridge, stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l2Messenger }, @@ -600,6 +635,9 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) 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) @@ -609,7 +647,7 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) deployer.address, recipient.address, amount, - data + dataToReceive ); await assert.emits(l2TokenBridge, tx, "DepositFinalized", [ diff --git a/test/optimism/bridging-rebasable.integration.test.ts b/test/optimism/bridging-rebasable.integration.test.ts index fbe208ae..905b6b36 100644 --- a/test/optimism/bridging-rebasable.integration.test.ts +++ b/test/optimism/bridging-rebasable.integration.test.ts @@ -5,11 +5,10 @@ import { wei } from "../../utils/wei"; import optimism from "../../utils/optimism"; import testing, { scenario } from "../../utils/testing"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { ERC20WrapperStub } from "../../typechain"; -scenario("Optimism :: Bridging integration test", ctxFactory) +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]); diff --git a/test/optimism/bridging.integration.test.ts b/test/optimism/bridging.integration.test.ts index 6cc2a509..9ec87c7d 100644 --- a/test/optimism/bridging.integration.test.ts +++ b/test/optimism/bridging.integration.test.ts @@ -4,8 +4,11 @@ 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]); @@ -101,13 +104,15 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); + 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 = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( @@ -118,7 +123,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x", + dataToSend, ] ); @@ -159,6 +164,7 @@ 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) @@ -174,7 +180,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderA.address, depositAmount, - "0x", + dataToReceive, ]), { gasLimit: 5_000_000 } ); @@ -326,13 +332,15 @@ scenario("Optimism :: Bridging integration test", ctxFactory) "0x" ); + 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 = l2ERC20ExtendedTokensBridge.interface.encodeFunctionData( @@ -343,7 +351,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderB.address, depositAmount, - "0x", + dataToSend, ] ); @@ -388,6 +396,8 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderB.address ); + const dataToReceive = await packedTokenRateAndTimestamp(ctx.l2Provider, l1Token); + const tx = await l2CrossDomainMessenger .connect(l1CrossDomainMessengerAliased) .relayMessage( @@ -402,7 +412,7 @@ scenario("Optimism :: Bridging integration test", ctxFactory) tokenHolderA.address, tokenHolderB.address, depositAmount, - "0x", + dataToReceive, ]), { gasLimit: 5_000_000 } ); @@ -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]); +} From bc1c56fbb5b539348c73a41650a2db90660fad61 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 15 Apr 2024 23:59:42 +0200 Subject: [PATCH 59/61] fix scripts --- .env.example | 5 +- .storage-layout | 8 +-- README.md | 4 +- artifacts-opt.json | 8 +-- contracts/arbitrum/L2ERC20TokenGateway.sol | 2 +- contracts/arbitrum/README.md | 28 ++++++++ scripts/optimism/deploy-new-impls.ts | 2 +- scripts/optimism/deploy-oracle.ts | 2 +- .../L2ERC20ExtendedTokensBridge.unit.test.ts | 31 +++++++++ test/optimism/TokenRateNotifier.unit.test.ts | 65 ++++++++++++++++--- test/optimism/TokenRateOracle.unit.test.ts | 4 +- test/token/ERC20Rebasable.unit.test.ts | 2 +- utils/deployment.ts | 4 +- utils/optimism/deploymentOracle.ts | 4 +- 14 files changed, 140 insertions(+), 29 deletions(-) diff --git a/.env.example b/.env.example index b683fcb0..3838caf4 100644 --- a/.env.example +++ b/.env.example @@ -35,10 +35,13 @@ REBASABLE_TOKEN= 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. -RATE_OUTDATED_DELAY=86400 # default is 24 hours +TOKEN_RATE_OUTDATED_DELAY=86400 # default is 86400 (24 hours) # Address of L1 token bridge proxy. L1_TOKEN_BRIDGE= 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 caf32a0d..ce8ce02d 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ The configuration of the deployment scripts happens via the ENV variables. The f - [`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. -- [`RATE_OUTDATED_DELAY`](#RATE_OUTDATED_DELAY) - a time period when token rate can be considered outdated. Default is 24 hours. +- [`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. 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 26f3c4fb..4197694f 100644 --- a/contracts/arbitrum/README.md +++ b/contracts/arbitrum/README.md @@ -695,6 +695,34 @@ 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) diff --git a/scripts/optimism/deploy-new-impls.ts b/scripts/optimism/deploy-new-impls.ts index 2026c9c5..b28ea202 100644 --- a/scripts/optimism/deploy-new-impls.ts +++ b/scripts/optimism/deploy-new-impls.ts @@ -48,7 +48,7 @@ async function main() { tokenBridgeProxyAddress: deploymentConfig.l2TokenBridge, tokenProxyAddress: deploymentConfig.l2Token, tokenRateOracleProxyAddress: deploymentConfig.l2TokenRateOracle, - tokenRateOracleRateOutdatedDelay: deploymentConfig.rateOutdatedDelay, + tokenRateOracleRateOutdatedDelay: deploymentConfig.tokenRateOutdatedDelay, } ); diff --git a/scripts/optimism/deploy-oracle.ts b/scripts/optimism/deploy-oracle.ts index 7d75e0f3..08edc448 100644 --- a/scripts/optimism/deploy-oracle.ts +++ b/scripts/optimism/deploy-oracle.ts @@ -26,7 +26,7 @@ async function main() { .oracleDeployScript( deploymentConfig.l1Token, deploymentConfig.l2GasLimitForPushingTokenRate, - deploymentConfig.rateOutdatedDelay, + deploymentConfig.tokenRateOutdatedDelay, { deployer: ethDeployer, admins: { diff --git a/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts index d84ff111..53040ce7 100644 --- a/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts +++ b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts @@ -70,6 +70,37 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) ); }) + .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, diff --git a/test/optimism/TokenRateNotifier.unit.test.ts b/test/optimism/TokenRateNotifier.unit.test.ts index d8ca1bc0..c2f3864c 100644 --- a/test/optimism/TokenRateNotifier.unit.test.ts +++ b/test/optimism/TokenRateNotifier.unit.test.ts @@ -3,6 +3,7 @@ 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, @@ -17,6 +18,15 @@ import { 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; @@ -64,10 +74,17 @@ unit("TokenRateNotifier", ctxFactory) .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); @@ -82,6 +99,21 @@ unit("TokenRateNotifier", ctxFactory) ); }) + .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; @@ -204,10 +236,18 @@ unit("TokenRateNotifier", ctxFactory) .run(); -async function ctxFactory() { - const [deployer, owner, stranger, tokenRateOracle] = await ethers.getSigners(); +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" @@ -219,12 +259,6 @@ async function ctxFactory() { "L1NR" ); - const l1MessengerStub = await new CrossDomainMessengerStub__factory( - deployer - ).deploy({ value: wei.toBigNumber(wei`1 ether`) }); - - const l2GasLimitForPushingTokenRate = 123; - const opStackTokenRatePusher = await new OpStackTokenRatePusher__factory(deployer).deploy( l1MessengerStub.address, l1TokenNonRebasableStub.address, @@ -232,6 +266,21 @@ async function ctxFactory() { 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 }, diff --git a/test/optimism/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index 6851237d..ca4d7b02 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -14,9 +14,9 @@ unit("TokenRateOracle", ctxFactory) const { bridge, l1TokenBridgeEOA } = ctx.accounts; assert.equal(await tokenRateOracle.MESSENGER(), l2MessengerStub.address); - assert.equal(await tokenRateOracle.BRIDGE(), bridge.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.RATE_OUTDATED_DELAY(), 86400); + assert.equalBN(await tokenRateOracle.TOKEN_RATE_OUTDATED_DELAY(), 86400); assert.equalBN(await tokenRateOracle.latestAnswer(), 0); diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 84f7c223..7517f3f0 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -16,7 +16,7 @@ unit("ERC20Rebasable", ctxFactory) .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { const { rebasableProxied, wrappedToken } = ctx.contracts; - assert.equal(await rebasableProxied.WRAPPED_TOKEN(), wrappedToken.address) + assert.equal(await rebasableProxied.TOKEN_TO_WRAP_FROM(), wrappedToken.address) }) .test("tokenRateOracle() :: has the same address is in constructor", async (ctx) => { diff --git a/utils/deployment.ts b/utils/deployment.ts index e820bd8f..d9037d42 100644 --- a/utils/deployment.ts +++ b/utils/deployment.ts @@ -14,7 +14,7 @@ interface MultiChainDeploymentConfig { l1RebasableToken: string; l1OpStackTokenRatePusher: string; l2GasLimitForPushingTokenRate: number; - rateOutdatedDelay: number; + tokenRateOutdatedDelay: number; l1TokenBridge: string; l2TokenBridge: string; l2Token: string; @@ -30,7 +30,7 @@ export function loadMultiChainDeploymentConfig(): MultiChainDeploymentConfig { 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")), - rateOutdatedDelay: Number(env.string("RATE_OUTDATED_DELAY")), + 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"), diff --git a/utils/optimism/deploymentOracle.ts b/utils/optimism/deploymentOracle.ts index 2d43f6b9..5be8d466 100644 --- a/utils/optimism/deploymentOracle.ts +++ b/utils/optimism/deploymentOracle.ts @@ -54,7 +54,7 @@ export default function deploymentOracle( async oracleDeployScript( l1Token: string, l2GasLimitForPushingTokenRate: number, - rateOutdatedDelay: number, + tokenRateOutdatedDelay: number, l1Params: OptDeployScriptParams, l2Params: OptDeployScriptParams, ): Promise<[OracleL1DeployScript, OracleL2DeployScript]> { @@ -109,7 +109,7 @@ export default function deploymentOracle( optAddresses.L2CrossDomainMessenger, ethers.constants.AddressZero, expectedL1OpStackTokenRatePusherImplAddress, - rateOutdatedDelay, + tokenRateOutdatedDelay, options?.overrides, ], afterDeploy: (c) => From b62fbeeed79ee93e7afff3092396451ed7c1fa6e Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 16 Apr 2024 00:05:07 +0200 Subject: [PATCH 60/61] PR fixes: add comments, rename constracts, more interfaces to contract files --- .../{optimism => lib}/DepositDataCodec.sol | 2 +- contracts/lib/ECDSA.sol | 57 ++++++++++ contracts/lib/SignatureChecker.sol | 84 ++++++-------- contracts/lido/TokenRateNotifier.sol | 14 ++- ...ckTokenRatePusherWithOutOfGasErrorStub.sol | 1 + ...pStackTokenRatePusherWithSomeErrorStub.sol | 3 +- .../optimism/L1ERC20ExtendedTokensBridge.sol | 13 +-- contracts/optimism/L1LidoTokensBridge.sol | 9 +- .../optimism/L2ERC20ExtendedTokensBridge.sol | 14 ++- contracts/optimism/OpStackTokenRatePusher.sol | 12 +- contracts/optimism/README.md | 2 +- .../RebasableAndNonRebasableTokens.sol | 105 +++++------------- contracts/optimism/TokenRateOracle.sol | 57 +++++----- .../IChainlinkAggregatorInterface.sol} | 10 +- .../interfaces/ITokenRateUpdatable.sol | 13 +++ .../stubs/CrossDomainMessengerStub.sol | 1 + contracts/stubs/ERC1271PermitSignerMock.sol | 2 +- contracts/stubs/ERC20BridgedStub.sol | 5 +- contracts/stubs/ERC20WrapperStub.sol | 8 +- contracts/stubs/EmptyContractStub.sol | 3 +- contracts/token/ERC20Bridged.sol | 24 +++- contracts/token/ERC20BridgedPermit.sol | 7 +- contracts/token/ERC20Metadata.sol | 13 ++- contracts/token/ERC20Rebasable.sol | 57 ++++++---- contracts/token/ERC20RebasablePermit.sol | 7 +- .../{ERC20Permit.sol => PermitExtension.sol} | 74 ++++++------ contracts/token/interfaces/IERC20Bridged.sol | 23 ---- .../token/interfaces/IERC20BridgedShares.sol | 23 ---- contracts/token/interfaces/IERC20Metadata.sol | 17 --- .../token/interfaces/IERC20TokenRate.sol | 12 -- contracts/token/interfaces/IERC20WstETH.sol | 14 --- 31 files changed, 333 insertions(+), 353 deletions(-) rename contracts/{optimism => lib}/DepositDataCodec.sol (97%) create mode 100644 contracts/lib/ECDSA.sol rename contracts/{token/interfaces/ITokenRateOracle.sol => optimism/interfaces/IChainlinkAggregatorInterface.sol} (75%) create mode 100644 contracts/optimism/interfaces/ITokenRateUpdatable.sol rename contracts/token/{ERC20Permit.sol => PermitExtension.sol} (57%) delete mode 100644 contracts/token/interfaces/IERC20Bridged.sol delete mode 100644 contracts/token/interfaces/IERC20BridgedShares.sol delete mode 100644 contracts/token/interfaces/IERC20Metadata.sol delete mode 100644 contracts/token/interfaces/IERC20TokenRate.sol delete mode 100644 contracts/token/interfaces/IERC20WstETH.sol diff --git a/contracts/optimism/DepositDataCodec.sol b/contracts/lib/DepositDataCodec.sol similarity index 97% rename from contracts/optimism/DepositDataCodec.sol rename to contracts/lib/DepositDataCodec.sol index 55178758..af8a9910 100644 --- a/contracts/optimism/DepositDataCodec.sol +++ b/contracts/lib/DepositDataCodec.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.10; /// @author kovalgek /// @notice encodes and decodes DepositData for crosschain transfering. -contract DepositDataCodec { +library DepositDataCodec { uint8 internal constant RATE_FIELD_SIZE = 12; uint8 internal constant TIMESTAMP_FIELD_SIZE = 5; 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 index e4e3ab59..183e0266 100644 --- a/contracts/lib/SignatureChecker.sol +++ b/contracts/lib/SignatureChecker.sol @@ -1,35 +1,22 @@ -// SPDX-FileCopyrightText: 2024 OpenZeppelin, Lido -// SPDX-License-Identifier: GPL-3.0 -// Written based on (utils/cryptography/SignatureChecker.sol from d398d68 +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: MIT pragma solidity 0.8.10; -import {ECDSA} from "@openzeppelin/contracts-v4.9/utils/cryptography/ECDSA.sol"; -import {IERC1271} from "@openzeppelin/contracts-v4.9/interfaces/IERC1271.sol"; +import {ECDSA} from "./ECDSA.sol"; - - -/** - * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA - * signatures from externally owned accounts (EOAs) as well as ERC-1271 signatures from smart contract wallets like - * Argent and Safe Wallet (previously Gnosis Safe). - */ +/// @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 Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the - * signature is validated against that smart contract using ERC-1271, otherwise it's validated using `ECDSA.recover`. + * @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. * - * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus - * change through time. It could return true at block N and false at block N+1 (or the opposite). + * bytes4(keccak256("isValidSignature(bytes32,bytes)") */ - function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { - if (signer.code.length == 0) { - (address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature); - return err == ECDSA.RecoverError.NoError && recovered == signer; - } else { - return isValidERC1271SignatureNow(signer, hash, signature); - } - } + bytes4 internal constant ERC1271_IS_VALID_SIGNATURE_SELECTOR = 0x1626ba7e; /** * @dev Checks signature validity. @@ -38,39 +25,40 @@ library SignatureChecker { * 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 isValidSignatureNow( + function isValidSignature( address signer, bytes32 msgHash, uint8 v, bytes32 r, bytes32 s ) internal view returns (bool) { - if (signer.code.length == 0) { - (address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(msgHash, v, r, s); - return err == ECDSA.RecoverError.NoError && recovered == signer; + 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 { - bytes memory signature = abi.encodePacked(r, s, v); - return isValidERC1271SignatureNow(signer, msgHash, signature); + return ECDSA.recover(msgHash, v, r, s) == signer; } } - /** - * @dev Checks if a signature is valid for a given signer and data hash. The signature is validated - * against the signer smart contract using ERC-1271. - * - * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus - * change through time. It could return true at block N and false at block N+1 (or the opposite). - */ - function isValidERC1271SignatureNow( - address signer, - bytes32 hash, - bytes memory signature - ) internal view returns (bool) { - (bool success, bytes memory result) = signer.staticcall( - abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) - ); - return (success && - result.length >= 32 && - abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); + 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 index 28cf18f0..24ca4c31 100644 --- a/contracts/lido/TokenRateNotifier.sol +++ b/contracts/lido/TokenRateNotifier.sol @@ -41,6 +41,9 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { /// @param initialOwner_ initial owner constructor(address initialOwner_) { + if (initialOwner_ == address(0)) { + revert ErrorZeroAddressOwner(); + } _transferOwnership(initialOwner_); } @@ -56,6 +59,9 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { if (observers.length >= MAX_OBSERVERS_COUNT) { revert ErrorMaxObserversCountExceeded(); } + if (_observerIndex(observer_) != INDEX_NOT_FOUND) { + revert ErrorAddExistedObserver(); + } observers.push(observer_); emit ObserverAdded(observer_); @@ -70,8 +76,9 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { if (observerIndexToRemove == INDEX_NOT_FOUND) { revert ErrorNoObserverToRemove(); } - - observers[observerIndexToRemove] = observers[observers.length - 1]; + if (observers.length > 1) { + observers[observerIndexToRemove] = observers[observers.length - 1]; + } observers.pop(); emit ObserverRemoved(observer_); @@ -89,6 +96,7 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { 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. @@ -131,4 +139,6 @@ contract TokenRateNotifier is Ownable, IPostTokenRebaseReceiver { error ErrorBadObserverInterface(); error ErrorMaxObserversCountExceeded(); error ErrorNoObserverToRemove(); + error ErrorZeroAddressOwner(); + error ErrorAddExistedObserver(); } diff --git a/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol index cb8d1c26..0ce7974c 100644 --- a/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithOutOfGasErrorStub.sol @@ -6,6 +6,7 @@ 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; diff --git a/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol b/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol index 5a8ddfdd..6df0b7fa 100644 --- a/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol +++ b/contracts/lido/stubs/OpStackTokenRatePusherWithSomeErrorStub.sol @@ -6,11 +6,12 @@ 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() external { + function pushTokenRate() pure external { revert SomeError(); } diff --git a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol index 6a16129d..bb67010c 100644 --- a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol @@ -119,14 +119,11 @@ abstract contract L1ERC20ExtendedTokensBridge is onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) onlySupportedL1L2TokensPair(l1Token_, l2Token_) { - if(_isRebasable(l1Token_)) { - uint256 rebasableTokenAmount = IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_); - IERC20(l1Token_).safeTransfer(to_, rebasableTokenAmount); - emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, rebasableTokenAmount, data_); - } else { - IERC20(l1Token_).safeTransfer(to_, amount_); - emit ERC20WithdrawalFinalized(l1Token_, l2Token_, from_, to_, amount_, data_); - } + uint256 amountToWithdraw = _isRebasable(l1Token_) ? + 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 diff --git a/contracts/optimism/L1LidoTokensBridge.sol b/contracts/optimism/L1LidoTokensBridge.sol index c28ed325..d749e034 100644 --- a/contracts/optimism/L1LidoTokensBridge.sol +++ b/contracts/optimism/L1LidoTokensBridge.sol @@ -4,7 +4,14 @@ pragma solidity 0.8.10; import {L1ERC20ExtendedTokensBridge} from "./L1ERC20ExtendedTokensBridge.sol"; -import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.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. diff --git a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol index 21bc55e7..38a6c834 100644 --- a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol @@ -5,10 +5,11 @@ 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/interfaces/IERC20Bridged.sol"; -import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; +import {IERC20Bridged} from "../token/ERC20Bridged.sol"; +import {ITokenRateUpdatable} from "../optimism/interfaces/ITokenRateUpdatable.sol"; import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; import {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; import {BridgingManager} from "../BridgingManager.sol"; @@ -30,7 +31,7 @@ contract L2ERC20ExtendedTokensBridge is { using SafeERC20 for IERC20; - address public immutable L1_TOKEN_BRIDGE; + 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 @@ -69,6 +70,9 @@ contract L2ERC20ExtendedTokensBridge is 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_); } @@ -103,7 +107,7 @@ contract L2ERC20ExtendedTokensBridge is onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) { DepositDataCodec.DepositData memory depositData = DepositDataCodec.decodeDepositData(data_); - ITokenRateOracle tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); + ITokenRateUpdatable tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); uint256 depositedAmount = _mintTokens(l1Token_, l2Token_, to_, amount_); @@ -165,4 +169,6 @@ contract L2ERC20ExtendedTokensBridge is IERC20Bridged(l2Token_).bridgeBurn(from_, amount_); return amount_; } + + error ErrorSenderNotEOA(); } diff --git a/contracts/optimism/OpStackTokenRatePusher.sol b/contracts/optimism/OpStackTokenRatePusher.sol index 65b06a34..52479a22 100644 --- a/contracts/optimism/OpStackTokenRatePusher.sol +++ b/contracts/optimism/OpStackTokenRatePusher.sol @@ -5,8 +5,8 @@ pragma solidity 0.8.10; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; import {ITokenRatePusher} from "../lido/interfaces/ITokenRatePusher.sol"; -import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; -import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; +import {IERC20WstETH} from "./L1LidoTokensBridge.sol"; +import {ITokenRateUpdatable} from "../optimism/interfaces/ITokenRateUpdatable.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; /// @author kovalgek @@ -19,7 +19,11 @@ contract OpStackTokenRatePusher is CrossDomainEnabled, ERC165, ITokenRatePusher /// @notice Non-rebasable token of Core Lido procotol. address public immutable WSTETH; - /// @notice Gas limit required to complete pushing token rate on L2. + /// @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 @@ -42,7 +46,7 @@ contract OpStackTokenRatePusher is CrossDomainEnabled, ERC165, ITokenRatePusher uint256 tokenRate = IERC20WstETH(WSTETH).stEthPerToken(); bytes memory message = abi.encodeWithSelector( - ITokenRateOracle.updateRate.selector, + ITokenRateUpdatable.updateRate.selector, tokenRate, block.timestamp ); diff --git a/contracts/optimism/README.md b/contracts/optimism/README.md index c493349d..6b853ba3 100644 --- a/contracts/optimism/README.md +++ b/contracts/optimism/README.md @@ -514,7 +514,7 @@ Transfers `amount` of token from the `from_` account to `to_` using the allowanc ## `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. diff --git a/contracts/optimism/RebasableAndNonRebasableTokens.sol b/contracts/optimism/RebasableAndNonRebasableTokens.sol index 45313fc6..21491d19 100644 --- a/contracts/optimism/RebasableAndNonRebasableTokens.sol +++ b/contracts/optimism/RebasableAndNonRebasableTokens.sol @@ -8,32 +8,18 @@ import {UnstructuredRefStorage} from "../token/UnstructuredRefStorage.sol"; /// @author psirex, kovalgek /// @notice Contains the logic for validation of tokens used in the bridging process contract RebasableAndNonRebasableTokens { - using UnstructuredRefStorage for bytes32; - - /// @dev Servers for pairing tokens by one-layer and wrapping. - /// @param `oppositeLayerToken` token representation on opposite layer. - /// @param `pairedToken` paired token address on the same domain. - struct TokenInfo { - address oppositeLayerToken; - address pairedToken; - } - bytes32 internal constant REBASABLE_TOKENS_POSITION = keccak256("RebasableAndNonRebasableTokens.REBASABLE_TOKENS_POSITION"); - bytes32 internal constant NON_REBASABLE_TOKENS_POSITION = keccak256("RebasableAndNonRebasableTokens.NON_REBASABLE_TOKENS_POSITION"); + /// @notice Address of the bridged non rebasable token in the L1 chain + address public immutable L1_TOKEN_NON_REBASABLE; - function _getRebasableTokens() internal pure returns (mapping(address => TokenInfo) storage) { - return _storageMapAddressTokenInfo(REBASABLE_TOKENS_POSITION); - } + /// @notice Address of the bridged rebasable token in the L1 chain + address public immutable L1_TOKEN_REBASABLE; - function _getNonRebasableTokens() internal pure returns (mapping(address => TokenInfo) storage) { - return _storageMapAddressTokenInfo(REBASABLE_TOKENS_POSITION); - } + /// @notice Address of the non rebasable token minted on the L2 chain when token bridged + address public immutable L2_TOKEN_NON_REBASABLE; - function _storageMapAddressTokenInfo(bytes32 _position) internal pure returns ( - mapping(address => TokenInfo) storage result - ) { - assembly { result.slot := _position } - } + /// @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 @@ -45,69 +31,35 @@ contract RebasableAndNonRebasableTokens { address l2TokenNonRebasable_, address l2TokenRebasable_ ) { - _getRebasableTokens()[l1TokenRebasable_] = TokenInfo({ - oppositeLayerToken: l2TokenRebasable_, - pairedToken: l1TokenNonRebasable_ - }); - _getRebasableTokens()[l2TokenRebasable_] = TokenInfo({ - oppositeLayerToken: l1TokenRebasable_, - pairedToken: l2TokenNonRebasable_ - }); - _getNonRebasableTokens()[l1TokenNonRebasable_] = TokenInfo({ - oppositeLayerToken: l2TokenNonRebasable_, - pairedToken: l1TokenRebasable_ - }); - _getNonRebasableTokens()[l2TokenNonRebasable_] = TokenInfo({ - oppositeLayerToken: l1TokenNonRebasable_, - pairedToken: l2TokenRebasable_ - }); - } - - function initialize( - address l1TokenNonRebasable_, - address l1TokenRebasable_, - address l2TokenNonRebasable_, - address l2TokenRebasable_ - ) public { - _getRebasableTokens()[l1TokenRebasable_] = TokenInfo({ - oppositeLayerToken: l2TokenRebasable_, - pairedToken: l1TokenNonRebasable_ - }); - _getRebasableTokens()[l2TokenRebasable_] = TokenInfo({ - oppositeLayerToken: l1TokenRebasable_, - pairedToken: l2TokenNonRebasable_ - }); - _getNonRebasableTokens()[l1TokenNonRebasable_] = TokenInfo({ - oppositeLayerToken: l2TokenNonRebasable_, - pairedToken: l1TokenRebasable_ - }); - _getNonRebasableTokens()[l2TokenNonRebasable_] = TokenInfo({ - oppositeLayerToken: l1TokenNonRebasable_, - pairedToken: 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 (_getRebasableTokens()[l1Token_].oppositeLayerToken == address(0) && - _getNonRebasableTokens()[l1Token_].oppositeLayerToken == address(0)) { + if (l1Token_ != L1_TOKEN_NON_REBASABLE && l1Token_ != L1_TOKEN_REBASABLE) { revert ErrorUnsupportedL1Token(); } - if (_getRebasableTokens()[l2Token_].oppositeLayerToken == address(0) && - _getNonRebasableTokens()[l2Token_].oppositeLayerToken == address(0)) { + if (l2Token_ != L2_TOKEN_NON_REBASABLE && l2Token_ != L2_TOKEN_REBASABLE) { revert ErrorUnsupportedL2Token(); } - if (_getRebasableTokens()[l1Token_].oppositeLayerToken != l2Token_ && - _getNonRebasableTokens()[l2Token_].oppositeLayerToken != l1Token_) { + 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 (_getRebasableTokens()[l1Token_].oppositeLayerToken == address(0) && - _getNonRebasableTokens()[l1Token_].oppositeLayerToken == address(0)) { + if (l1Token_ != L1_TOKEN_NON_REBASABLE && l1Token_ != L1_TOKEN_REBASABLE) { revert ErrorUnsupportedL1Token(); } _; @@ -115,8 +67,7 @@ contract RebasableAndNonRebasableTokens { /// @dev Validates that passed l2Token_ is supported by the bridge modifier onlySupportedL2Token(address l2Token_) { - if (_getRebasableTokens()[l2Token_].oppositeLayerToken == address(0) && - _getNonRebasableTokens()[l2Token_].oppositeLayerToken == address(0)) { + if (l2Token_ != L2_TOKEN_NON_REBASABLE && l2Token_ != L2_TOKEN_REBASABLE) { revert ErrorUnsupportedL2Token(); } _; @@ -131,17 +82,11 @@ contract RebasableAndNonRebasableTokens { } function _isRebasable(address token_) internal view returns (bool) { - return _getRebasableTokens()[token_].oppositeLayerToken != address(0); + return token_ == L1_TOKEN_REBASABLE || token_ == L2_TOKEN_REBASABLE; } function _l1Token(address l2Token_) internal view returns (address) { - return _isRebasable(l2Token_) ? - _getRebasableTokens()[l2Token_].oppositeLayerToken : - _getNonRebasableTokens()[l2Token_].oppositeLayerToken; - } - - function _l1NonRebasableToken(address l1Token_) internal view returns (address) { - return _isRebasable(l1Token_) ? _getRebasableTokens()[l1Token_].pairedToken : l1Token_; + return _isRebasable(l2Token_) ? L1_TOKEN_REBASABLE : L1_TOKEN_NON_REBASABLE; } error ErrorUnsupportedL1Token(); diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index a191e216..beeef21a 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -1,49 +1,52 @@ -// SPDX-FileCopyrightText: 2022 Lido +// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.10; -import {ITokenRateOracle} from "../token/interfaces/ITokenRateOracle.sol"; +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 BRIDGE; + 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 RATE_OUTDATED_DELAY; + uint256 public immutable TOKEN_RATE_OUTDATED_DELAY; + + /// @notice Decimals of the oracle response. + uint8 public constant DECIMALS = 18; /// @notice wstETH/stETH token rate. - uint256 private tokenRate; + uint256 public tokenRate; /// @notice L1 time when token rate was pushed. - uint256 private rateL1Timestamp; - - /// @notice Decimals of the oracle response. - uint8 private constant DECIMALS = 18; + uint256 public rateL1Timestamp; /// @param messenger_ L2 messenger address being used for cross-chain communications - /// @param bridge_ the bridge address that has a right to updates oracle. + /// @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 rateOutdatedDelay_ time period when token rate can be considered outdated. + /// @param tokenRateOutdatedDelay_ time period when token rate can be considered outdated. constructor( address messenger_, - address bridge_, + address l2ERC20TokenBridge_, address l1TokenRatePusher_, - uint256 rateOutdatedDelay_ + uint256 tokenRateOutdatedDelay_ ) CrossDomainEnabled(messenger_) { - BRIDGE = bridge_; + L2_ERC20_TOKEN_BRIDGE = l2ERC20TokenBridge_; L1_TOKEN_RATE_PUSHER = l1TokenRatePusher_; - RATE_OUTDATED_DELAY = rateOutdatedDelay_; + TOKEN_RATE_OUTDATED_DELAY = tokenRateOutdatedDelay_; } - /// @inheritdoc ITokenRateOracle + /// @inheritdoc IChainlinkAggregatorInterface function latestRoundData() external view returns ( uint80 roundId_, int256 answer_, @@ -51,7 +54,7 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { uint256 updatedAt_, uint80 answeredInRound_ ) { - uint80 roundId = uint80(rateL1Timestamp); // TODO: add solt + uint80 roundId = uint80(rateL1Timestamp); return ( roundId, @@ -62,23 +65,20 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { ); } - /// @inheritdoc ITokenRateOracle + /// @inheritdoc IChainlinkAggregatorInterface function latestAnswer() external view returns (int256) { return int256(tokenRate); } - /// @inheritdoc ITokenRateOracle + /// @inheritdoc IChainlinkAggregatorInterface function decimals() external pure returns (uint8) { return DECIMALS; } - /// @inheritdoc ITokenRateOracle + /// @inheritdoc ITokenRateUpdatable function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { - if (!( - (msg.sender == address(MESSENGER) && MESSENGER.xDomainMessageSender() == L1_TOKEN_RATE_PUSHER) - || (msg.sender == BRIDGE) - )) { + if (_isNoRightsToCall(msg.sender)) { revert ErrorNoRights(msg.sender); } @@ -98,7 +98,14 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { /// @notice Returns flag that shows that token rate can be considered outdated. function isLikelyOutdated() external view returns (bool) { - return block.timestamp - rateL1Timestamp > RATE_OUTDATED_DELAY; + 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_); diff --git a/contracts/token/interfaces/ITokenRateOracle.sol b/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol similarity index 75% rename from contracts/token/interfaces/ITokenRateOracle.sol rename to contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol index ce057ac8..e9a1e624 100644 --- a/contracts/token/interfaces/ITokenRateOracle.sol +++ b/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol @@ -4,9 +4,8 @@ pragma solidity 0.8.10; /// @author kovalgek -/// @notice Oracle interface for token rate. A subset of Chainlink data feed interface. -interface ITokenRateOracle { - +/// @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. @@ -31,9 +30,4 @@ interface ITokenRateOracle { /// @notice represents the number of decimals the oracle responses represent. /// @return decimals of the oracle response. function decimals() external view returns (uint8); - - /// @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/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 index c865691a..56e94718 100644 --- a/contracts/stubs/ERC1271PermitSignerMock.sol +++ b/contracts/stubs/ERC1271PermitSignerMock.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.10; - +/// @dev For testing purposes. contract ERC1271PermitSignerMock { bytes4 public constant ERC1271_MAGIC_VALUE = 0x1626ba7e; 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 index b23817bc..fc9ab194 100644 --- a/contracts/stubs/ERC20WrapperStub.sol +++ b/contracts/stubs/ERC20WrapperStub.sol @@ -1,15 +1,15 @@ -// 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"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20WstETH} from "../token/interfaces/IERC20WstETH.sol"; +import {IERC20WstETH} from "../optimism/L1LidoTokensBridge.sol"; import {IERC20Wrapper} from "../token/interfaces/IERC20Wrapper.sol"; -// represents wstETH on L1 +/// @dev represents wstETH on L1. For testing purposes. contract ERC20WrapperStub is IERC20Wrapper, IERC20WstETH, ERC20 { IERC20 public stETH; 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 index d936ae37..c6341000 100644 --- a/contracts/token/ERC20BridgedPermit.sol +++ b/contracts/token/ERC20BridgedPermit.sol @@ -4,9 +4,10 @@ pragma solidity 0.8.10; import {ERC20Bridged} from "./ERC20Bridged.sol"; -import {ERC20Permit} from "./ERC20Permit.sol"; +import {PermitExtension} from "./PermitExtension.sol"; -contract ERC20BridgedPermit is ERC20Bridged, ERC20Permit { +/// @author kovalgek +contract ERC20BridgedPermit is ERC20Bridged, PermitExtension { /// @param name_ The name of the token /// @param symbol_ The symbol of the token @@ -21,7 +22,7 @@ contract ERC20BridgedPermit is ERC20Bridged, ERC20Permit { address bridge_ ) ERC20Bridged(name_, symbol_, decimals_, bridge_) - ERC20Permit(name_, version_) + PermitExtension(name_, version_) { } 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/ERC20Rebasable.sol b/contracts/token/ERC20Rebasable.sol index 49939548..b8660ded 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20Rebasable.sol @@ -1,16 +1,32 @@ -// SPDX-FileCopyrightText: 2022 Lido +// 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 {IERC20Wrapper} from "./interfaces/IERC20Wrapper.sol"; -import {IERC20BridgedShares} from "./interfaces/IERC20BridgedShares.sol"; -import {ITokenRateOracle} from "./interfaces/ITokenRateOracle.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 ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Metadata { @@ -19,10 +35,10 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta using UnstructuredStorage for bytes32; /// @inheritdoc IERC20BridgedShares - address public immutable BRIDGE; + address public immutable L2_ERC20_TOKEN_BRIDGE; - /// @notice Contract of non-rebasable token to wrap. - IERC20 public immutable WRAPPED_TOKEN; + /// @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; @@ -41,18 +57,18 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta /// @param decimals_ The decimals places of the token /// @param wrappedToken_ 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 + /// @param l2ERC20TokenBridge_ The bridge address which allowd to mint/burn tokens constructor( string memory name_, string memory symbol_, uint8 decimals_, address wrappedToken_, address tokenRateOracle_, - address bridge_ + address l2ERC20TokenBridge_ ) ERC20Metadata(name_, symbol_, decimals_) { - WRAPPED_TOKEN = IERC20(wrappedToken_); + TOKEN_TO_WRAP_FROM = IERC20(wrappedToken_); TOKEN_RATE_ORACLE = ITokenRateOracle(tokenRateOracle_); - BRIDGE = bridge_; + L2_ERC20_TOKEN_BRIDGE = l2ERC20TokenBridge_; } /// @notice Sets the name and the symbol of the tokens if they both are empty @@ -68,7 +84,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); _mintShares(msg.sender, sharesAmount_); - if(!WRAPPED_TOKEN.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); + if(!TOKEN_TO_WRAP_FROM.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); return _getTokensByShares(sharesAmount_); } @@ -79,7 +95,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta uint256 sharesAmount = _getSharesByTokens(tokenAmount_); _burnShares(msg.sender, sharesAmount); - if(!WRAPPED_TOKEN.transfer(msg.sender, sharesAmount)) revert ErrorERC20Transfer(); + if(!TOKEN_TO_WRAP_FROM.transfer(msg.sender, sharesAmount)) revert ErrorERC20Transfer(); return sharesAmount; } @@ -249,12 +265,13 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta if (rateDecimals == uint8(0)) revert ErrorTokenRateDecimalsIsZero(); //slither-disable-next-line unused-return - (, - int256 answer - , - , - uint256 updatedAt - ,) = TOKEN_RATE_ORACLE.latestRoundData(); + ( + /* roundId_ */, + int256 answer, + /* startedAt_ */, + uint256 updatedAt, + /* answeredInRound_ */ + ) = TOKEN_RATE_ORACLE.latestRoundData(); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); if (answer <= 0) revert ErrorOracleAnswerIsNotPositive(); @@ -312,7 +329,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta function _emitTransferEvents( address _from, address _to, - uint _tokenAmount, + uint256 _tokenAmount, uint256 _sharesAmount ) internal { emit Transfer(_from, _to, _tokenAmount); @@ -329,7 +346,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta /// @dev Validates that sender of the transaction is the bridge modifier onlyBridge() { - if (msg.sender != BRIDGE) { + if (msg.sender != L2_ERC20_TOKEN_BRIDGE) { revert ErrorNotBridge(); } _; diff --git a/contracts/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasablePermit.sol index 57606446..03c4fb65 100644 --- a/contracts/token/ERC20RebasablePermit.sol +++ b/contracts/token/ERC20RebasablePermit.sol @@ -4,9 +4,10 @@ pragma solidity 0.8.10; import {ERC20Rebasable} from "./ERC20Rebasable.sol"; -import {ERC20Permit} from "./ERC20Permit.sol"; +import {PermitExtension} from "./PermitExtension.sol"; -contract ERC20RebasablePermit is ERC20Rebasable, ERC20Permit { +/// @author kovalgek +contract ERC20RebasablePermit is ERC20Rebasable, PermitExtension { /// @param name_ The name of the token /// @param symbol_ The symbol of the token @@ -25,7 +26,7 @@ contract ERC20RebasablePermit is ERC20Rebasable, ERC20Permit { address bridge_ ) ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) - ERC20Permit(name_, version_) + PermitExtension(name_, version_) { } diff --git a/contracts/token/ERC20Permit.sol b/contracts/token/PermitExtension.sol similarity index 57% rename from contracts/token/ERC20Permit.sol rename to contracts/token/PermitExtension.sol index 634731c7..ed68d6c3 100644 --- a/contracts/token/ERC20Permit.sol +++ b/contracts/token/PermitExtension.sol @@ -8,21 +8,19 @@ 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"; -contract ERC20Permit is IERC2612, EIP712 { +abstract contract PermitExtension is IERC2612, EIP712 { using UnstructuredStorage for bytes32; - /** - * @dev Nonces for ERC-2612 (Permit) - */ + /// @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)") - */ + + /// @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; @@ -35,19 +33,18 @@ contract ERC20Permit is IERC2612, EIP712 { { } - /** - * @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}). - */ + /// @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 { @@ -61,47 +58,40 @@ contract ERC20Permit is IERC2612, EIP712 { bytes32 hash = _hashTypedDataV4(structHash); - if (!SignatureChecker.isValidSignatureNow(_owner, hash, _v, _r, _s)) { + if (!SignatureChecker.isValidSignature(_owner, hash, _v, _r, _s)) { revert ErrorInvalidSignature(); } _permitAccepted(_owner, _spender, _value); } - function _permitAccepted( - address owner_, - address spender_, - uint256 amount_ - ) internal virtual { - } - /** - * @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. - */ + /// @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}. - */ + /// @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. - */ + + /// @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 is used to override in inherited contracts and call approve function + function _permitAccepted(address owner_, address spender_, uint256 amount_) internal virtual; + error ErrorInvalidSignature(); error ErrorDeadlineExpired(); } 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/IERC20BridgedShares.sol b/contracts/token/interfaces/IERC20BridgedShares.sol deleted file mode 100644 index 8ae4be6d..00000000 --- a/contracts/token/interfaces/IERC20BridgedShares.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 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 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; -} 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/IERC20TokenRate.sol b/contracts/token/interfaces/IERC20TokenRate.sol deleted file mode 100644 index 0b57716e..00000000 --- a/contracts/token/interfaces/IERC20TokenRate.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @author kovalgek -/// @notice Token rate interface. -interface IERC20TokenRate { - - /// @notice Returns token rate. - function tokenRate() external view returns (uint256); -} diff --git a/contracts/token/interfaces/IERC20WstETH.sol b/contracts/token/interfaces/IERC20WstETH.sol deleted file mode 100644 index 4bb216c4..00000000 --- a/contracts/token/interfaces/IERC20WstETH.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.10; - -/// @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); -} From f387de67cec527b2dd3075c4a847c3e14d060859 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 16 Apr 2024 23:20:33 +0200 Subject: [PATCH 61/61] PR fixes: rename tokens, add sanity check to oracle update, add unit tests, fix comments --- .../optimism/L1ERC20ExtendedTokensBridge.sol | 12 +- .../optimism/L2ERC20ExtendedTokensBridge.sol | 24 +- contracts/optimism/TokenRateOracle.sol | 19 +- .../IChainlinkAggregatorInterface.sol | 4 +- contracts/token/ERC20BridgedPermit.sol | 7 +- ...ebasable.sol => ERC20RebasableBridged.sol} | 26 +- ...it.sol => ERC20RebasableBridgedPermit.sol} | 17 +- contracts/token/PermitExtension.sol | 2 +- test/optimism/L1LidoTokensBridge.unit.test.ts | 92 ++++++- .../L2ERC20ExtendedTokensBridge.unit.test.ts | 254 +++++++++++++++++- test/optimism/TokenRateOracle.unit.test.ts | 91 +++++-- test/token/ERC20Permit.unit.test.ts | 14 +- test/token/ERC20Rebasable.unit.test.ts | 77 +++--- utils/optimism/deploymentAllFromScratch.ts | 6 +- .../deploymentBridgesAndRebasableToken.ts | 6 +- .../optimism/deploymentNewImplementations.ts | 6 +- utils/optimism/testing.ts | 4 +- 17 files changed, 519 insertions(+), 142 deletions(-) rename contracts/token/{ERC20Rebasable.sol => ERC20RebasableBridged.sol} (94%) rename contracts/token/{ERC20RebasablePermit.sol => ERC20RebasableBridgedPermit.sol} (64%) diff --git a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol index bb67010c..8bdaf2d5 100644 --- a/contracts/optimism/L1ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L1ERC20ExtendedTokensBridge.sol @@ -119,7 +119,7 @@ abstract contract L1ERC20ExtendedTokensBridge is onlyFromCrossDomainAccount(L2_TOKEN_BRIDGE) onlySupportedL1L2TokensPair(l1Token_, l2Token_) { - uint256 amountToWithdraw = _isRebasable(l1Token_) ? + uint256 amountToWithdraw = (_isRebasable(l1Token_) && amount_ != 0) ? IERC20Wrapper(L1_TOKEN_NON_REBASABLE).unwrap(amount_) : amount_; IERC20(l1Token_).safeTransfer(to_, amountToWithdraw); @@ -134,9 +134,8 @@ abstract contract L1ERC20ExtendedTokensBridge is /// @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_ 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. + /// @param encodedDepositData_ a concatenation of packed token rate with L1 time and + /// optional data passed by external contract function _depositERC20To( address l1Token_, address l2Token_, @@ -156,6 +155,11 @@ abstract contract L1ERC20ExtendedTokensBridge is 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_, diff --git a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol index 38a6c834..ccba2bdd 100644 --- a/contracts/optimism/L2ERC20ExtendedTokensBridge.sol +++ b/contracts/optimism/L2ERC20ExtendedTokensBridge.sol @@ -11,7 +11,7 @@ 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 {ERC20Rebasable} from "../token/ERC20Rebasable.sol"; +import {ERC20RebasableBridged} from "../token/ERC20RebasableBridged.sol"; import {BridgingManager} from "../BridgingManager.sol"; import {RebasableAndNonRebasableTokens} from "./RebasableAndNonRebasableTokens.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; @@ -107,7 +107,7 @@ contract L2ERC20ExtendedTokensBridge is onlyFromCrossDomainAccount(L1_TOKEN_BRIDGE) { DepositDataCodec.DepositData memory depositData = DepositDataCodec.decodeDepositData(data_); - ITokenRateUpdatable tokenRateOracle = ERC20Rebasable(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); + ITokenRateUpdatable tokenRateOracle = ERC20RebasableBridged(L2_TOKEN_REBASABLE).TOKEN_RATE_ORACLE(); tokenRateOracle.updateRate(depositData.rate, depositData.timestamp); uint256 depositedAmount = _mintTokens(l1Token_, l2Token_, to_, amount_); @@ -116,6 +116,7 @@ contract L2ERC20ExtendedTokensBridge is /// @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 @@ -140,6 +141,12 @@ contract L2ERC20ExtendedTokensBridge is 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_, @@ -147,22 +154,27 @@ contract L2ERC20ExtendedTokensBridge is uint256 amount_ ) internal returns (uint256) { if(_isRebasable(l1Token_)) { - ERC20Rebasable(l2Token_).bridgeMintShares(to_, amount_); - return ERC20Rebasable(l2Token_).getTokensByShares(amount_); + 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 = ERC20Rebasable(l2Token_).getSharesByTokens(amount_); - ERC20Rebasable(l2Token_).bridgeBurnShares(from_, shares); + uint256 shares = ERC20RebasableBridged(l2Token_).getSharesByTokens(amount_); + ERC20RebasableBridged(l2Token_).bridgeBurnShares(from_, shares); return shares; } diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index beeef21a..164807b4 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -25,6 +25,10 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { /// @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; @@ -83,7 +87,16 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { } if (rateL1Timestamp_ < rateL1Timestamp) { - revert ErrorIncorrectRateTimestamp(); + 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) { @@ -109,7 +122,9 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { } event RateUpdated(uint256 tokenRate_, uint256 rateL1Timestamp_); + event NewTokenRateOutdated(uint256 tokenRate_, uint256 rateL1Timestamp_, uint256 newTateL1Timestamp_); error ErrorNoRights(address caller); - error ErrorIncorrectRateTimestamp(); + 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 index e9a1e624..03332adf 100644 --- a/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol +++ b/contracts/optimism/interfaces/IChainlinkAggregatorInterface.sol @@ -8,7 +8,7 @@ pragma solidity 0.8.10; 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. + /// @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_. @@ -24,7 +24,7 @@ interface IChainlinkAggregatorInterface { ); /// @notice get the lastest token rate. - /// @return wstETH/stETH 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. diff --git a/contracts/token/ERC20BridgedPermit.sol b/contracts/token/ERC20BridgedPermit.sol index c6341000..d7eca099 100644 --- a/contracts/token/ERC20BridgedPermit.sol +++ b/contracts/token/ERC20BridgedPermit.sol @@ -26,11 +26,8 @@ contract ERC20BridgedPermit is ERC20Bridged, PermitExtension { { } - function _permitAccepted( - address owner_, - address spender_, - uint256 amount_ - ) internal override { + /// @inheritdoc PermitExtension + function _permitAccepted(address owner_, address spender_, uint256 amount_) internal override { _approve(owner_, spender_, amount_); } } diff --git a/contracts/token/ERC20Rebasable.sol b/contracts/token/ERC20RebasableBridged.sol similarity index 94% rename from contracts/token/ERC20Rebasable.sol rename to contracts/token/ERC20RebasableBridged.sol index b8660ded..4de21b66 100644 --- a/contracts/token/ERC20Rebasable.sol +++ b/contracts/token/ERC20RebasableBridged.sol @@ -4,6 +4,7 @@ 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"; @@ -29,8 +30,8 @@ interface IERC20BridgedShares is IERC20 { /// @author kovalgek /// @notice Rebasable token that wraps/unwraps non-rebasable token and allow to mint/burn tokens by bridge. -contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Metadata { - +contract ERC20RebasableBridged is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Metadata { + using SafeERC20 for IERC20; using UnstructuredRefStorage for bytes32; using UnstructuredStorage for bytes32; @@ -44,29 +45,29 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ITokenRateOracle public immutable TOKEN_RATE_ORACLE; /// @dev token allowance slot position. - bytes32 internal constant TOKEN_ALLOWANCE_POSITION = keccak256("ERC20Rebasable.TOKEN_ALLOWANCE_POSITION"); + bytes32 internal constant TOKEN_ALLOWANCE_POSITION = keccak256("ERC20RebasableBridged.TOKEN_ALLOWANCE_POSITION"); /// @dev user shares slot position. - bytes32 internal constant SHARES_POSITION = keccak256("ERC20Rebasable.SHARES_POSITION"); + bytes32 internal constant SHARES_POSITION = keccak256("ERC20RebasableBridged.SHARES_POSITION"); /// @dev token shares slot position. - bytes32 internal constant TOTAL_SHARES_POSITION = keccak256("ERC20Rebasable.TOTAL_SHARES_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 wrappedToken_ address of the ERC20 token to wrap + /// @param tokenToWrapFrom_ address of the ERC20 token to wrap /// @param tokenRateOracle_ address of oracle that returns tokens rate - /// @param l2ERC20TokenBridge_ The bridge address which allowd to mint/burn tokens + /// @param l2ERC20TokenBridge_ The bridge address which allows to mint/burn tokens constructor( string memory name_, string memory symbol_, uint8 decimals_, - address wrappedToken_, + address tokenToWrapFrom_, address tokenRateOracle_, address l2ERC20TokenBridge_ ) ERC20Metadata(name_, symbol_, decimals_) { - TOKEN_TO_WRAP_FROM = IERC20(wrappedToken_); + TOKEN_TO_WRAP_FROM = IERC20(tokenToWrapFrom_); TOKEN_RATE_ORACLE = ITokenRateOracle(tokenRateOracle_); L2_ERC20_TOKEN_BRIDGE = l2ERC20TokenBridge_; } @@ -84,7 +85,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta if (sharesAmount_ == 0) revert ErrorZeroSharesWrap(); _mintShares(msg.sender, sharesAmount_); - if(!TOKEN_TO_WRAP_FROM.transferFrom(msg.sender, address(this), sharesAmount_)) revert ErrorERC20Transfer(); + TOKEN_TO_WRAP_FROM.safeTransferFrom(msg.sender, address(this), sharesAmount_); return _getTokensByShares(sharesAmount_); } @@ -95,7 +96,7 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta uint256 sharesAmount = _getSharesByTokens(tokenAmount_); _burnShares(msg.sender, sharesAmount); - if(!TOKEN_TO_WRAP_FROM.transfer(msg.sender, sharesAmount)) revert ErrorERC20Transfer(); + TOKEN_TO_WRAP_FROM.safeTransfer(msg.sender, sharesAmount); return sharesAmount; } @@ -274,7 +275,6 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta ) = TOKEN_RATE_ORACLE.latestRoundData(); if (updatedAt == 0) revert ErrorWrongOracleUpdateTime(); - if (answer <= 0) revert ErrorOracleAnswerIsNotPositive(); return (uint256(answer), uint256(rateDecimals)); } @@ -364,12 +364,10 @@ contract ERC20Rebasable is IERC20, IERC20Wrapper, IERC20BridgedShares, ERC20Meta error ErrorZeroTokensUnwrap(); error ErrorTokenRateDecimalsIsZero(); error ErrorWrongOracleUpdateTime(); - error ErrorOracleAnswerIsNotPositive(); error ErrorTrasferToRebasableContract(); error ErrorNotEnoughBalance(); error ErrorNotEnoughAllowance(); error ErrorAccountIsZeroAddress(); error ErrorDecreasedAllowanceBelowZero(); error ErrorNotBridge(); - error ErrorERC20Transfer(); } diff --git a/contracts/token/ERC20RebasablePermit.sol b/contracts/token/ERC20RebasableBridgedPermit.sol similarity index 64% rename from contracts/token/ERC20RebasablePermit.sol rename to contracts/token/ERC20RebasableBridgedPermit.sol index 03c4fb65..6b9be86d 100644 --- a/contracts/token/ERC20RebasablePermit.sol +++ b/contracts/token/ERC20RebasableBridgedPermit.sol @@ -3,17 +3,17 @@ pragma solidity 0.8.10; -import {ERC20Rebasable} from "./ERC20Rebasable.sol"; +import {ERC20RebasableBridged} from "./ERC20RebasableBridged.sol"; import {PermitExtension} from "./PermitExtension.sol"; /// @author kovalgek -contract ERC20RebasablePermit is ERC20Rebasable, PermitExtension { +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 wrappedToken_ address of the ERC20 token to wrap + /// @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( @@ -21,20 +21,17 @@ contract ERC20RebasablePermit is ERC20Rebasable, PermitExtension { string memory symbol_, string memory version_, uint8 decimals_, - address wrappedToken_, + address tokenToWrapFrom_, address tokenRateOracle_, address bridge_ ) - ERC20Rebasable(name_, symbol_, decimals_, wrappedToken_, tokenRateOracle_, bridge_) + ERC20RebasableBridged(name_, symbol_, decimals_, tokenToWrapFrom_, tokenRateOracle_, bridge_) PermitExtension(name_, version_) { } - function _permitAccepted( - address owner_, - address spender_, - uint256 amount_ - ) internal override { + /// @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 index ed68d6c3..40cd3617 100644 --- a/contracts/token/PermitExtension.sol +++ b/contracts/token/PermitExtension.sol @@ -89,7 +89,7 @@ abstract contract PermitExtension is IERC2612, EIP712 { noncesByAddress[_owner] = current + 1; } - /// @dev is used to override in inherited contracts and call approve function + /// @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(); diff --git a/test/optimism/L1LidoTokensBridge.unit.test.ts b/test/optimism/L1LidoTokensBridge.unit.test.ts index 15642120..df1aaae0 100644 --- a/test/optimism/L1LidoTokensBridge.unit.test.ts +++ b/test/optimism/L1LidoTokensBridge.unit.test.ts @@ -804,10 +804,9 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) ), "ErrorWrongCrossDomainSender()" ); - } - ) + }) - .test("finalizeERC20Withdrawal() :: non rebasable token flow", async (ctx) => { + .test("finalizeERC20Withdrawal() :: non-rebasable token flow", async (ctx) => { const { l1TokenBridge, stubs: { l1TokenNonRebasable, l2TokenNonRebasable, l1Messenger }, @@ -816,10 +815,9 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); - const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); - const amount = wei`1 ether`; const data = "0xdeadbeaf"; + const bridgeBalanceBefore = await l1TokenNonRebasable.balanceOf(l1TokenBridge.address); const tx = await l1TokenBridge .connect(l1MessengerStubAsEOA) @@ -855,19 +853,15 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) 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 deployerBalanceBefore = await l1TokenRebasable.balanceOf(deployer.address); - - await l1Messenger.setXDomainMessageSender(l2TokenBridgeEOA.address); - - await l1TokenRebasable.transfer(l1TokenNonRebasable.address, wei`100 ether`); - const bridgeBalanceBefore = await l1TokenRebasable.balanceOf(l1TokenBridge.address); const tx = await l1TokenBridge @@ -897,6 +891,80 @@ unit("Optimism :: L1LidoTokensBridge", ctxFactory) ); }) + .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() { diff --git a/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts index 53040ce7..ed800439 100644 --- a/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts +++ b/test/optimism/L2ERC20ExtendedTokensBridge.unit.test.ts @@ -3,12 +3,13 @@ import { ERC20BridgedStub__factory, ERC20WrapperStub__factory, TokenRateOracle__factory, - ERC20Rebasable__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"; @@ -241,6 +242,120 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) ); }) + .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, @@ -436,6 +551,122 @@ unit("Optimism:: L2ERC20ExtendedTokensBridge", ctxFactory) ); }) + .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, @@ -787,7 +1018,7 @@ async function ctxFactory() { 86400 ); - const l2TokenRebasableStub = await new ERC20Rebasable__factory(deployer).deploy( + const l2TokenRebasableStub = await new ERC20RebasableBridged__factory(deployer).deploy( "L2 Token Rebasable", "L2R", decimals, @@ -880,3 +1111,22 @@ async function packedTokenRateAndTimestamp(provider: JsonRpcProvider, tokenRate: 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/TokenRateOracle.unit.test.ts b/test/optimism/TokenRateOracle.unit.test.ts index ca4d7b02..b309ef77 100644 --- a/test/optimism/TokenRateOracle.unit.test.ts +++ b/test/optimism/TokenRateOracle.unit.test.ts @@ -1,5 +1,6 @@ import hre from "hardhat"; import { assert } from "chai"; +import { BigNumber } from "ethers"; import testing, { unit } from "../../utils/testing"; import { TokenRateOracle__factory, @@ -52,32 +53,61 @@ unit("TokenRateOracle", ctxFactory) .test("updateRate() :: incorrect time", async (ctx) => { const { tokenRateOracle } = ctx.contracts; const { bridge } = ctx.accounts; + const { tokenRateCorrect } = ctx.constants; - await tokenRateOracle.connect(bridge).updateRate(10, 1000); - await assert.revertsWith(tokenRateOracle.connect(bridge).updateRate(12, 20), "ErrorIncorrectRateTimestamp()"); + 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(10, 1000); - await assert.emits(tokenRateOracle, tx1, "RateUpdated", [10, 1000]); + const tx1 = await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, 1000); + await assert.emits(tokenRateOracle, tx1, "RateUpdated", [tokenRateCorrect, 1000]); - const tx2 = await tokenRateOracle.connect(bridge).updateRate(10, 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; - const currentTime = Date.now(); - const tokenRate = 123; + await tokenRateOracle.connect(bridge).updateRate(tokenRateCorrect, blockTimestamp); - await tokenRateOracle.connect(bridge).updateRate(tokenRate, currentTime); - - assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRate); + assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRateCorrect); const { roundId_, @@ -87,26 +117,24 @@ unit("TokenRateOracle", ctxFactory) answeredInRound_ } = await tokenRateOracle.latestRoundData(); - assert.equalBN(roundId_, currentTime); - assert.equalBN(answer_, tokenRate); - assert.equalBN(startedAt_, currentTime); - assert.equalBN(updatedAt_, currentTime); - assert.equalBN(answeredInRound_, currentTime); + 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); - const currentTime = Date.now(); - const tokenRate = 123; - - await tokenRateOracle.connect(l2MessengerStubEOA).updateRate(tokenRate, currentTime); + await tokenRateOracle.connect(l2MessengerStubEOA).updateRate(tokenRateCorrect, blockTimestamp); - assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRate); + assert.equalBN(await tokenRateOracle.latestAnswer(), tokenRateCorrect); const { roundId_, @@ -116,11 +144,11 @@ unit("TokenRateOracle", ctxFactory) answeredInRound_ } = await tokenRateOracle.latestRoundData(); - assert.equalBN(roundId_, currentTime); - assert.equalBN(answer_, tokenRate); - assert.equalBN(startedAt_, currentTime); - assert.equalBN(updatedAt_, currentTime); - assert.equalBN(answeredInRound_, currentTime); + 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); }) @@ -142,8 +170,19 @@ async function ctxFactory() { 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 } + contracts: { tokenRateOracle, l2MessengerStub }, + constants: { tokenRateCorrect, tokenRateTooBig, tokenRateTooSmall, blockTimestamp } }; } diff --git a/test/token/ERC20Permit.unit.test.ts b/test/token/ERC20Permit.unit.test.ts index 35ce44b3..20e2ae7f 100644 --- a/test/token/ERC20Permit.unit.test.ts +++ b/test/token/ERC20Permit.unit.test.ts @@ -10,7 +10,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { TokenRateOracle__factory, OssifiableProxy__factory, - ERC20RebasablePermit__factory, + ERC20RebasableBridgedPermit__factory, ERC1271PermitSignerMock__factory, ERC20BridgedPermit__factory, } from "../../typechain"; @@ -56,7 +56,7 @@ function permitTestsSuit(unitInstance: UnitTest) { // .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { // const { rebasableProxied, wrappedToken } = ctx.contracts; - // assert.equal(await rebasableProxied.WRAPPED_TOKEN(), wrappedToken.address) + // assert.equal(await rebasableProxied.TOKEN_TO_WRAP_FROM(), wrappedToken.address) // }) .test('eip712Domain() is correct', async (ctx) => { @@ -404,7 +404,7 @@ async function tokenProxied( hre.ethers.constants.AddressZero, 86400 ); - const rebasableTokenImpl = await new ERC20RebasablePermit__factory(deployer).deploy( + const rebasableTokenImpl = await new ERC20RebasableBridgedPermit__factory(deployer).deploy( name, symbol, SIGNING_DOMAIN_VERSION, @@ -417,13 +417,13 @@ async function tokenProxied( const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( rebasableTokenImpl.address, deployer.address, - ERC20RebasablePermit__factory.createInterface().encodeFunctionData("initialize", [ + ERC20RebasableBridgedPermit__factory.createInterface().encodeFunctionData("initialize", [ name, symbol, ]) ); - const rebasableProxied = ERC20RebasablePermit__factory.connect( + const rebasableProxied = ERC20RebasableBridgedPermit__factory.connect( l2TokensProxy.address, holder ); @@ -461,7 +461,7 @@ async function tokenProxied( } permitTestsSuit( - unit("ERC20RebasablePermit with EIP1271 (contract) signing", + unit("ERC20RebasableBridgedPermit with EIP1271 (contract) signing", ctxFactoryFactory( "Liquid staked Ether 2.0", "stETH", @@ -472,7 +472,7 @@ permitTestsSuit( ); permitTestsSuit( - unit("ERC20RebasablePermit with ECDSA (EOA) signing", + unit("ERC20RebasableBridgedPermit with ECDSA (EOA) signing", ctxFactoryFactory( "Liquid staked Ether 2.0", "stETH", diff --git a/test/token/ERC20Rebasable.unit.test.ts b/test/token/ERC20Rebasable.unit.test.ts index 7517f3f0..e71c6c73 100644 --- a/test/token/ERC20Rebasable.unit.test.ts +++ b/test/token/ERC20Rebasable.unit.test.ts @@ -6,13 +6,13 @@ import { wei } from "../../utils/wei"; import { ERC20Bridged__factory, TokenRateOracle__factory, - ERC20Rebasable__factory, + ERC20RebasableBridged__factory, OssifiableProxy__factory, CrossDomainMessengerStub__factory } from "../../typechain"; import { BigNumber } from "ethers"; -unit("ERC20Rebasable", ctxFactory) +unit("ERC20RebasableBridged", ctxFactory) .test("wrappedToken() :: has the same address is in constructor", async (ctx) => { const { rebasableProxied, wrappedToken } = ctx.contracts; @@ -50,7 +50,7 @@ unit("ERC20Rebasable", ctxFactory) zero.address, 86400 ); - const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + const rebasableTokenImpl = await new ERC20RebasableBridged__factory(deployer).deploy( "name", "", 10, @@ -81,7 +81,7 @@ unit("ERC20Rebasable", ctxFactory) zero.address, 86400 ); - const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + const rebasableTokenImpl = await new ERC20RebasableBridged__factory(deployer).deploy( "", "symbol", 10, @@ -128,7 +128,7 @@ unit("ERC20Rebasable", ctxFactory) zero.address, 86400 ); - const rebasableProxied = await new ERC20Rebasable__factory(deployer).deploy( + const rebasableProxied = await new ERC20RebasableBridged__factory(deployer).deploy( "", "symbol", 10, @@ -143,18 +143,6 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).wrap(5), "ErrorWrongOracleUpdateTime()"); }) -.test("wrap() :: wrong oracle answer", async (ctx) => { - - const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; - const { user1, owner } = ctx.accounts; - - await tokenRateOracle.connect(owner).updateRate(0, 2000); - await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); - await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); - - await assert.revertsWith(rebasableProxied.connect(user1).wrap(21), "ErrorOracleAnswerIsNotPositive()"); - }) - .test("wrap() :: when no balance", async (ctx) => { const { rebasableProxied, wrappedToken } = ctx.contracts; const { user1 } = ctx.accounts; @@ -166,7 +154,7 @@ unit("ERC20Rebasable", ctxFactory) .test("wrap() :: happy path", async (ctx) => { const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; - const {user1, user2, owner } = ctx.accounts; + const {user1, user2, owner, zero } = ctx.accounts; const { rate, decimals, premintShares } = ctx.constants; await tokenRateOracle.connect(owner).updateRate(rate, 1000); @@ -189,6 +177,9 @@ unit("ERC20Rebasable", ctxFactory) 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); @@ -210,6 +201,9 @@ unit("ERC20Rebasable", ctxFactory) 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)); @@ -308,7 +302,7 @@ unit("ERC20Rebasable", ctxFactory) zero.address, 86400 ); - const rebasableProxied = await new ERC20Rebasable__factory(deployer).deploy( + const rebasableProxied = await new ERC20RebasableBridged__factory(deployer).deploy( "", "symbol", 10, @@ -323,18 +317,6 @@ unit("ERC20Rebasable", ctxFactory) await assert.revertsWith(rebasableProxied.connect(user1).unwrap(5), "ErrorWrongOracleUpdateTime()"); }) -.test("unwrap() :: wrong oracle answer", async (ctx) => { - - const { rebasableProxied, wrappedToken, tokenRateOracle } = ctx.contracts; - const { user1, owner } = ctx.accounts; - - await tokenRateOracle.connect(owner).updateRate(0, 2000); - await wrappedToken.connect(owner).bridgeMint(user1.address, 1000); - await wrappedToken.connect(user1).approve(rebasableProxied.address, 1000); - - await assert.revertsWith(rebasableProxied.connect(user1).unwrap(21), "ErrorOracleAnswerIsNotPositive()"); - }) - .test("unwrap() :: when no balance", async (ctx) => { const { rebasableProxied } = ctx.contracts; const { user1 } = ctx.accounts; @@ -345,7 +327,7 @@ unit("ERC20Rebasable", ctxFactory) .test("bridgeMintShares() :: happy path", async (ctx) => { const { rebasableProxied } = ctx.contracts; - const {user1, user2, owner } = ctx.accounts; + const {user1, user2, owner, zero } = ctx.accounts; const { rate, decimals, premintShares, premintTokens } = ctx.constants; assert.equalBN(await rebasableProxied.getTotalShares(), premintShares); @@ -359,6 +341,8 @@ unit("ERC20Rebasable", ctxFactory) 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); @@ -375,6 +359,8 @@ unit("ERC20Rebasable", ctxFactory) 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); @@ -538,6 +524,7 @@ unit("ERC20Rebasable", ctxFactory) 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 @@ -551,6 +538,12 @@ unit("ERC20Rebasable", ctxFactory) amount, ]); + await assert.emits(rebasableProxied, tx, "TransferShares", [ + holder.address, + recipient.address, + sharesToTransfer, + ]); + // validate balance was updated assert.equalBN( await rebasableProxied.balanceOf(holder.address), @@ -723,7 +716,7 @@ unit("ERC20Rebasable", ctxFactory) async (ctx) => { const { rebasableProxied } = ctx.contracts; const { premintShares } = ctx.constants; - const { recipient, owner } = ctx.accounts; + const { recipient, owner, zero } = ctx.accounts; // validate balance before mint assert.equalBN(await rebasableProxied.balanceOf(recipient.address), 0); @@ -739,12 +732,12 @@ unit("ERC20Rebasable", ctxFactory) // validate Transfer event was emitted const mintAmountInTokens = await rebasableProxied.getTokensByShares(mintAmount); await assert.emits(rebasableProxied, tx, "Transfer", [ - hre.ethers.constants.AddressZero, + zero.address, recipient.address, mintAmountInTokens, ]); await assert.emits(rebasableProxied, tx, "TransferShares", [ - hre.ethers.constants.AddressZero, + zero.address, recipient.address, mintAmount, ]); @@ -839,6 +832,10 @@ async function ctxFactory() { 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, @@ -863,7 +860,7 @@ async function ctxFactory() { zero.address, 86400 ); - const rebasableTokenImpl = await new ERC20Rebasable__factory(deployer).deploy( + const rebasableTokenImpl = await new ERC20RebasableBridged__factory(deployer).deploy( name, symbol, decimalsToSet, @@ -880,23 +877,23 @@ async function ctxFactory() { const l2TokensProxy = await new OssifiableProxy__factory(deployer).deploy( rebasableTokenImpl.address, deployer.address, - ERC20Rebasable__factory.createInterface().encodeFunctionData("initialize", [ + ERC20RebasableBridged__factory.createInterface().encodeFunctionData("initialize", [ name, symbol, ]) ); - const rebasableProxied = ERC20Rebasable__factory.connect( + const rebasableProxied = ERC20RebasableBridged__factory.connect( l2TokensProxy.address, holder ); - await tokenRateOracle.connect(owner).updateRate(rate, 1000); + 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 }, + constants: { name, symbol, decimalsToSet, decimals, premintShares, premintTokens, rate, blockTimestamp }, contracts: { rebasableProxied, wrappedToken, tokenRateOracle } }; } diff --git a/utils/optimism/deploymentAllFromScratch.ts b/utils/optimism/deploymentAllFromScratch.ts index 8d59c629..6fdfaad4 100644 --- a/utils/optimism/deploymentAllFromScratch.ts +++ b/utils/optimism/deploymentAllFromScratch.ts @@ -6,7 +6,7 @@ import network, { NetworkName } from "../network"; import { DeployScript, Logger } from "../deployment/DeployScript"; import { ERC20Bridged__factory, - ERC20Rebasable__factory, + ERC20RebasableBridged__factory, IERC20Metadata__factory, L1LidoTokensBridge__factory, L2ERC20ExtendedTokensBridge__factory, @@ -238,7 +238,7 @@ export default function deploymentAll( assert.equal(c.address, expectedL2TokenProxyAddress), }) .addStep({ - factory: ERC20Rebasable__factory, + factory: ERC20RebasableBridged__factory, args: [ l2TokenRebasableName, l2TokenRebasableSymbol, @@ -256,7 +256,7 @@ export default function deploymentAll( args: [ expectedL2TokenRebasableImplAddress, l2Params.admins.proxy, - ERC20Rebasable__factory.createInterface().encodeFunctionData( + ERC20RebasableBridged__factory.createInterface().encodeFunctionData( "initialize", [l2TokenRebasableName, l2TokenRebasableSymbol] ), diff --git a/utils/optimism/deploymentBridgesAndRebasableToken.ts b/utils/optimism/deploymentBridgesAndRebasableToken.ts index 1241ab24..f5ad4bc4 100644 --- a/utils/optimism/deploymentBridgesAndRebasableToken.ts +++ b/utils/optimism/deploymentBridgesAndRebasableToken.ts @@ -6,7 +6,7 @@ import network, { NetworkName } from "../network"; import { DeployScript, Logger } from "../deployment/DeployScript"; import { ERC20Bridged__factory, - ERC20Rebasable__factory, + ERC20RebasableBridged__factory, IERC20Metadata__factory, L1LidoTokensBridge__factory, L2ERC20ExtendedTokensBridge__factory, @@ -194,7 +194,7 @@ export default function deployment( assert.equal(c.address, expectedL2TokenProxyAddress), }) .addStep({ - factory: ERC20Rebasable__factory, + factory: ERC20RebasableBridged__factory, args: [ l2TokenRebasableName, l2TokenRebasableSymbol, @@ -212,7 +212,7 @@ export default function deployment( args: [ expectedL2TokenRebasableImplAddress, l2Params.admins.proxy, - ERC20Rebasable__factory.createInterface().encodeFunctionData( + ERC20RebasableBridged__factory.createInterface().encodeFunctionData( "initialize", [l2TokenRebasableName, l2TokenRebasableSymbol] ), diff --git a/utils/optimism/deploymentNewImplementations.ts b/utils/optimism/deploymentNewImplementations.ts index 60398029..776fe8df 100644 --- a/utils/optimism/deploymentNewImplementations.ts +++ b/utils/optimism/deploymentNewImplementations.ts @@ -6,7 +6,7 @@ import network, { NetworkName } from "../network"; import { DeployScript, Logger } from "../deployment/DeployScript"; import { ERC20Bridged__factory, - ERC20Rebasable__factory, + ERC20RebasableBridged__factory, IERC20Metadata__factory, L1LidoTokensBridge__factory, L2ERC20ExtendedTokensBridge__factory, @@ -169,7 +169,7 @@ export default function deploymentNewImplementations( assert.equal(c.address, expectedL2TokenImplAddress), }) .addStep({ - factory: ERC20Rebasable__factory, + factory: ERC20RebasableBridged__factory, args: [ l2TokenRebasableName, l2TokenRebasableSymbol, @@ -187,7 +187,7 @@ export default function deploymentNewImplementations( args: [ expectedL2TokenRebasableImplAddress, l2Params.admins.proxy, - ERC20Rebasable__factory.createInterface().encodeFunctionData( + ERC20RebasableBridged__factory.createInterface().encodeFunctionData( "initialize", [l2TokenRebasableName, l2TokenRebasableSymbol] ), diff --git a/utils/optimism/testing.ts b/utils/optimism/testing.ts index 97de55c4..af114de0 100644 --- a/utils/optimism/testing.ts +++ b/utils/optimism/testing.ts @@ -14,7 +14,7 @@ import { L1LidoTokensBridge__factory, L2ERC20ExtendedTokensBridge__factory, CrossDomainMessengerStub__factory, - ERC20Rebasable__factory, + ERC20RebasableBridged__factory, } from "../../typechain"; import addresses from "./addresses"; import contracts from "./contracts"; @@ -280,7 +280,7 @@ function connectBridgeContracts( addresses.l2Token, optSignerOrProvider ); - const l2TokenRebasable = ERC20Rebasable__factory.connect( + const l2TokenRebasable = ERC20RebasableBridged__factory.connect( addresses.l2TokenRebasable, optSignerOrProvider );