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); -}