From 0e222ec44014c3a2337c7af3faef08a0d13fc218 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Mon, 22 Apr 2024 01:03:40 +0400 Subject: [PATCH] change filters for updating token rate --- contracts/optimism/TokenRateOracle.sol | 133 ++++++++++++++++++------- 1 file changed, 96 insertions(+), 37 deletions(-) diff --git a/contracts/optimism/TokenRateOracle.sol b/contracts/optimism/TokenRateOracle.sol index de40eadc..ddb1a187 100644 --- a/contracts/optimism/TokenRateOracle.sol +++ b/contracts/optimism/TokenRateOracle.sol @@ -6,12 +6,15 @@ pragma solidity 0.8.10; import {ITokenRateUpdatable} from "./interfaces/ITokenRateUpdatable.sol"; import {IChainlinkAggregatorInterface} from "./interfaces/IChainlinkAggregatorInterface.sol"; import {CrossDomainEnabled} from "./CrossDomainEnabled.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {Versioned} from "../utils/Versioned.sol"; interface ITokenRateOracle is ITokenRateUpdatable, IChainlinkAggregatorInterface {} /// @author kovalgek /// @notice Oracle for storing token rate. -contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { +/// @dev Token rate updates can be delivered from two sources: L1 token rate pusher and L2 bridge. +contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle, Versioned, Initializable { /// @dev Stores the dynamic data of the oracle. Allows safely use of this /// contract with upgradable proxies @@ -35,14 +38,20 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { /// @notice A time period when token rate can be considered outdated. uint256 public immutable TOKEN_RATE_OUTDATED_DELAY; + /// @notice A time period when token rate can be considered outdated. + uint256 public constant MAX_ALLOWED_L1_L2_TIME_DIFFERENCE = 86400; + + /// @notice Number of seconds in one day. + uint256 public constant ONE_DAY_SECONDS = 86400; + + /// @notice Allowed token rate deviation per day in basic points. + uint256 public constant TOKEN_RATE_DEVIATION = 500; + /// @notice Decimals of the oracle response. uint8 public constant DECIMALS = 18; - /// @notice The minimum value that the token rate can be - uint256 public constant MIN_TOKEN_RATE = 1_000_000_000_000_000; // 0.001 - - /// @notice The maximum value that the token rate can be. - uint256 public constant MAX_TOKEN_RATE = 1_000_000_000_000_000_000_000; // 1000 + /// @notice Basic point scale. + uint256 private constant BASIS_POINT_SCALE = 1e4; /// @param messenger_ L2 messenger address being used for cross-chain communications /// @param l2ERC20TokenBridge_ the bridge address that has a right to updates oracle. @@ -57,6 +66,13 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { L2_ERC20_TOKEN_BRIDGE = l2ERC20TokenBridge_; L1_TOKEN_RATE_PUSHER = l1TokenRatePusher_; TOKEN_RATE_OUTDATED_DELAY = tokenRateOutdatedDelay_; + + _disableInitializers(); + } + + function initialize(uint256 tokenRate_, uint256 rateL1Timestamp_) external initializer { + _initializeContractVersionTo(1); + _updateRate(tokenRate_, rateL1Timestamp_); } /// @inheritdoc IChainlinkAggregatorInterface @@ -67,20 +83,20 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { uint256 updatedAt_, uint80 answeredInRound_ ) { - uint80 roundId = uint80(_loadTokenRateData().rateL1Timestamp); + uint80 roundId = uint80(_rateL1Timestamp()); return ( roundId, - int256(uint(_loadTokenRateData().tokenRate)), - _loadTokenRateData().rateL1Timestamp, - _loadTokenRateData().rateL1Timestamp, + int256(uint256(_tokenRate())), + _rateL1Timestamp(), + _rateL1Timestamp(), roundId ); } /// @inheritdoc IChainlinkAggregatorInterface function latestAnswer() external view returns (int256) { - return int256(uint(_loadTokenRateData().tokenRate)); + return int256(uint256(_tokenRate())); } /// @inheritdoc IChainlinkAggregatorInterface @@ -89,45 +105,41 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { } /// @inheritdoc ITokenRateUpdatable - function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external { + function updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) external onlyAuthorized { + _updateRate(tokenRate_, rateL1Timestamp_); + } - if (!_isAuthorized(msg.sender)) { - revert ErrorNoRights(msg.sender); - } + /// @notice Returns flag that shows that token rate can be considered outdated. + function isLikelyOutdated() external view returns (bool) { + return block.timestamp - _rateL1Timestamp() > TOKEN_RATE_OUTDATED_DELAY; + } - if (rateL1Timestamp_ < _loadTokenRateData().rateL1Timestamp) { - emit NewTokenRateOutdated(tokenRate_, _loadTokenRateData().rateL1Timestamp, rateL1Timestamp_); + function _updateRate(uint256 tokenRate_, uint256 rateL1Timestamp_) internal { + if (rateL1Timestamp_ < _rateL1Timestamp()) { + emit ATryToUpdateTokenRateWithOutdatedTime(tokenRate_, _rateL1Timestamp(), rateL1Timestamp_); return; } + if (rateL1Timestamp_ - block.timestamp > MAX_ALLOWED_L1_L2_TIME_DIFFERENCE) { + revert ErrorInvalidTime(tokenRate_, rateL1Timestamp_); + } + if (rateL1Timestamp_ > block.timestamp) { - revert ErrorL1TimestampInFuture(tokenRate_, rateL1Timestamp_); + emit TokenRateL1TimestampAheadOfL2Time(); } - if (tokenRate_ < MIN_TOKEN_RATE || tokenRate_ > MAX_TOKEN_RATE) { + if (!_isTokenRateWithinAllowedRange(tokenRate_, rateL1Timestamp_)) { revert ErrorTokenRateIsOutOfRange(tokenRate_, rateL1Timestamp_); } - if (tokenRate_ == _loadTokenRateData().tokenRate && rateL1Timestamp_ == _loadTokenRateData().rateL1Timestamp) { + if (tokenRate_ == _tokenRate() && rateL1Timestamp_ == _rateL1Timestamp()) { return; } - _loadTokenRateData().tokenRate = uint192(tokenRate_); - _loadTokenRateData().rateL1Timestamp = uint64(rateL1Timestamp_); - - emit RateUpdated(_loadTokenRateData().tokenRate, _loadTokenRateData().rateL1Timestamp); - } - - /// @notice Returns flag that shows that token rate can be considered outdated. - function isLikelyOutdated() external view returns (bool) { - return block.timestamp - _loadTokenRateData().rateL1Timestamp > TOKEN_RATE_OUTDATED_DELAY; - } + _setTokenRate(uint192(tokenRate_)); + _setRateL1Timestamp(uint64(rateL1Timestamp_)); - function _isAuthorized(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; + emit RateUpdated(_tokenRate(), _rateL1Timestamp()); } /// @dev Returns the reference to the slot with TokenRateData struct @@ -142,10 +154,57 @@ contract TokenRateOracle is CrossDomainEnabled, ITokenRateOracle { } } + function _isTokenRateWithinAllowedRange(uint256 tokenRate_, uint256 rateL1Timestamp_) internal view returns (bool) { + uint256 rateL1TimestampDiff = rateL1Timestamp_ - _rateL1Timestamp(); + uint256 roundedUpNumberOfDays = rateL1TimestampDiff / ONE_DAY_SECONDS + 1; + uint256 allowedTokenRateDeviation = roundedUpNumberOfDays * TOKEN_RATE_DEVIATION; + uint256 topTokenRateLimitFactor = 1 + allowedTokenRateDeviation / BASIS_POINT_SCALE; + uint256 bottomTokenRateLimitFactor = + allowedTokenRateDeviation <= BASIS_POINT_SCALE ? + (1 - allowedTokenRateDeviation / BASIS_POINT_SCALE) : 0; + + return tokenRate_ <= _tokenRate() * topTokenRateLimitFactor && + tokenRate_ >= _tokenRate() * bottomTokenRateLimitFactor; + } + + function _isAuthorized(address caller_) internal view returns (bool) { + if(caller_ == address(MESSENGER) && MESSENGER.xDomainMessageSender() == L1_TOKEN_RATE_PUSHER) { + return true; + } + if(caller_ == L2_ERC20_TOKEN_BRIDGE) { + return true; + } + return false; + } + + function _tokenRate() private view returns (uint192) { + return _loadTokenRateData().tokenRate; + } + + function _rateL1Timestamp() private view returns (uint64) { + return _loadTokenRateData().rateL1Timestamp; + } + + function _setTokenRate(uint192 tokenRate_) internal { + _loadTokenRateData().tokenRate = tokenRate_; + } + + function _setRateL1Timestamp(uint64 rateL1Timestamp_) internal { + _loadTokenRateData().rateL1Timestamp = rateL1Timestamp_; + } + + modifier onlyAuthorized() { + if(!_isAuthorized(msg.sender)) { + revert ErrorNoRights(msg.sender); + } + _; + } + event RateUpdated(uint256 tokenRate_, uint256 rateL1Timestamp_); - event NewTokenRateOutdated(uint256 tokenRate_, uint256 rateL1Timestamp_, uint256 newTateL1Timestamp_); + event ATryToUpdateTokenRateWithOutdatedTime(uint256 tokenRate_, uint256 rateL1Timestamp_, uint256 newTateL1Timestamp_); + event TokenRateL1TimestampAheadOfL2Time(); error ErrorNoRights(address caller); - error ErrorL1TimestampInFuture(uint256 tokenRate_, uint256 rateL1Timestamp_); + error ErrorInvalidTime(uint256 tokenRate_, uint256 rateL1Timestamp_); error ErrorTokenRateIsOutOfRange(uint256 tokenRate_, uint256 rateL1Timestamp_); }