From 86788b201d8f8689f7a3359dac528039b19146d2 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 27 Dec 2023 23:26:12 +0100 Subject: [PATCH] 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