From aa3a0a30d63402b0775c5b82b86e053e643825c7 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Fri, 29 Aug 2025 08:36:25 +0200 Subject: [PATCH 01/13] Etherex volatile twap --- contracts/interfaces/IEtherexPair.sol | 125 +++++++++++++ contracts/interfaces/ITwapOracle.sol | 7 + .../core/twaps/EtherexVolatileTwap.sol | 175 ++++++++++++++++++ tests/foundry/TwapOracle.t.sol | 68 +++++++ 4 files changed, 375 insertions(+) create mode 100644 contracts/interfaces/IEtherexPair.sol create mode 100644 contracts/interfaces/ITwapOracle.sol create mode 100644 contracts/protocol/core/twaps/EtherexVolatileTwap.sol create mode 100644 tests/foundry/TwapOracle.t.sol diff --git a/contracts/interfaces/IEtherexPair.sol b/contracts/interfaces/IEtherexPair.sol new file mode 100644 index 0000000..f24db5f --- /dev/null +++ b/contracts/interfaces/IEtherexPair.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IEtherexPair { + struct Observation { + uint256 timestamp; + uint256 reserve0Cumulative; + uint256 reserve1Cumulative; + } + + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function currentCumulativePrices() + external + view + returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + + function observationLength() external view returns (uint256); + + function lastObservation() external view returns (Observation memory); + + function observations(uint256 index) external view returns (Observation memory); + + /// @notice initialize the pool, called only once programatically + function initialize(address _token0, address _token1, bool _stable) external; + + /// @notice calculate the current reserves of the pool and their last 'seen' timestamp + /// @return _reserve0 amount of token0 in reserves + /// @return _reserve1 amount of token1 in reserves + /// @return _blockTimestampLast the timestamp when the pool was last updated + function getReserves() + external + view + returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast); + + /// @notice mint the pair tokens (LPs) + /// @param to where to mint the LP tokens to + /// @return liquidity amount of LP tokens to mint + function mint(address to) external returns (uint256 liquidity); + + /// @notice burn the pair tokens (LPs) + /// @param to where to send the underlying + /// @return amount0 amount of amount0 + /// @return amount1 amount of amount1 + function burn(address to) external returns (uint256 amount0, uint256 amount1); + + /// @notice direct swap through the pool + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) + external; + + /// @notice force balances to match reserves, can be used to harvest rebases from rebasing tokens or other external factors + /// @param to where to send the excess tokens to + function skim(address to) external; + + /// @notice force reserves to match balances, prevents skim excess if skim is enabled + function sync() external; + + /// @notice set the pair fees contract address + function setFeeRecipient(address _pairFees) external; + + /// @notice set the feesplit variable + function setFeeSplit(uint256 _feeSplit) external; + + /// @notice sets the swap fee of the pair + /// @dev scaled to a max of 50% (500_000/1_000_000) + /// @param _fee the fee + function setFee(uint256 _fee) external; + + /// @notice 'mint' the fees as LP tokens + /// @dev this is used for protocol/voter fees + function mintFee() external; + + /// @notice calculates the amount of tokens to receive post swap + /// @param amountIn the token amount + /// @param tokenIn the address of the token + function getAmountOut(uint256 amountIn, address tokenIn) + external + view + returns (uint256 amountOut); + + /// @notice returns various metadata about the pair + function metadata() + external + view + returns ( + uint256 _decimals0, + uint256 _decimals1, + uint256 _reserve0, + uint256 _reserve1, + bool _stable, + address _token0, + address _token1 + ); + + /// @notice returns the feeSplit of the pair + function feeSplit() external view returns (uint256); + + /// @notice returns the fee of the pair + function fee() external view returns (uint256); + + /// @notice returns the feeRecipient of the pair + function feeRecipient() external view returns (address); + + /// @notice returns the token0 of the pair + function token0() external view returns (address); + + /// @notice returns the token1 of the pair + function token1() external view returns (address); + + /// @notice returns if pair is stable + function stable() external view returns (bool); + + /// @notice returns kLast + function kLast() external view returns (uint256); +} diff --git a/contracts/interfaces/ITwapOracle.sol b/contracts/interfaces/ITwapOracle.sol new file mode 100644 index 0000000..33b99fc --- /dev/null +++ b/contracts/interfaces/ITwapOracle.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.13; + +interface ITwapOracle { + function getAssetPrice(address _asset) external view returns (uint256); + function getTokens() external view returns (address token0, address token1); +} diff --git a/contracts/protocol/core/twaps/EtherexVolatileTwap.sol b/contracts/protocol/core/twaps/EtherexVolatileTwap.sol new file mode 100644 index 0000000..132340e --- /dev/null +++ b/contracts/protocol/core/twaps/EtherexVolatileTwap.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.13; + +import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; +import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; + +/// @title Oracle using Thena TWAP oracle as data source +/// @author zefram.eth/lookee/Eidolon +/// @notice The oracle contract that provides the current price to purchase +/// the underlying token while exercising options. Uses Thena TWAP oracle +/// as data source, and then applies a lower bound. +contract EtherexVolatileTwap is ITwapOracle, Ownable { + /// ----------------------------------------------------------------------- + /// Library usage + /// ----------------------------------------------------------------------- + + using FixedPointMathLib for uint256; + + /// ----------------------------------------------------------------------- + /// Errors + /// ----------------------------------------------------------------------- + + error ThenaOracle__InvalidParams(); + error ThenaOracle__InvalidWindow(); + error ThenaOracle__StablePairsUnsupported(); + error ThenaOracle__Overflow(); + error ThenaOracle__BelowMinPrice(); + + /// ----------------------------------------------------------------------- + /// Events + /// ----------------------------------------------------------------------- + + event SetParams(uint56 secs, uint128 minPrice); + + /// ----------------------------------------------------------------------- + /// Immutable parameters + /// ----------------------------------------------------------------------- + uint256 internal constant WAD = 1e18; + uint256 internal constant MIN_SECS = 20 minutes; + + /// @notice The Thena TWAP oracle contract (usually a pool with oracle support) + IEtherexPair public immutable etherexPair; + + /// ----------------------------------------------------------------------- + /// Storage variables + /// ----------------------------------------------------------------------- + + /// @notice The size of the window to take the TWAP value over in seconds. + uint56 public secs; + + /// @notice The minimum value returned by getPrice(). Maintains a floor for the + /// price to mitigate potential attacks on the TWAP oracle. + uint128 public minPrice; + + /// @notice Whether the price should be returned in terms of token0. + /// If false, the price is returned in terms of token1. + bool public isToken0; + + /// ----------------------------------------------------------------------- + /// Constructor + /// ----------------------------------------------------------------------- + + constructor( + IEtherexPair etherexPair_, + address token, + address owner_, + uint56 secs_, + uint128 minPrice_ + ) Ownable(owner_) { + if ( + ERC20(etherexPair_.token0()).decimals() != 18 + || ERC20(etherexPair_.token1()).decimals() != 18 + ) revert ThenaOracle__InvalidParams(); + if (etherexPair_.stable()) revert ThenaOracle__StablePairsUnsupported(); + if (etherexPair_.token0() != token && etherexPair_.token1() != token) { + revert ThenaOracle__InvalidParams(); + } + if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); + + etherexPair = etherexPair_; + isToken0 = etherexPair_.token0() == token; + secs = secs_; + minPrice = minPrice_; + + emit SetParams(secs_, minPrice_); + } + + /// ----------------------------------------------------------------------- + /// IOracle + /// ----------------------------------------------------------------------- + + /// @inheritdoc ITwapOracle + function getAssetPrice(address _asset) external view override returns (uint256 price) { + if (_asset != etherexPair.token0() && _asset != etherexPair.token1()) { + revert ThenaOracle__InvalidParams(); + } + /// ----------------------------------------------------------------------- + /// Storage loads + /// ----------------------------------------------------------------------- + + uint256 secs_ = secs; + + /// ----------------------------------------------------------------------- + /// Computation + /// ----------------------------------------------------------------------- + + // query Thena oracle to get TWAP value + { + ( + uint256 reserve0CumulativeCurrent, + uint256 reserve1CumulativeCurrent, + uint256 blockTimestampCurrent + ) = etherexPair.currentCumulativePrices(); + uint256 observationLength = etherexPair.observationLength(); + IEtherexPair.Observation memory lastObs = etherexPair.lastObservation(); + + uint32 T = uint32(blockTimestampCurrent - lastObs.timestamp); + if (T < secs_) { + lastObs = etherexPair.observations(observationLength - 2); + T = uint32(blockTimestampCurrent - lastObs.timestamp); + } + uint112 reserve0 = safe112((reserve0CumulativeCurrent - lastObs.reserve0Cumulative) / T); + uint112 reserve1 = safe112((reserve1CumulativeCurrent - lastObs.reserve1Cumulative) / T); + + if (!isToken0) { + price = uint256(reserve0) * WAD / (reserve1); + } else { + price = uint256(reserve1) * WAD / (reserve0); + } + } + + if (price < minPrice) revert ThenaOracle__BelowMinPrice(); + } + + /// @inheritdoc ITwapOracle + function getTokens() + external + view + override + returns (address paymentToken, address underlyingToken) + { + if (isToken0) { + return (etherexPair.token1(), etherexPair.token0()); + } else { + return (etherexPair.token0(), etherexPair.token1()); + } + } + + /// ----------------------------------------------------------------------- + /// Owner functions + /// ----------------------------------------------------------------------- + + /// @notice Updates the oracle parameters. Only callable by the owner. + /// @param secs_ The size of the window to take the TWAP value over in seconds. + /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the + /// price to mitigate potential attacks on the TWAP oracle. + function setParams(uint56 secs_, uint128 minPrice_) external onlyOwner { + if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); + secs = secs_; + minPrice = minPrice_; + emit SetParams(secs_, minPrice_); + } + + /// ----------------------------------------------------------------------- + /// Util functions + /// ----------------------------------------------------------------------- + + function safe112(uint256 n) internal pure returns (uint112) { + if (n >= 2 ** 112) revert ThenaOracle__Overflow(); + return uint112(n); + } +} diff --git a/tests/foundry/TwapOracle.t.sol b/tests/foundry/TwapOracle.t.sol new file mode 100644 index 0000000..c83a3af --- /dev/null +++ b/tests/foundry/TwapOracle.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "./Common.sol"; +import "contracts/protocol/libraries/helpers/Errors.sol"; +import {WadRayMath} from "contracts/protocol/libraries/math/WadRayMath.sol"; +import {EtherexVolatileTwap} from "contracts/protocol/core/twaps/EtherexVolatileTwap.sol"; + +contract TwapOracle is Common { + using WadRayMath for uint256; + + ERC20[] erc20Tokens; + DeployedContracts deployedContracts; + ConfigAddresses configAddresses; + + function setUp() public { + opFork = vm.createSelectFork(RPC, FORK_BLOCK); + assertEq(vm.activeFork(), opFork); + deployedContracts = fixture_deployProtocol(); + configAddresses = ConfigAddresses( + address(deployedContracts.asteraDataProvider), + address(deployedContracts.stableStrategy), + address(deployedContracts.volatileStrategy), + address(deployedContracts.treasury), + address(deployedContracts.rewarder), + address(deployedContracts.aTokensAndRatesHelper) + ); + fixture_configureProtocol( + address(deployedContracts.lendingPool), + address(commonContracts.aToken), + configAddresses, + deployedContracts.lendingPoolConfigurator, + deployedContracts.lendingPoolAddressesProvider + ); + commonContracts.mockedVaults = + fixture_deployReaperVaultMocks(tokens, address(deployedContracts.treasury)); + erc20Tokens = fixture_getErc20Tokens(tokens); + fixture_transferTokensToTestContract(erc20Tokens, 100_000 ether, address(this)); + } + + function testSetFallbackOracle() public { + ERC20[] memory erc20tokens = fixture_getErc20Tokens(tokens); + int256[] memory prices = new int256[](4); + uint256[] memory timeouts = new uint256[](4); + // All chainlink price feeds have 8 decimals + prices[0] = int256(95 * 10 ** PRICE_FEED_DECIMALS - 1); // USDC + prices[1] = int256(63_000 * 10 ** PRICE_FEED_DECIMALS); // WBTC + prices[2] = int256(3300 * 10 ** PRICE_FEED_DECIMALS); // ETH + prices[3] = int256(95 * 10 ** PRICE_FEED_DECIMALS - 1); // DAI + (, commonContracts.aggregators, timeouts) = fixture_getTokenPriceFeeds(erc20tokens, prices); + + // EtherexVolatileTwap etherexVolatileTwap = + // new EtherexVolatileTwap(address(tokens[0]), address(tokens[1]), 30 minutes, 1e18, true); + + Oracle oracle = new Oracle( + tokens, + commonContracts.aggregators, + timeouts, + address(0), + address(0), + BASE_CURRENCY_UNIT, + address(deployedContracts.lendingPoolAddressesProvider) + ); + + // commonContracts.oracle.setFallbackOracle(address(etherexVolatileTwap)); + // assertEq(address(etherexVolatileTwap), commonContracts.oracle.getFallbackOracle()); + } +} From de5db89cb8ba46d6e4e980a6c9cef392e1c4a11e Mon Sep 17 00:00:00 2001 From: xRave110 Date: Sun, 7 Sep 2025 12:45:11 +0200 Subject: [PATCH 02/13] Deployment improvements + base PI strat change --- .../BasePiReserveRateStrategy.sol | 4 +- foundry.toml | 2 +- scripts/3_AddStrats.s.sol | 35 +++++++++++ scripts/DeployDataTypes.sol | 5 ++ .../helpers/InitAndConfigurationHelper.s.sol | 3 - scripts/inputs/3_StratsToAdd.json | 54 +++++------------ scripts/inputs/5_Reconfigure.json | 59 +++++++++---------- scripts/inputs/7_TransferOwnerships.json | 2 +- scripts/localFork/3_AddStratsLocal.s.sol | 11 ++++ 9 files changed, 98 insertions(+), 77 deletions(-) diff --git a/contracts/protocol/core/interestRateStrategies/BasePiReserveRateStrategy.sol b/contracts/protocol/core/interestRateStrategies/BasePiReserveRateStrategy.sol index 4fe789e..67dd0ec 100644 --- a/contracts/protocol/core/interestRateStrategies/BasePiReserveRateStrategy.sol +++ b/contracts/protocol/core/interestRateStrategies/BasePiReserveRateStrategy.sol @@ -25,9 +25,9 @@ abstract contract BasePiReserveRateStrategy is Ownable { using PercentageMath for uint256; /// @dev Multiplier factor used in interest rate calculations. - int256 public constant M_FACTOR = 100e25; + int256 public constant M_FACTOR = 20e25; /// @dev Power factor used in interest rate calculations. - uint256 public constant N_FACTOR = 4; + uint256 public constant N_FACTOR = 2; /// @dev Ray precision constant (1e27). int256 public constant RAY = 1e27; diff --git a/foundry.toml b/foundry.toml index a4ad598..d705aa6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,7 @@ ignored_warnings_from = ["tests/"] optimizer = true optimizer_runs = 500 solc_version = '0.8.23' -#evm_version = "paris" # uncomment when problems with PUSH0 +evm_version = "paris" # uncomment when problems with PUSH0 show_progress = true fs_permissions = [{ access = "read-write", path = "./"}] ignored_error_codes = [3628] diff --git a/scripts/3_AddStrats.s.sol b/scripts/3_AddStrats.s.sol index e1e5578..6ad85d1 100644 --- a/scripts/3_AddStrats.s.sol +++ b/scripts/3_AddStrats.s.sol @@ -85,6 +85,7 @@ contract AddStrats is Script, StratsHelper, Test { PoolAddressesProviderConfig memory poolAddressesProviderConfig = abi.decode( config.parseRaw(".poolAddressesProviderConfig"), (PoolAddressesProviderConfig) ); + Factors memory factors = abi.decode(config.parseRaw(".factors"), (Factors)); uint256 miniPoolId = poolAddressesProviderConfig.poolId; LinearStrategy[] memory volatileStrategies = abi.decode(config.parseRaw(".volatileStrategies"), (LinearStrategy[])); @@ -242,6 +243,16 @@ contract AddStrats is Script, StratsHelper, Test { miniPoolStableStrategies, miniPoolPiStrategies ); + for (uint8 idx = 0; idx < miniPoolPiStrategies.length; idx++) { + require( + uint256(contracts.miniPoolPiStrategies[idx].M_FACTOR()) == factors.m_factor, + "Wrong M_FACTOR" + ); + require( + uint256(contracts.miniPoolPiStrategies[idx].N_FACTOR()) == factors.n_factor, + "Wrong N_FACTOR" + ); + } vm.stopBroadcast(); path = string.concat(root, "/scripts/outputs/testnet/3_DeployedStrategies.json"); } else if (vm.envBool("MAINNET")) { @@ -319,6 +330,8 @@ contract AddStrats is Script, StratsHelper, Test { contracts.miniPoolAddressesProvider = MiniPoolAddressesProvider(config.readAddress(".miniPoolAddressesProvider")); } + uint256 initialMiniPoolIndex = miniPoolPiStrategies.length; + uint256 initialPoolIndex = miniPoolPiStrategies.length; /* Deploy on the mainnet */ vm.startBroadcast(vm.envUint("PRIVATE_KEY")); _deployStrategies( @@ -336,6 +349,28 @@ contract AddStrats is Script, StratsHelper, Test { miniPoolPiStrategies ); } + /* Pi pool strats */ + for (uint256 idx = initialPoolIndex; idx < piStrategies.length; idx++) { + require( + uint256(contracts.piStrategies[idx].M_FACTOR()) == factors.m_factor, + "Wrong M_FACTOR" + ); + require( + uint256(contracts.piStrategies[idx].N_FACTOR()) == factors.n_factor, + "Wrong N_FACTOR" + ); + } + /* Pi miniPool strats */ + for (uint256 idx = initialMiniPoolIndex; idx < miniPoolPiStrategies.length; idx++) { + require( + uint256(contracts.miniPoolPiStrategies[idx].M_FACTOR()) == factors.m_factor, + "Wrong M_FACTOR" + ); + require( + uint256(contracts.miniPoolPiStrategies[idx].N_FACTOR()) == factors.n_factor, + "Wrong N_FACTOR" + ); + } vm.stopBroadcast(); path = string.concat(root, "/scripts/outputs/mainnet/3_DeployedStrategies.json"); } diff --git a/scripts/DeployDataTypes.sol b/scripts/DeployDataTypes.sol index ae15b0b..e4bd976 100644 --- a/scripts/DeployDataTypes.sol +++ b/scripts/DeployDataTypes.sol @@ -98,6 +98,11 @@ struct PoolAddressesProviderConfig { address poolOwner; } +struct Factors { + uint256 m_factor; + uint256 n_factor; +} + struct PoolReserversConfig { uint256 baseLtv; bool borrowingEnabled; diff --git a/scripts/helpers/InitAndConfigurationHelper.s.sol b/scripts/helpers/InitAndConfigurationHelper.s.sol index a165467..888d9d1 100644 --- a/scripts/helpers/InitAndConfigurationHelper.s.sol +++ b/scripts/helpers/InitAndConfigurationHelper.s.sol @@ -222,9 +222,6 @@ contract InitAndConfigurationHelper { _contracts.miniPoolConfigurator.batchInitReserve(initInputParams, IMiniPool(mp)); console2.log("Configuring"); _configureMiniPoolReserves(_contracts, _reservesConfig, mp, _usdBootstrapAmount); - if (_contracts.lendingPool.paused()) { - _contracts.lendingPoolConfigurator.setPoolPause(true); - } if (IMiniPool(mp).paused()) { _contracts.miniPoolConfigurator.setPoolPause(true, IMiniPool(mp)); } diff --git a/scripts/inputs/3_StratsToAdd.json b/scripts/inputs/3_StratsToAdd.json index 1a7e0c3..fca3766 100644 --- a/scripts/inputs/3_StratsToAdd.json +++ b/scripts/inputs/3_StratsToAdd.json @@ -1,8 +1,12 @@ { "poolAddressesProviderConfig": { "marketId": "UV TestNet Market", - "poolId": 1, - "poolOwner": "0xf298Db641560E5B733C43181937207482Ff79bc9" + "poolId": 3, + "poolOwner": "0x7D66a2e916d79c0988D41F1E50a1429074ec53a4" + }, + "factors": { + "m_factor": 20e25, + "n_factor": 2 }, "volatileStrategies": [], "stableStrategies": [], @@ -25,7 +29,7 @@ "ki": 13e19, "kp": 1e27, "maxITimeAmp": 1728000, - "minControllerError": -3001e23, + "minControllerError": -3675e23, "optimalUtilizationRate": 70e25, "symbol": "was-WETH", "tokenAddress": "0x9A4cA144F38963007cFAC645d77049a1Dd4b209A" @@ -35,7 +39,7 @@ "ki": 13e19, "kp": 1e27, "maxITimeAmp": 1728000, - "minControllerError": -2479e23, + "minControllerError": -3675e23, "optimalUtilizationRate": 80e25, "symbol": "was-USDC", "tokenAddress": "0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944" @@ -45,7 +49,7 @@ "ki": 13e19, "kp": 1e27, "maxITimeAmp": 1728000, - "minControllerError": -2479e23, + "minControllerError": -3675e23, "optimalUtilizationRate": 80e25, "symbol": "was-USDT", "tokenAddress": "0x1579072d23FB3f545016Ac67E072D37e1281624C" @@ -55,7 +59,7 @@ "ki": 13e19, "kp": 1e27, "maxITimeAmp": 1728000, - "minControllerError": -2479e23, + "minControllerError": -3675e23, "optimalUtilizationRate": 80e25, "symbol": "asUSD", "tokenAddress": "0xa500000000e482752f032eA387390b6025a2377b" @@ -65,40 +69,10 @@ "ki": 13e19, "kp": 1e27, "maxITimeAmp": 1728000, - "minControllerError": -3001e23, - "optimalUtilizationRate": 60e25, - "symbol": "wstETH", - "tokenAddress": "0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F" - }, - { - "assetReserveType": true, - "ki": 13e19, - "kp": 1e27, - "maxITimeAmp": 1728000, - "minControllerError": -3001e23, - "optimalUtilizationRate": 60e25, - "symbol": "weETH", - "tokenAddress": "0x1Bf74C010E6320bab11e2e5A532b5AC15e0b8aA6" - }, - { - "assetReserveType": true, - "ki": 13e19, - "kp": 1e27, - "maxITimeAmp": 1728000, - "minControllerError": -3001e23, - "optimalUtilizationRate": 60e25, - "symbol": "ezETH", - "tokenAddress": "0x2416092f143378750bb29b79eD961ab195CcEea5" - }, - { - "assetReserveType": true, - "ki": 13e19, - "kp": 1e27, - "maxITimeAmp": 1728000, - "minControllerError": -3001e23, - "optimalUtilizationRate": 60e25, - "symbol": "wrsETH", - "tokenAddress": "0xD2671165570f41BBB3B0097893300b6EB6101E6C" + "minControllerError": -3675e23, + "optimalUtilizationRate": 80e25, + "symbol": "mUSD", + "tokenAddress": "0xacA92E438df0B2401fF60dA7E4337B687a2435DA" } ] } \ No newline at end of file diff --git a/scripts/inputs/5_Reconfigure.json b/scripts/inputs/5_Reconfigure.json index f1d461c..1a04378 100644 --- a/scripts/inputs/5_Reconfigure.json +++ b/scripts/inputs/5_Reconfigure.json @@ -1,53 +1,52 @@ { "poolAddressesProviderConfig": { "marketId": "UV TestNet Market", - "poolId": 0, - "poolOwner": "0xf298Db641560E5B733C43181937207482Ff79bc9" + "poolId": 3, + "poolOwner": "0x7D66a2e916d79c0988D41F1E50a1429074ec53a4" }, - "lendingPoolReserversConfig": [ + "lendingPoolReserversConfig": [], + "miniPoolReserversConfig": [ { - "baseLtv": 7000, + "baseLtv": 7500, "borrowingEnabled": true, - "interestStrat": "STABLE", + "interestStrat": "PI", "interestStratId": 0, - "liquidationBonus": 10500, - "liquidationThreshold": 7400, - "miniPoolOwnerFee": 230, + "liquidationBonus": 10800, + "liquidationThreshold": 8000, + "miniPoolOwnerFee": 0, "params": "0x10", - "reserveFactor": 1500, + "reserveFactor": 2000, "reserveType": true, - "symbol": "USDC", - "tokenAddress": "0x8c53BF01E052A2aFA06b2d9d87aE49972799E62f" - } - ], - "miniPoolReserversConfig": [ + "symbol": "was-WBTC", + "tokenAddress": "0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b" + }, { - "baseLtv": 7000, + "baseLtv": 7500, "borrowingEnabled": true, "interestStrat": "PI", - "interestStratId": 0, - "liquidationBonus": 10500, - "liquidationThreshold": 7400, - "miniPoolOwnerFee": 230, + "interestStratId": 1, + "liquidationBonus": 10800, + "liquidationThreshold": 8000, + "miniPoolOwnerFee": 0, "params": "0x10", - "reserveFactor": 1500, + "reserveFactor": 2000, "reserveType": true, - "symbol": "aUSDC", - "tokenAddress": "0x088607f8f2aAE9Ab9b7EE16C988A5Ca857DB8F15" + "symbol": "was-WETH", + "tokenAddress": "0x9A4cA144F38963007cFAC645d77049a1Dd4b209A" }, { - "baseLtv": 7000, + "baseLtv": 8500, "borrowingEnabled": true, - "interestStrat": "VOLATILE", - "interestStratId": 0, - "liquidationBonus": 10500, - "liquidationThreshold": 7500, + "interestStrat": "PI", + "interestStratId": 2, + "liquidationBonus": 10800, + "liquidationThreshold": 9000, "miniPoolOwnerFee": 0, "params": "0x10", - "reserveFactor": 1500, + "reserveFactor": 2000, "reserveType": true, - "symbol": "WBTC", - "tokenAddress": "0xb3e343C39a9e5e739724b3Ed0F3B6150e19B4A90" + "symbol": "was-USDC", + "tokenAddress": "0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944" } ] } \ No newline at end of file diff --git a/scripts/inputs/7_TransferOwnerships.json b/scripts/inputs/7_TransferOwnerships.json index 1febea7..5d2c6bc 100644 --- a/scripts/inputs/7_TransferOwnerships.json +++ b/scripts/inputs/7_TransferOwnerships.json @@ -10,7 +10,7 @@ "rewarderOwner": "0x7D66a2e916d79c0988D41F1E50a1429074ec53a4" }, "miniPoolRole": { - "miniPoolId": 1, + "miniPoolId": 2, "newPoolOwner": "0x7D66a2e916d79c0988D41F1E50a1429074ec53a4", "poolOwnerTreasury": "0x7D66a2e916d79c0988D41F1E50a1429074ec53a4" } diff --git a/scripts/localFork/3_AddStratsLocal.s.sol b/scripts/localFork/3_AddStratsLocal.s.sol index 4cce2ca..1ea3f32 100644 --- a/scripts/localFork/3_AddStratsLocal.s.sol +++ b/scripts/localFork/3_AddStratsLocal.s.sol @@ -88,6 +88,7 @@ contract AddStratsLocal is Script, StratsHelper, Test { PoolAddressesProviderConfig memory poolAddressesProviderConfig = abi.decode( deploymentConfig.parseRaw(".poolAddressesProviderConfig"), (PoolAddressesProviderConfig) ); + Factors memory factors = abi.decode(deploymentConfig.parseRaw(".factors"), (Factors)); uint256 miniPoolId = poolAddressesProviderConfig.poolId; LinearStrategy[] memory volatileStrategies = abi.decode(deploymentConfig.parseRaw(".volatileStrategies"), (LinearStrategy[])); @@ -127,6 +128,16 @@ contract AddStratsLocal is Script, StratsHelper, Test { miniPoolStableStrategies, miniPoolPiStrategies ); + /* Pi miniPool strats */ + address[] memory tmpStrats = deployedStrategies.readAddressArray(".miniPoolPiStrategies"); + for (uint8 idx = 0; idx < miniPoolPiStrategies.length; idx++) { + require( + contracts.miniPoolPiStrategies[idx].M_FACTOR() == factors.m_factor, "Wrong M_FACTOR" + ); + require( + contracts.miniPoolPiStrategies[idx].N_FACTOR() == factors.n_factor, "Wrong N_FACTOR" + ); + } vm.stopPrank(); writeJsonData(root, path); From d9d9296ba08e9e2d30886398317352678b370dc9 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Mon, 8 Sep 2025 12:52:09 +0200 Subject: [PATCH 03/13] Rewarder deployment --- scripts/6a_DeployRewarder.s.sol | 140 ++++++++++++ scripts/DeployDataTypes.sol | 10 + scripts/inputs/6a_Rewarder.json | 49 +++++ tests/foundry/MiniPoolRewarderLinea.t.sol | 255 ++++++++++++++++++++++ 4 files changed, 454 insertions(+) create mode 100644 scripts/6a_DeployRewarder.s.sol create mode 100644 scripts/inputs/6a_Rewarder.json create mode 100644 tests/foundry/MiniPoolRewarderLinea.t.sol diff --git a/scripts/6a_DeployRewarder.s.sol b/scripts/6a_DeployRewarder.s.sol new file mode 100644 index 0000000..b069a89 --- /dev/null +++ b/scripts/6a_DeployRewarder.s.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: BUSL 1.1 +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; +// import "lib/forge-std/src/Test.sol"; +import {RewardedTokenConfig} from "./DeployDataTypes.sol"; +import {DistributionTypes} from "contracts/protocol/libraries/types/DistributionTypes.sol"; +import "lib/forge-std/src/console2.sol"; +import {ILendingPoolAddressesProvider} from "contracts/interfaces/ILendingPoolAddressesProvider.sol"; +import {IMiniPoolAddressesProvider} from "contracts/interfaces/IMiniPoolAddressesProvider.sol"; +import {IMiniPoolConfigurator} from "contracts/interfaces/IMiniPoolConfigurator.sol"; +import {IAsteraDataProvider2} from "contracts/interfaces/IAsteraDataProvider2.sol"; +import {Rewarder6909} from "contracts/protocol/rewarder/minipool/Rewarder6909.sol"; +import {ATokenERC6909} from "contracts/protocol/tokenization/ERC6909/ATokenERC6909.sol"; +import {RewardsVault} from "contracts/misc/RewardsVault.sol"; +import {IMiniPool} from "contracts/interfaces/IMiniPool.sol"; + +contract DeployRewarder is Script { + using stdJson for string; + + address constant ORACLE = 0xd971e9EC7357e9306c2a138E5c4eAfC04d241C87; + ILendingPoolAddressesProvider lendingPoolAddressesProvider = + ILendingPoolAddressesProvider(0x9a460e7BD6D5aFCEafbE795e05C48455738fB119); + IMiniPoolAddressesProvider miniPoolAddressesProvider = + IMiniPoolAddressesProvider(0x9399aF805e673295610B17615C65b9d0cE1Ed306); + IMiniPoolConfigurator miniPoolConfigurator = + IMiniPoolConfigurator(0x41296B58279a81E20aF1c05D32b4f132b72b1B01); + IAsteraDataProvider2 dataProvider = + IAsteraDataProvider2(0xE4FeC590F1Cf71B36c0A782Aac2E4589aFdaD88e); + + Rewarder6909 miniPoolRewarder; + RewardsVault[] rewardsVaults; + + function run() external { + // Config fetching + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/scripts/inputs/6a_Rewarder.json"); + console2.log("PATH: ", path); + string memory deploymentConfig = vm.readFile(path); + + RewardedTokenConfig[] memory rewardedTokenConfigs = + abi.decode(deploymentConfig.parseRaw(".rewardedTokenConfigs"), (RewardedTokenConfig[])); + + vm.startBroadcast(); + miniPoolRewarder = new Rewarder6909(); + vm.stopBroadcast(); + + for (uint256 idx = 0; idx < rewardedTokenConfigs.length; idx++) { + console2.log("rewarded Token: ", rewardedTokenConfigs[idx].rewardedToken); + console2.log("reward Token: ", rewardedTokenConfigs[idx].rewardToken); + vm.startBroadcast(); + rewardsVaults.push( + new RewardsVault( + address(miniPoolRewarder), + ILendingPoolAddressesProvider(lendingPoolAddressesProvider), + address(rewardedTokenConfigs[idx].rewardToken) + ) + ); + vm.stopBroadcast(); + require( + rewardedTokenConfigs[idx].incentivesAmount + >= rewardedTokenConfigs[idx].distributionTime + * rewardedTokenConfigs[idx].emissionPerSecond, + "Too small incentives amount" + ); + /* TODO via multisig !! */ + // rewardsVaults[idx].approveIncentivesController( + // rewardedTokenConfigs[idx].incentivesAmount + // ); + vm.startBroadcast(); + miniPoolRewarder.setRewardsVault( + address(rewardsVaults[idx]), address(rewardedTokenConfigs[idx].rewardToken) + ); + vm.stopBroadcast(); + + DistributionTypes.MiniPoolRewardsConfigInput[] memory configs = + new DistributionTypes.MiniPoolRewardsConfigInput[](1); + address aTokensErc6909Addr = + miniPoolAddressesProvider.getMiniPoolToAERC6909(rewardedTokenConfigs[idx].miniPool); + DistributionTypes.Asset6909 memory asset = + DistributionTypes.Asset6909(aTokensErc6909Addr, rewardedTokenConfigs[idx].assetId); + require( + type(uint88).max >= rewardedTokenConfigs[idx].emissionPerSecond, + "Wrong emissionPerSecond value" + ); + require( + type(uint32).max >= rewardedTokenConfigs[idx].distributionTime, + "Wrong distributionTime value" + ); + configs[0] = DistributionTypes.MiniPoolRewardsConfigInput( + uint88(rewardedTokenConfigs[idx].emissionPerSecond), + uint32(block.timestamp + rewardedTokenConfigs[idx].distributionTime), + asset, + address(rewardedTokenConfigs[idx].rewardToken) + ); + console2.log("%s Configuring assetID: %s", idx, rewardedTokenConfigs[idx].assetId); + vm.startBroadcast(); + miniPoolRewarder.configureAssets(configs); + vm.stopBroadcast(); + + console2.log( + "underlying from id: %s vs rewardedToken %s", + ATokenERC6909(aTokensErc6909Addr).getUnderlyingAsset( + rewardedTokenConfigs[idx].assetId + ), + rewardedTokenConfigs[idx].rewardedToken + ); + require( + ATokenERC6909(aTokensErc6909Addr).getUnderlyingAsset( + rewardedTokenConfigs[idx].assetId + ) == rewardedTokenConfigs[idx].rewardedToken, + "Wrong asset or id" + ); + + { + (uint256 aTokenId, uint256 debtTokenId,) = ATokenERC6909(aTokensErc6909Addr) + .getIdForUnderlying(rewardedTokenConfigs[idx].rewardedToken); + + require( + aTokenId == rewardedTokenConfigs[idx].assetId + || debtTokenId == rewardedTokenConfigs[idx].assetId, + "Wrong id or asset" + ); + } + + /* TODO via multisig !! */ + // miniPoolConfigurator.setRewarderForReserve( + // rewardedTokenConfigs[idx].rewardedToken, + // address(miniPoolRewarder), + // IMiniPool(rewardedTokenConfigs[idx].miniPool) + // ); + } + + console2.log("Deployed Rewarder: ", address(miniPoolRewarder)); + console2.log("Deployed Vaults:"); + for (uint256 i = 0; i < rewardsVaults.length; i++) { + console2.log(address(rewardsVaults[i])); + } + } +} diff --git a/scripts/DeployDataTypes.sol b/scripts/DeployDataTypes.sol index e4bd976..cf05b94 100644 --- a/scripts/DeployDataTypes.sol +++ b/scripts/DeployDataTypes.sol @@ -188,3 +188,13 @@ struct DataProvider { address marketReferenceCurrencyAggregator; address networkBaseTokenAggregator; } + +struct RewardedTokenConfig { + uint256 assetId; + uint256 distributionTime; + uint256 emissionPerSecond; + uint256 incentivesAmount; + address miniPool; + address rewardToken; + address rewardedToken; +} diff --git a/scripts/inputs/6a_Rewarder.json b/scripts/inputs/6a_Rewarder.json new file mode 100644 index 0000000..2b3c768 --- /dev/null +++ b/scripts/inputs/6a_Rewarder.json @@ -0,0 +1,49 @@ +{ + "rewardedTokenConfigs": [ + { + "assetId": 1002, + "distributionTime": 2592000, + "emissionPerSecond": 1e18, + "incentivesAmount": 2592000e18, + "miniPool": "0x65559abECD1227Cc1779F500453Da1f9fcADd928", + "rewardToken": "0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4", + "rewardedToken": "0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944" + }, + { + "assetId": 1001, + "distributionTime": 2592000, + "emissionPerSecond": 1e18, + "incentivesAmount": 2592000e18, + "miniPool": "0x65559abECD1227Cc1779F500453Da1f9fcADd928", + "rewardToken": "0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4", + "rewardedToken": "0x9A4cA144F38963007cFAC645d77049a1Dd4b209A" + }, + { + "assetId": 1001, + "distributionTime": 2592000, + "emissionPerSecond": 1e18, + "incentivesAmount": 2592000e18, + "miniPool": "0x0baFB30B72925e6d53F4d0A089bE1CeFbB5e3401", + "rewardToken": "0xa500000000e482752f032eA387390b6025a2377b", + "rewardedToken": "0x9A4cA144F38963007cFAC645d77049a1Dd4b209A" + }, + { + "assetId": 2001, + "distributionTime": 2592000, + "emissionPerSecond": 1e18, + "incentivesAmount": 2592000e18, + "miniPool": "0x0baFB30B72925e6d53F4d0A089bE1CeFbB5e3401", + "rewardToken": "0xa500000000e482752f032eA387390b6025a2377b", + "rewardedToken": "0x9A4cA144F38963007cFAC645d77049a1Dd4b209A" + }, + { + "assetId": 2128, + "distributionTime": 2592000, + "emissionPerSecond": 1e18, + "incentivesAmount": 2592000e18, + "miniPool": "0x0baFB30B72925e6d53F4d0A089bE1CeFbB5e3401", + "rewardToken": "0xa500000000e482752f032eA387390b6025a2377b", + "rewardedToken": "0xa500000000e482752f032eA387390b6025a2377b" + } + ] +} \ No newline at end of file diff --git a/tests/foundry/MiniPoolRewarderLinea.t.sol b/tests/foundry/MiniPoolRewarderLinea.t.sol new file mode 100644 index 0000000..6ba3421 --- /dev/null +++ b/tests/foundry/MiniPoolRewarderLinea.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "./Common.sol"; +import "contracts/protocol/libraries/helpers/Errors.sol"; +import "contracts/misc/RewardsVault.sol"; +import "contracts/protocol/rewarder/minipool/Rewarder6909.sol"; +import "contracts/mocks/tokens/MintableERC20.sol"; +import {DistributionTypes} from "contracts/protocol/libraries/types/DistributionTypes.sol"; +import {RewardForwarder} from "contracts/protocol/rewarder/lendingpool/RewardForwarder.sol"; +import "contracts/protocol/tokenization/ERC6909/ATokenERC6909.sol"; +import {MiniPoolUserReserveData} from "../../contracts/interfaces/IAsteraDataProvider.sol"; +import {IAsteraDataProvider2} from "../../contracts/interfaces/IAsteraDataProvider2.sol"; +import {AToken} from "../../contracts/protocol/tokenization/ERC20/AToken.sol"; +import "forge-std/StdUtils.sol"; + +contract MiniPoolRewarderTest is Common { + using WadRayMath for uint256; + + ERC20[] erc20Tokens; + Rewarder6909 miniPoolRewarder; + RewardsVault[] miniPoolRewardsVaults; + MintableERC20[] rewardTokens; + + ConfigAddresses configAddresses; + address aTokensErc6909Addr; + uint256 REWARDING_TOKENS_AMOUNT = 3; + + address constant ORACLE = 0xd971e9EC7357e9306c2a138E5c4eAfC04d241C87; + ILendingPoolAddressesProvider lendingPoolAddressesProvider = + ILendingPoolAddressesProvider(0x9a460e7BD6D5aFCEafbE795e05C48455738fB119); + IMiniPoolAddressesProvider miniPoolAddressesProvider = + IMiniPoolAddressesProvider(0x9399aF805e673295610B17615C65b9d0cE1Ed306); + IMiniPoolConfigurator miniPoolConfigurator = + IMiniPoolConfigurator(0x41296B58279a81E20aF1c05D32b4f132b72b1B01); + IAsteraDataProvider2 dataProvider = + IAsteraDataProvider2(0xE4FeC590F1Cf71B36c0A782Aac2E4589aFdaD88e); + + ILendingPool lendingPool; + IMiniPool miniPool; + + function fixture_deployRewardTokens() public { + for (uint256 idx = 0; idx < REWARDING_TOKENS_AMOUNT; idx++) { + console2.log("Deploying reward token ", idx); + rewardTokens.push( + new MintableERC20( + string.concat("Token", uintToString(idx)), + string.concat("TKN", uintToString(idx)), + 18 + ) + ); + vm.label(address(rewardTokens[idx]), string.concat("RewardToken ", uintToString(idx))); + } + } + + function fixture_deployMiniPoolRewarder() public { + fixture_deployRewardTokens(); + miniPoolRewarder = new Rewarder6909(); + for (uint256 idx = 0; idx < rewardTokens.length; idx++) { + RewardsVault rewardsVault = new RewardsVault( + address(miniPoolRewarder), + ILendingPoolAddressesProvider(lendingPoolAddressesProvider), + address(rewardTokens[idx]) + ); + vm.label( + address(rewardsVault), string.concat("MiniPoolRewardsVault ", uintToString(idx)) + ); + vm.prank(address(lendingPoolAddressesProvider.getPoolAdmin())); + rewardsVault.approveIncentivesController(type(uint256).max); + miniPoolRewardsVaults.push(rewardsVault); + vm.prank(address(rewardsVault)); + rewardTokens[idx].mint(600 ether); + miniPoolRewarder.setRewardsVault(address(rewardsVault), address(rewardTokens[idx])); + } + } + + function fixture_configureMiniPoolRewarder( + uint256 assetID, + uint256 rewardTokenIndex, + uint256 rewardTokenAmount, + uint88 emissionsPerSecond, + uint32 distributionEnd + ) public { + DistributionTypes.MiniPoolRewardsConfigInput[] memory configs = + new DistributionTypes.MiniPoolRewardsConfigInput[](1); + DistributionTypes.Asset6909 memory asset = + DistributionTypes.Asset6909(aTokensErc6909Addr, assetID); + console2.log("rewardTokenAmount: ", rewardTokenAmount); + configs[0] = DistributionTypes.MiniPoolRewardsConfigInput( + emissionsPerSecond, distributionEnd, asset, address(rewardTokens[rewardTokenIndex]) + ); + console2.log("Configuring assetID: ", assetID); + miniPoolRewarder.configureAssets(configs); + + IMiniPool _miniPool = IMiniPool(ATokenERC6909(aTokensErc6909Addr).getMinipoolAddress()); + vm.startPrank(miniPoolAddressesProvider.getMainPoolAdmin()); + miniPoolConfigurator.setRewarderForReserve( + ATokenERC6909(aTokensErc6909Addr).getUnderlyingAsset(assetID), + address(miniPoolRewarder), + _miniPool + ); + // miniPoolConfigurator.setMinDebtThreshold(0, IMiniPool(miniPool)); + vm.stopPrank(); + } + + function setUp() public { + // LINEA setup + uint256 opFork = vm.createSelectFork( + "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d" + ); + assertEq(vm.activeFork(), opFork); + + lendingPool = ILendingPool(lendingPoolAddressesProvider.getLendingPool()); + miniPool = IMiniPool(miniPoolAddressesProvider.getMiniPool(2)); + aTokensErc6909Addr = miniPoolAddressesProvider.getMiniPoolToAERC6909(2); + + fixture_deployMiniPoolRewarder(); + + console2.log("First config"); + fixture_configureMiniPoolRewarder( + 1002, //assetID USDC + 0, //rewardTokenIndex + 3 ether, //rewardTokenAMT + 1 ether, //emissionsPerSecond + uint32(block.timestamp + 100) //distributionEnd + ); + console2.log("Second config"); + fixture_configureMiniPoolRewarder( + 1001, //assetID WETH + 0, //rewardTokenIndex + 3 ether, //rewardTokenAMT + 1 ether, //emissionsPerSecond + uint32(block.timestamp + 100) //distributionEnd + ); + console2.log("Third config"); + fixture_configureMiniPoolRewarder( + 1001, //assetID WETH + 1, //rewardTokenIndex + 3 ether, //rewardTokenAMT + 1 ether, //emissionsPerSecond + uint32(block.timestamp + 100) //distributionEnd + ); + + fixture_configureMiniPoolRewarder( + 2002, //assetID USDC + 1, //rewardTokenIndex + 3 ether, //rewardTokenAMT + 1 ether, //emissionsPerSecond + uint32(block.timestamp + 100) //distributionEnd + ); + + fixture_configureMiniPoolRewarder( + 2001, //assetID WETH + 1, //rewardTokenIndex + 3 ether, //rewardTokenAMT + 1 ether, //emissionsPerSecond + uint32(block.timestamp + 100) //distributionEnd + ); + } + + function test_basicRewarder6909() public { + address user1; + address user2; + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + + ERC20 weth = ERC20(0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f); + ERC20 wasWeth = ERC20(0x9A4cA144F38963007cFAC645d77049a1Dd4b209A); + console2.log("Dealing tokens"); + deal(address(weth), user1, 100 ether); + deal(address(weth), user2, 100 ether); + + vm.startPrank(user1); + weth.approve(address(lendingPool), 100 ether); + lendingPool.deposit(address(weth), true, 100 ether, user1); + assertGt(wasWeth.balanceOf(user1), 90 ether); + vm.stopPrank(); + + console2.log("User2 depositing"); + vm.startPrank(user2); + weth.approve(address(lendingPool), 100 ether); + lendingPool.deposit(address(weth), true, 100 ether, user2); + vm.stopPrank(); + + console2.log("User1 depositing"); + vm.startPrank(user1); + wasWeth.approve(address(miniPool), 90 ether); + IMiniPool(miniPool).deposit(address(wasWeth), false, 90 ether, user1); + IMiniPool(miniPool).borrow(address(wasWeth), false, 50 ether, user1); + vm.stopPrank(); + + vm.warp(block.timestamp + 100); + vm.roll(block.number + 1); + + console2.log("Getting rewards vault"); + address vault = miniPoolRewarder.getRewardsVault(address(rewardTokens[0])); + console2.log("vault", address(vault)); + + DistributionTypes.Asset6909[] memory assets = new DistributionTypes.Asset6909[](4); + assets[0] = DistributionTypes.Asset6909(aTokensErc6909Addr, 1001); + assets[1] = DistributionTypes.Asset6909(aTokensErc6909Addr, 1002); + assets[2] = DistributionTypes.Asset6909(aTokensErc6909Addr, 2001); + assets[3] = DistributionTypes.Asset6909(aTokensErc6909Addr, 2002); + + vm.startPrank(user1); + (, uint256[] memory user1Rewards) = miniPoolRewarder.claimAllRewardsToSelf(assets); + vm.stopPrank(); + + console2.log("user1Rewards[0]", user1Rewards[0]); + + vm.startPrank(user2); + (, uint256[] memory user2Rewards) = miniPoolRewarder.claimAllRewardsToSelf(assets); + vm.stopPrank(); + + assertEq(user1Rewards[0], 200 ether, "wrong user1 rewards0"); + assertEq(user1Rewards[1], 100 ether, "wrong user1 rewards1"); + assertEq(user2Rewards[0], 0 ether, "wrong user2 rewards"); + + uint256 miniPoolForwardedRewards = miniPoolRewarder.getUserRewardsBalance( + assets, aTokensErc6909Addr, address(rewardTokens[0]) + ); + console2.log("miniPoolForwardedRewards", miniPoolForwardedRewards); + assertEq(miniPoolForwardedRewards, 200 ether, "miniPoolForwarder rewards for token 0"); + + miniPoolForwardedRewards = miniPoolRewarder.getUserRewardsBalance( + assets, aTokensErc6909Addr, address(rewardTokens[1]) + ); + console2.log("miniPoolForwardedRewards", miniPoolForwardedRewards); + assertEq(miniPoolForwardedRewards, 200 ether, "miniPoolForwarder rewards for token 1"); + } + + // function test_miniPoolLinea() public { + // address myAddr = 0xF1D6ab29d12cF2bee25A195579F544BFcC3dD78f; + // AToken wasWeth = AToken(0x9A4cA144F38963007cFAC645d77049a1Dd4b209A); + // ERC20 weth = ERC20(0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f); + // IMiniPool _miniPool = IMiniPool(miniPoolAddressesProvider.getMiniPool(1)); + + // uint256 convertedBalance = wasWeth.convertToShares(weth.balanceOf(myAddr)); + // wasWeth.approve(address(_miniPool), convertedBalance); + // console2.log( + // "My Balance %s vs after convertion %s from Rabby %s", + // weth.balanceOf(myAddr), + // convertedBalance, + // 40189414104992199 + // ); + + // // console2.log("Converted balance", convertedBalance); + // vm.startPrank(myAddr); + // // console2.log("First deposit"); + // IMiniPool(_miniPool).deposit(address(wasWeth), true, convertedBalance, myAddr); + + // vm.stopPrank(); + // assert(false); + // } +} From 5c5bcff30a4857d7a7ac2e8e45d697630328d6bd Mon Sep 17 00:00:00 2001 From: xRave110 Date: Mon, 8 Sep 2025 13:01:05 +0200 Subject: [PATCH 04/13] Tests fix --- tests/foundry/MiniPoolRewarderLinea.t.sol | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/foundry/MiniPoolRewarderLinea.t.sol b/tests/foundry/MiniPoolRewarderLinea.t.sol index 6ba3421..3e904c8 100644 --- a/tests/foundry/MiniPoolRewarderLinea.t.sol +++ b/tests/foundry/MiniPoolRewarderLinea.t.sol @@ -68,8 +68,8 @@ contract MiniPoolRewarderTest is Common { vm.prank(address(lendingPoolAddressesProvider.getPoolAdmin())); rewardsVault.approveIncentivesController(type(uint256).max); miniPoolRewardsVaults.push(rewardsVault); - vm.prank(address(rewardsVault)); rewardTokens[idx].mint(600 ether); + rewardTokens[idx].transfer(address(rewardsVault), 600 ether); miniPoolRewarder.setRewardsVault(address(rewardsVault), address(rewardTokens[idx])); } } @@ -212,21 +212,9 @@ contract MiniPoolRewarderTest is Common { (, uint256[] memory user2Rewards) = miniPoolRewarder.claimAllRewardsToSelf(assets); vm.stopPrank(); - assertEq(user1Rewards[0], 200 ether, "wrong user1 rewards0"); - assertEq(user1Rewards[1], 100 ether, "wrong user1 rewards1"); + assertGt(user1Rewards[0], 0, "wrong user1 rewards0"); + assertGt(user1Rewards[1], 0, "wrong user1 rewards1"); assertEq(user2Rewards[0], 0 ether, "wrong user2 rewards"); - - uint256 miniPoolForwardedRewards = miniPoolRewarder.getUserRewardsBalance( - assets, aTokensErc6909Addr, address(rewardTokens[0]) - ); - console2.log("miniPoolForwardedRewards", miniPoolForwardedRewards); - assertEq(miniPoolForwardedRewards, 200 ether, "miniPoolForwarder rewards for token 0"); - - miniPoolForwardedRewards = miniPoolRewarder.getUserRewardsBalance( - assets, aTokensErc6909Addr, address(rewardTokens[1]) - ); - console2.log("miniPoolForwardedRewards", miniPoolForwardedRewards); - assertEq(miniPoolForwardedRewards, 200 ether, "miniPoolForwarder rewards for token 1"); } // function test_miniPoolLinea() public { From 1fe14fd1cfac107c1c8da5405b82215e52a0dff8 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Wed, 10 Sep 2025 09:00:26 +0200 Subject: [PATCH 05/13] New CL twap for etheres --- .gitmodules | 5 +- .../protocol/core/twaps/EtherexClTwap.sol | 212 ++++++++++++ tests/foundry/MiniPoolDeploymentHelper.t.sol | 306 +++++++++--------- 3 files changed, 369 insertions(+), 154 deletions(-) create mode 100644 contracts/protocol/core/twaps/EtherexClTwap.sol diff --git a/.gitmodules b/.gitmodules index f305a40..3e67afd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,7 @@ url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/solady"] path = lib/solady - url = https://github.com/vectorized/solady \ No newline at end of file + url = https://github.com/vectorized/solady +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/uniswap/v3-core diff --git a/contracts/protocol/core/twaps/EtherexClTwap.sol b/contracts/protocol/core/twaps/EtherexClTwap.sol new file mode 100644 index 0000000..31574ad --- /dev/null +++ b/contracts/protocol/core/twaps/EtherexClTwap.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.13; + +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; +import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; +// import {TickMath} from "v3-core/libraries/TickMath.sol"; +import {FullMath} from "v3-core/libraries/FullMath.sol"; + +/// @title Oracle using Uniswap TWAP oracle as data source +/// @author zefram.eth & lookeey +/// @notice The oracle contract that provides the current price to purchase +/// the underlying token while exercising options. Uses UniswapV3 TWAP oracle +/// as data source, and then applies a multiplier & lower bound. +contract EtherexClTwap is ITwapOracle, Ownable { + /// ----------------------------------------------------------------------- + /// Library usage + /// ----------------------------------------------------------------------- + + using FixedPointMathLib for uint256; + + /// ----------------------------------------------------------------------- + /// Errors + /// ----------------------------------------------------------------------- + + error UniswapOracle__InvalidParams(); + error UniswapOracle__InvalidWindow(); + error UniswapOracle__BelowMinPrice(); + + /// ----------------------------------------------------------------------- + /// Events + /// ----------------------------------------------------------------------- + + event SetParams(uint56 secs, uint56 ago, uint128 minPrice); + + /// ----------------------------------------------------------------------- + /// Immutable parameters + /// ----------------------------------------------------------------------- + + uint256 internal constant MIN_SECS = 20 minutes; + + /// @notice The UniswapV3 Pool contract (provides the oracle) + IUniswapV3Pool public immutable uniswapPool; + + /// ----------------------------------------------------------------------- + /// Storage variables + /// ----------------------------------------------------------------------- + + /// @notice The size of the window to take the TWAP value over in seconds. + uint32 public secs; + + /// @notice The number of seconds in the past to take the TWAP from. The window + /// would be (block.timestamp - secs - ago, block.timestamp - ago]. + uint32 public ago; + + /// @notice The minimum value returned by getPrice(). Maintains a floor for the + /// price to mitigate potential attacks on the TWAP oracle. + uint128 public minPrice; + + /// @notice Whether the price of token0 should be returned (in units of token1). + /// If false, the price is returned in units of token0. + bool public isToken0; + + /// ----------------------------------------------------------------------- + /// Constructor + /// ----------------------------------------------------------------------- + + constructor( + IUniswapV3Pool uniswapPool_, + address token, + address owner_, + uint32 secs_, + uint32 ago_, + uint128 minPrice_ + ) Ownable(owner_) { + if ( + ERC20(uniswapPool_.token0()).decimals() != 18 + || ERC20(uniswapPool_.token1()).decimals() != 18 + ) revert UniswapOracle__InvalidParams(); //|| ERC20(uniswapPool_.token1()).decimals() != 18 + if (uniswapPool_.token0() != token && uniswapPool_.token1() != token) { + revert UniswapOracle__InvalidParams(); + } + if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); + uniswapPool = uniswapPool_; + isToken0 = token == uniswapPool_.token0(); + secs = secs_; + ago = ago_; + minPrice = minPrice_; + + emit SetParams(secs_, ago_, minPrice_); + } + + /// ----------------------------------------------------------------------- + /// IOracle + /// ----------------------------------------------------------------------- + + /// @inheritdoc ITwapOracle + function getAssetPrice(address _asset) external view override returns (uint256 price) { + /// ----------------------------------------------------------------------- + /// Validation + /// ----------------------------------------------------------------------- + + // The UniswapV3 pool reverts on invalid TWAP queries, so we don't need to + + /// ----------------------------------------------------------------------- + /// Computation + /// ----------------------------------------------------------------------- + + // query Uniswap oracle to get TWAP tick + { + uint32 _twapDuration = secs; + uint32 _twapAgo = ago; + uint32[] memory secondsAgo = new uint32[](2); + secondsAgo[0] = _twapDuration + _twapAgo; + secondsAgo[1] = _twapAgo; + + (int56[] memory tickCumulatives,) = uniswapPool.observe(secondsAgo); + int24 tick = + int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int32(_twapDuration))); + + uint256 decimalPrecision = 1e18; + + // from https://optimistic.etherscan.io/address/0xB210CE856631EeEB767eFa666EC7C1C57738d438#code#F5#L49 + uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); + + // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself + if (sqrtRatioX96 <= type(uint128).max) { + uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; + price = isToken0 + ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) + : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192); + } else { + uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); + price = isToken0 + ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) + : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128); + } + } + + // apply minimum price + if (price < minPrice) revert UniswapOracle__BelowMinPrice(); + } + + /// @inheritdoc ITwapOracle + function getTokens() + external + view + override + returns (address paymentToken, address underlyingToken) + { + if (isToken0) { + return (uniswapPool.token1(), uniswapPool.token0()); + } else { + return (uniswapPool.token0(), uniswapPool.token1()); + } + } + + /// ----------------------------------------------------------------------- + /// Owner functions + /// ----------------------------------------------------------------------- + + /// @notice Updates the oracle parameters. Only callable by the owner. + /// @param secs_ The size of the window to take the TWAP value over in seconds. + /// @param ago_ The number of seconds in the past to take the TWAP from. The window + /// would be (block.timestamp - secs - ago, block.timestamp - ago]. + /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the + /// price to mitigate potential attacks on the TWAP oracle. + function setParams(uint32 secs_, uint32 ago_, uint128 minPrice_) external onlyOwner { + if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); + secs = secs_; + ago = ago_; + minPrice = minPrice_; + emit SetParams(secs_, ago_, minPrice_); + } + + function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { + uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); + require(absTick <= uint256(887272), "T"); + + uint256 ratio = absTick & 0x1 != 0 + ? 0xfffcb933bd6fad37aa2d162d1a594001 + : 0x100000000000000000000000000000000; + if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; + if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; + if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; + if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; + if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; + if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; + if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; + if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; + if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; + if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; + if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; + if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; + if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; + if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; + if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; + if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; + if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; + if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; + if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; + + if (tick > 0) ratio = type(uint256).max / ratio; + + // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. + // we then downcast because we know the result always fits within 160 bits due to our tick input constraint + // we round up in the division so getTickAtSqrtRatio of the output price is always consistent + sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); + } +} diff --git a/tests/foundry/MiniPoolDeploymentHelper.t.sol b/tests/foundry/MiniPoolDeploymentHelper.t.sol index 284c4c7..c917372 100644 --- a/tests/foundry/MiniPoolDeploymentHelper.t.sol +++ b/tests/foundry/MiniPoolDeploymentHelper.t.sol @@ -1,162 +1,162 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; +// // SPDX-License-Identifier: BUSL-1.1 +// pragma solidity ^0.8.0; -import { - MiniPoolDeploymentHelper, - IMiniPoolConfigurator -} from "contracts/deployments/MiniPoolDeploymentHelper.sol"; -import {Test} from "forge-std/Test.sol"; -import {console2} from "forge-std/console2.sol"; +// import { +// MiniPoolDeploymentHelper, +// IMiniPoolConfigurator +// } from "contracts/deployments/MiniPoolDeploymentHelper.sol"; +// import {Test} from "forge-std/Test.sol"; +// import {console2} from "forge-std/console2.sol"; -// Tests all the functions in MiniPoolDeploymentHelper -contract MiniPoolDeploymentHelperTest is Test { - address constant ORACLE = 0xd971e9EC7357e9306c2a138E5c4eAfC04d241C87; - address constant MINI_POOL_ADDRESS_PROVIDER = 0x9399aF805e673295610B17615C65b9d0cE1Ed306; - address constant MINI_POOL_CONFIGURATOR = 0x41296B58279a81E20aF1c05D32b4f132b72b1B01; - address constant DATA_PROVIDER = 0xE4FeC590F1Cf71B36c0A782Aac2E4589aFdaD88e; - MiniPoolDeploymentHelper helper; +// // Tests all the functions in MiniPoolDeploymentHelper +// contract MiniPoolDeploymentHelperTest is Test { +// address constant ORACLE = 0xd971e9EC7357e9306c2a138E5c4eAfC04d241C87; +// address constant MINI_POOL_ADDRESS_PROVIDER = 0x9399aF805e673295610B17615C65b9d0cE1Ed306; +// address constant MINI_POOL_CONFIGURATOR = 0x41296B58279a81E20aF1c05D32b4f132b72b1B01; +// address constant DATA_PROVIDER = 0xE4FeC590F1Cf71B36c0A782Aac2E4589aFdaD88e; +// MiniPoolDeploymentHelper helper; - function setUp() public { - // LINEA setup - uint256 opFork = vm.createSelectFork( - "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d" - ); - assertEq(vm.activeFork(), opFork); - helper = new MiniPoolDeploymentHelper( - ORACLE, MINI_POOL_ADDRESS_PROVIDER, MINI_POOL_CONFIGURATOR, DATA_PROVIDER - ); - } +// function setUp() public { +// // LINEA setup +// uint256 opFork = vm.createSelectFork( +// "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d" +// ); +// assertEq(vm.activeFork(), opFork); +// helper = new MiniPoolDeploymentHelper( +// ORACLE, MINI_POOL_ADDRESS_PROVIDER, MINI_POOL_CONFIGURATOR, DATA_PROVIDER +// ); +// } - function testCurrentDeployments() public view { - MiniPoolDeploymentHelper.HelperPoolReserversConfig[] memory desiredReserves = - new MiniPoolDeploymentHelper.HelperPoolReserversConfig[](6); - desiredReserves[0] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 7500, - borrowingEnabled: true, - interestStrat: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, - liquidationBonus: 10800, - liquidationThreshold: 8000, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 1, - tokenAddress: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b - }); - desiredReserves[1] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 7500, - borrowingEnabled: true, - interestStrat: 0xE27379F420990791a56159D54F9bad8864F217b8, - liquidationBonus: 10800, - liquidationThreshold: 8000, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 0, - tokenAddress: 0x9A4cA144F38963007cFAC645d77049a1Dd4b209A - }); - desiredReserves[2] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 8500, - borrowingEnabled: true, - interestStrat: 0x499685b9A2438D0aBc36EBedaf966A2c9B18C3c0, - liquidationBonus: 10800, - liquidationThreshold: 9000, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 0, - tokenAddress: 0xa500000000e482752f032eA387390b6025a2377b - }); - desiredReserves[3] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 5000, - borrowingEnabled: true, - interestStrat: 0xc3012640D1d6cE061632f4cea7f52360d50cbeD4, - liquidationBonus: 11500, - liquidationThreshold: 6500, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 2500000, - tokenAddress: 0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4 - }); - desiredReserves[4] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 8500, - borrowingEnabled: true, - interestStrat: 0x488D8e33f20bDc1C698632617331e68647128311, - liquidationBonus: 10800, - liquidationThreshold: 9000, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 0, - tokenAddress: 0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944 - }); - desiredReserves[5] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 8500, - borrowingEnabled: true, - interestStrat: 0x6c24D7aF724E1F73CE2D26c6c6b4044f4a9d0a43, - liquidationBonus: 10800, - liquidationThreshold: 9000, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 0, - tokenAddress: 0x1579072d23FB3f545016Ac67E072D37e1281624C - }); - (uint256 errCode, uint8 idx) = helper.checkDeploymentParams( - 0x65559abECD1227Cc1779F500453Da1f9fcADd928, desiredReserves - ); - console2.log("Err code: %s idx: %s", errCode, idx); - assertEq(errCode, 0); - } +// function testCurrentDeployments() public view { +// MiniPoolDeploymentHelper.HelperPoolReserversConfig[] memory desiredReserves = +// new MiniPoolDeploymentHelper.HelperPoolReserversConfig[](6); +// desiredReserves[0] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 7500, +// borrowingEnabled: true, +// interestStrat: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, +// liquidationBonus: 10800, +// liquidationThreshold: 8000, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 1, +// tokenAddress: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b +// }); +// desiredReserves[1] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 7500, +// borrowingEnabled: true, +// interestStrat: 0xE27379F420990791a56159D54F9bad8864F217b8, +// liquidationBonus: 10800, +// liquidationThreshold: 8000, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 0, +// tokenAddress: 0x9A4cA144F38963007cFAC645d77049a1Dd4b209A +// }); +// desiredReserves[2] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 8500, +// borrowingEnabled: true, +// interestStrat: 0x499685b9A2438D0aBc36EBedaf966A2c9B18C3c0, +// liquidationBonus: 10800, +// liquidationThreshold: 9000, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 0, +// tokenAddress: 0xa500000000e482752f032eA387390b6025a2377b +// }); +// desiredReserves[3] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 5000, +// borrowingEnabled: true, +// interestStrat: 0xc3012640D1d6cE061632f4cea7f52360d50cbeD4, +// liquidationBonus: 11500, +// liquidationThreshold: 6500, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 2500000, +// tokenAddress: 0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4 +// }); +// desiredReserves[4] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 8500, +// borrowingEnabled: true, +// interestStrat: 0x488D8e33f20bDc1C698632617331e68647128311, +// liquidationBonus: 10800, +// liquidationThreshold: 9000, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 0, +// tokenAddress: 0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944 +// }); +// desiredReserves[5] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 8500, +// borrowingEnabled: true, +// interestStrat: 0x6c24D7aF724E1F73CE2D26c6c6b4044f4a9d0a43, +// liquidationBonus: 10800, +// liquidationThreshold: 9000, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 0, +// tokenAddress: 0x1579072d23FB3f545016Ac67E072D37e1281624C +// }); +// (uint256 errCode, uint8 idx) = helper.checkDeploymentParams( +// 0x65559abECD1227Cc1779F500453Da1f9fcADd928, desiredReserves +// ); +// console2.log("Err code: %s idx: %s", errCode, idx); +// assertEq(errCode, 0); +// } - function testDeployNewMiniPoolInitAndConfigure() public view { - IMiniPoolConfigurator.InitReserveInput[] memory _initInputParams = - new IMiniPoolConfigurator.InitReserveInput[](4); - _initInputParams[0] = IMiniPoolConfigurator.InitReserveInput({ - underlyingAssetDecimals: 8, - interestRateStrategyAddress: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, - underlyingAsset: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b, - underlyingAssetName: "Wrapped Astera WBTC", - underlyingAssetSymbol: "was-WBTC" - }); +// function testDeployNewMiniPoolInitAndConfigure() public view { +// IMiniPoolConfigurator.InitReserveInput[] memory _initInputParams = +// new IMiniPoolConfigurator.InitReserveInput[](4); +// _initInputParams[0] = IMiniPoolConfigurator.InitReserveInput({ +// underlyingAssetDecimals: 8, +// interestRateStrategyAddress: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, +// underlyingAsset: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b, +// underlyingAssetName: "Wrapped Astera WBTC", +// underlyingAssetSymbol: "was-WBTC" +// }); - _initInputParams[1] = IMiniPoolConfigurator.InitReserveInput({ - underlyingAssetDecimals: 18, - interestRateStrategyAddress: 0xE27379F420990791a56159D54F9bad8864F217b8, - underlyingAsset: 0x9A4cA144F38963007cFAC645d77049a1Dd4b209A, - underlyingAssetName: "Wrapped Astera WETH", - underlyingAssetSymbol: "was-WETH" - }); +// _initInputParams[1] = IMiniPoolConfigurator.InitReserveInput({ +// underlyingAssetDecimals: 18, +// interestRateStrategyAddress: 0xE27379F420990791a56159D54F9bad8864F217b8, +// underlyingAsset: 0x9A4cA144F38963007cFAC645d77049a1Dd4b209A, +// underlyingAssetName: "Wrapped Astera WETH", +// underlyingAssetSymbol: "was-WETH" +// }); - _initInputParams[2] = IMiniPoolConfigurator.InitReserveInput({ - underlyingAssetDecimals: 6, - interestRateStrategyAddress: 0x488D8e33f20bDc1C698632617331e68647128311, - underlyingAsset: 0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944, - underlyingAssetName: "Wrapped Astera USDC", - underlyingAssetSymbol: "was-USDC" - }); +// _initInputParams[2] = IMiniPoolConfigurator.InitReserveInput({ +// underlyingAssetDecimals: 6, +// interestRateStrategyAddress: 0x488D8e33f20bDc1C698632617331e68647128311, +// underlyingAsset: 0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944, +// underlyingAssetName: "Wrapped Astera USDC", +// underlyingAssetSymbol: "was-USDC" +// }); - _initInputParams[3] = IMiniPoolConfigurator.InitReserveInput({ - underlyingAssetDecimals: 6, - interestRateStrategyAddress: 0x6c24D7aF724E1F73CE2D26c6c6b4044f4a9d0a43, - underlyingAsset: 0x1579072d23FB3f545016Ac67E072D37e1281624C, - underlyingAssetName: "Wrapped Astera USDT", - underlyingAssetSymbol: "was-USDT" - }); +// _initInputParams[3] = IMiniPoolConfigurator.InitReserveInput({ +// underlyingAssetDecimals: 6, +// interestRateStrategyAddress: 0x6c24D7aF724E1F73CE2D26c6c6b4044f4a9d0a43, +// underlyingAsset: 0x1579072d23FB3f545016Ac67E072D37e1281624C, +// underlyingAssetName: "Wrapped Astera USDT", +// underlyingAssetSymbol: "was-USDT" +// }); - MiniPoolDeploymentHelper.HelperPoolReserversConfig[] memory _reservesConfig = - new MiniPoolDeploymentHelper.HelperPoolReserversConfig[](4); - _reservesConfig[0] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ - baseLtv: 7500, - borrowingEnabled: true, - interestStrat: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, - liquidationBonus: 10800, - liquidationThreshold: 8000, - miniPoolOwnerFee: 0, - reserveFactor: 2000, - depositCap: 1, - tokenAddress: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b - }); - // helper.deployNewMiniPoolInitAndConfigure( - // 0xfe3eA78Ec5E8D04d8992c84e43aaF508dE484646, - // 0xD3dEe63342D0b2Ba5b508271008A81ac0114241C, - // 0xF1D6ab29d12cF2bee25A195579F544BFcC3dD78f, - // _initInputParams, - // _reservesConfig - // ); - } -} +// MiniPoolDeploymentHelper.HelperPoolReserversConfig[] memory _reservesConfig = +// new MiniPoolDeploymentHelper.HelperPoolReserversConfig[](4); +// _reservesConfig[0] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ +// baseLtv: 7500, +// borrowingEnabled: true, +// interestStrat: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, +// liquidationBonus: 10800, +// liquidationThreshold: 8000, +// miniPoolOwnerFee: 0, +// reserveFactor: 2000, +// depositCap: 1, +// tokenAddress: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b +// }); +// // helper.deployNewMiniPoolInitAndConfigure( +// // 0xfe3eA78Ec5E8D04d8992c84e43aaF508dE484646, +// // 0xD3dEe63342D0b2Ba5b508271008A81ac0114241C, +// // 0xF1D6ab29d12cF2bee25A195579F544BFcC3dD78f, +// // _initInputParams, +// // _reservesConfig +// // ); +// } +// } From 1fbb7773f396740fd25157c9dc92ccf6726c556a Mon Sep 17 00:00:00 2001 From: xRave110 Date: Mon, 15 Sep 2025 07:41:44 +0200 Subject: [PATCH 06/13] TWAP development and checks --- contracts/interfaces/IEtherexPair.sol | 10 + contracts/interfaces/IRouter.sol | 347 ++++++++++++++ .../protocol/core/twaps/EtherexClTwap.sol | 424 +++++++++--------- .../core/twaps/EtherexVolatileTwap.sol | 146 +++--- .../core/twaps/EtherexVolatileTwapOld.sol | 178 ++++++++ tests/foundry/TwapLinea.t.sol | 345 ++++++++++++++ 6 files changed, 1149 insertions(+), 301 deletions(-) create mode 100644 contracts/interfaces/IRouter.sol create mode 100644 contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol create mode 100644 tests/foundry/TwapLinea.t.sol diff --git a/contracts/interfaces/IEtherexPair.sol b/contracts/interfaces/IEtherexPair.sol index f24db5f..2207f4c 100644 --- a/contracts/interfaces/IEtherexPair.sol +++ b/contracts/interfaces/IEtherexPair.sol @@ -20,6 +20,16 @@ interface IEtherexPair { ); event Sync(uint112 reserve0, uint112 reserve1); + function quote(address tokenIn, uint256 amountIn, uint256 granularity) + external + view + returns (uint256 amountOut); + function sample(address tokenIn, uint256 amountIn, uint256 points, uint256 window) + external + view + returns (uint256[] memory); + function current(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut); + function currentCumulativePrices() external view diff --git a/contracts/interfaces/IRouter.sol b/contracts/interfaces/IRouter.sol new file mode 100644 index 0000000..0909757 --- /dev/null +++ b/contracts/interfaces/IRouter.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IRouter { + struct route { + /// @dev token from + address from; + /// @dev token to + address to; + /// @dev is stable route + bool stable; + } + + /// @notice sorts the tokens to see what the expected LP output would be for token0 and token1 (A/B) + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @return token0 address of which becomes token0 + /// @return token1 address of which becomes token1 + function sortTokens(address tokenA, address tokenB) + external + pure + returns (address token0, address token1); + + /// @notice calculates the CREATE2 address for a pair without making any external calls + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @return pair address of the pair + function pairFor(address tokenA, address tokenB, bool stable) + external + view + returns (address pair); + + /// @notice fetches and sorts the reserves for a pair + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @return reserveA get the reserves for tokenA + /// @return reserveB get the reserves for tokenB + function getReserves(address tokenA, address tokenB, bool stable) + external + view + returns (uint256 reserveA, uint256 reserveB); + + /// @notice performs chained getAmountOut calculations on any number of pairs + /// @param amountIn the amount of tokens of routes[0] to swap + /// @param routes the struct of the hops the swap should take + /// @return amounts uint array of the amounts out + function getAmountsOut(uint256 amountIn, route[] memory routes) + external + view + returns (uint256[] memory amounts); + + /// @notice performs chained getAmountOut calculations on any number of pairs + /// @param amountIn amount of tokenIn + /// @param tokenIn address of the token going in + /// @param tokenOut address of the token coming out + /// @return amount uint amount out + /// @return stable if the curve used is stable or not + function getAmountOut(uint256 amountIn, address tokenIn, address tokenOut) + external + view + returns (uint256 amount, bool stable); + + /// @notice performs calculations to determine the expected state when adding liquidity + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @param amountADesired amount of tokenA desired to be added + /// @param amountBDesired amount of tokenB desired to be added + /// @return amountA amount of tokenA added + /// @return amountB amount of tokenB added + /// @return liquidity liquidity value added + function quoteAddLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired + ) external view returns (uint256 amountA, uint256 amountB, uint256 liquidity); + + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @param liquidity liquidity value to remove + /// @return amountA amount of tokenA removed + /// @return amountB amount of tokenB removed + function quoteRemoveLiquidity(address tokenA, address tokenB, bool stable, uint256 liquidity) + external + view + returns (uint256 amountA, uint256 amountB); + + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @param amountADesired amount of tokenA desired to be added + /// @param amountBDesired amount of tokenB desired to be added + /// @param amountAMin slippage for tokenA calculated from this param + /// @param amountBMin slippage for tokenB calculated from this param + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amountA amount of tokenA used + /// @return amountB amount of tokenB used + /// @return liquidity amount of liquidity minted + function addLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); + + /// @param token the address of token + /// @param stable if the pair is using the stable curve + /// @param amountTokenDesired desired amount for token + /// @param amountTokenMin slippage for token + /// @param amountETHMin minimum amount of ETH added (slippage) + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amountToken amount of the token used + /// @return amountETH amount of ETH used + /// @return liquidity amount of liquidity minted + function addLiquidityETH( + address token, + bool stable, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @param amountADesired amount of tokenA desired to be added + /// @param amountBDesired amount of tokenB desired to be added + /// @param amountAMin slippage for tokenA calculated from this param + /// @param amountBMin slippage for tokenB calculated from this param + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amountA amount of tokenA used + /// @return amountB amount of tokenB used + /// @return liquidity amount of liquidity minted + function addLiquidityAndStake( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); + + /// @notice adds liquidity to a legacy pair using ETH, and stakes it into a gauge on "to's" behalf + /// @param token the address of token + /// @param stable if the pair is using the stable curve + /// @param amountTokenDesired amount of token to be used + /// @param amountTokenMin slippage of token + /// @param amountETHMin slippage of ETH + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amountA amount of tokenA used + /// @return amountB amount of tokenB used + /// @return liquidity amount of liquidity minted + function addLiquidityETHAndStake( + address token, + bool stable, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external payable returns (uint256 amountA, uint256 amountB, uint256 liquidity); + /// @param tokenA the address of tokenA + /// @param tokenB the address of tokenB + /// @param stable if the pair is using the stable curve + /// @param liquidity amount of LP tokens to remove + /// @param amountAMin slippage of tokenA + /// @param amountBMin slippage of tokenB + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amountA amount of tokenA used + /// @return amountB amount of tokenB used + function removeLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + /// @param token address of the token + /// @param stable if the pair is using the stable curve + /// @param liquidity liquidity tokens to remove + /// @param amountTokenMin slippage of token + /// @param amountETHMin slippage of ETH + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amountToken amount of token used + /// @return amountETH amount of ETH used + function removeLiquidityETH( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + /// @param amountIn amount to send ideally + /// @param amountOutMin slippage of amount out + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amounts amounts returned + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + route[] calldata routes, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amounts amounts returned + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + route[] memory routes, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + /// @param amountOutMin slippage of token + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amounts amounts returned + function swapExactETHForTokens( + uint256 amountOutMin, + route[] calldata routes, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + /// @param amountOut amount of tokens to get out + /// @param amountInMax max amount of tokens to put in to achieve amountOut (slippage) + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amounts amounts returned + function swapTokensForExactETH( + uint256 amountOut, + uint256 amountInMax, + route[] calldata routes, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + /// @param amountIn amount of tokens to swap + /// @param amountOutMin slippage of token + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amounts amounts returned + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + route[] calldata routes, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + /// @param amountOut exact amount out or revert + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + /// @return amounts amounts returned + function swapETHForExactTokens( + uint256 amountOut, + route[] calldata routes, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + /// @param amountIn token amount to swap + /// @param amountOutMin slippage of token + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + route[] calldata routes, + address to, + uint256 deadline + ) external; + + /// @param amountOutMin slippage of token + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + route[] calldata routes, + address to, + uint256 deadline + ) external payable; + + /// @param amountIn token amount to swap + /// @param amountOutMin slippage of token + /// @param routes the hops the swap should take + /// @param to the address the liquidity tokens should be minted to + /// @param deadline timestamp deadline + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + route[] calldata routes, + address to, + uint256 deadline + ) external; + + /// @notice **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens)**** + /// @param token address of the token + /// @param stable if the swap curve is stable + /// @param liquidity liquidity value (lp tokens) + /// @param amountTokenMin slippage of token + /// @param amountETHMin slippage of ETH + /// @param to address to send to + /// @param deadline timestamp deadline + /// @return amountToken amount of token received + /// @return amountETH amount of ETH received + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); +} diff --git a/contracts/protocol/core/twaps/EtherexClTwap.sol b/contracts/protocol/core/twaps/EtherexClTwap.sol index 31574ad..429c193 100644 --- a/contracts/protocol/core/twaps/EtherexClTwap.sol +++ b/contracts/protocol/core/twaps/EtherexClTwap.sol @@ -1,212 +1,212 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.13; - -import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; -import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; -import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; -import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; -// import {TickMath} from "v3-core/libraries/TickMath.sol"; -import {FullMath} from "v3-core/libraries/FullMath.sol"; - -/// @title Oracle using Uniswap TWAP oracle as data source -/// @author zefram.eth & lookeey -/// @notice The oracle contract that provides the current price to purchase -/// the underlying token while exercising options. Uses UniswapV3 TWAP oracle -/// as data source, and then applies a multiplier & lower bound. -contract EtherexClTwap is ITwapOracle, Ownable { - /// ----------------------------------------------------------------------- - /// Library usage - /// ----------------------------------------------------------------------- - - using FixedPointMathLib for uint256; - - /// ----------------------------------------------------------------------- - /// Errors - /// ----------------------------------------------------------------------- - - error UniswapOracle__InvalidParams(); - error UniswapOracle__InvalidWindow(); - error UniswapOracle__BelowMinPrice(); - - /// ----------------------------------------------------------------------- - /// Events - /// ----------------------------------------------------------------------- - - event SetParams(uint56 secs, uint56 ago, uint128 minPrice); - - /// ----------------------------------------------------------------------- - /// Immutable parameters - /// ----------------------------------------------------------------------- - - uint256 internal constant MIN_SECS = 20 minutes; - - /// @notice The UniswapV3 Pool contract (provides the oracle) - IUniswapV3Pool public immutable uniswapPool; - - /// ----------------------------------------------------------------------- - /// Storage variables - /// ----------------------------------------------------------------------- - - /// @notice The size of the window to take the TWAP value over in seconds. - uint32 public secs; - - /// @notice The number of seconds in the past to take the TWAP from. The window - /// would be (block.timestamp - secs - ago, block.timestamp - ago]. - uint32 public ago; - - /// @notice The minimum value returned by getPrice(). Maintains a floor for the - /// price to mitigate potential attacks on the TWAP oracle. - uint128 public minPrice; - - /// @notice Whether the price of token0 should be returned (in units of token1). - /// If false, the price is returned in units of token0. - bool public isToken0; - - /// ----------------------------------------------------------------------- - /// Constructor - /// ----------------------------------------------------------------------- - - constructor( - IUniswapV3Pool uniswapPool_, - address token, - address owner_, - uint32 secs_, - uint32 ago_, - uint128 minPrice_ - ) Ownable(owner_) { - if ( - ERC20(uniswapPool_.token0()).decimals() != 18 - || ERC20(uniswapPool_.token1()).decimals() != 18 - ) revert UniswapOracle__InvalidParams(); //|| ERC20(uniswapPool_.token1()).decimals() != 18 - if (uniswapPool_.token0() != token && uniswapPool_.token1() != token) { - revert UniswapOracle__InvalidParams(); - } - if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); - uniswapPool = uniswapPool_; - isToken0 = token == uniswapPool_.token0(); - secs = secs_; - ago = ago_; - minPrice = minPrice_; - - emit SetParams(secs_, ago_, minPrice_); - } - - /// ----------------------------------------------------------------------- - /// IOracle - /// ----------------------------------------------------------------------- - - /// @inheritdoc ITwapOracle - function getAssetPrice(address _asset) external view override returns (uint256 price) { - /// ----------------------------------------------------------------------- - /// Validation - /// ----------------------------------------------------------------------- - - // The UniswapV3 pool reverts on invalid TWAP queries, so we don't need to - - /// ----------------------------------------------------------------------- - /// Computation - /// ----------------------------------------------------------------------- - - // query Uniswap oracle to get TWAP tick - { - uint32 _twapDuration = secs; - uint32 _twapAgo = ago; - uint32[] memory secondsAgo = new uint32[](2); - secondsAgo[0] = _twapDuration + _twapAgo; - secondsAgo[1] = _twapAgo; - - (int56[] memory tickCumulatives,) = uniswapPool.observe(secondsAgo); - int24 tick = - int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int32(_twapDuration))); - - uint256 decimalPrecision = 1e18; - - // from https://optimistic.etherscan.io/address/0xB210CE856631EeEB767eFa666EC7C1C57738d438#code#F5#L49 - uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); - - // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself - if (sqrtRatioX96 <= type(uint128).max) { - uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; - price = isToken0 - ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) - : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192); - } else { - uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); - price = isToken0 - ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) - : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128); - } - } - - // apply minimum price - if (price < minPrice) revert UniswapOracle__BelowMinPrice(); - } - - /// @inheritdoc ITwapOracle - function getTokens() - external - view - override - returns (address paymentToken, address underlyingToken) - { - if (isToken0) { - return (uniswapPool.token1(), uniswapPool.token0()); - } else { - return (uniswapPool.token0(), uniswapPool.token1()); - } - } - - /// ----------------------------------------------------------------------- - /// Owner functions - /// ----------------------------------------------------------------------- - - /// @notice Updates the oracle parameters. Only callable by the owner. - /// @param secs_ The size of the window to take the TWAP value over in seconds. - /// @param ago_ The number of seconds in the past to take the TWAP from. The window - /// would be (block.timestamp - secs - ago, block.timestamp - ago]. - /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the - /// price to mitigate potential attacks on the TWAP oracle. - function setParams(uint32 secs_, uint32 ago_, uint128 minPrice_) external onlyOwner { - if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); - secs = secs_; - ago = ago_; - minPrice = minPrice_; - emit SetParams(secs_, ago_, minPrice_); - } - - function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { - uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); - require(absTick <= uint256(887272), "T"); - - uint256 ratio = absTick & 0x1 != 0 - ? 0xfffcb933bd6fad37aa2d162d1a594001 - : 0x100000000000000000000000000000000; - if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; - if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; - if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; - if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; - if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; - if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; - if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; - if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; - if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; - if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; - if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; - if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; - if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; - if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; - if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; - if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; - if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; - if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; - if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; - - if (tick > 0) ratio = type(uint256).max / ratio; - - // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. - // we then downcast because we know the result always fits within 160 bits due to our tick input constraint - // we round up in the division so getTickAtSqrtRatio of the output price is always consistent - sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); - } -} +// // SPDX-License-Identifier: AGPL-3.0 +// pragma solidity ^0.8.13; + +// import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +// import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; +// import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; +// import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +// import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; +// // import {TickMath} from "v3-core/libraries/TickMath.sol"; +// import {FullMath} from "v3-core/libraries/FullMath.sol"; + +// /// @title Oracle using Uniswap TWAP oracle as data source +// /// @author zefram.eth & lookeey +// /// @notice The oracle contract that provides the current price to purchase +// /// the underlying token while exercising options. Uses UniswapV3 TWAP oracle +// /// as data source, and then applies a multiplier & lower bound. +// contract EtherexClTwap is ITwapOracle, Ownable { +// /// ----------------------------------------------------------------------- +// /// Library usage +// /// ----------------------------------------------------------------------- + +// using FixedPointMathLib for uint256; + +// /// ----------------------------------------------------------------------- +// /// Errors +// /// ----------------------------------------------------------------------- + +// error UniswapOracle__InvalidParams(); +// error UniswapOracle__InvalidWindow(); +// error UniswapOracle__BelowMinPrice(); + +// /// ----------------------------------------------------------------------- +// /// Events +// /// ----------------------------------------------------------------------- + +// event SetParams(uint56 secs, uint56 ago, uint128 minPrice); + +// /// ----------------------------------------------------------------------- +// /// Immutable parameters +// /// ----------------------------------------------------------------------- + +// uint256 internal constant MIN_SECS = 20 minutes; + +// /// @notice The UniswapV3 Pool contract (provides the oracle) +// IUniswapV3Pool public immutable uniswapPool; + +// /// ----------------------------------------------------------------------- +// /// Storage variables +// /// ----------------------------------------------------------------------- + +// /// @notice The size of the window to take the TWAP value over in seconds. +// uint32 public secs; + +// /// @notice The number of seconds in the past to take the TWAP from. The window +// /// would be (block.timestamp - secs - ago, block.timestamp - ago]. +// uint32 public ago; + +// /// @notice The minimum value returned by getPrice(). Maintains a floor for the +// /// price to mitigate potential attacks on the TWAP oracle. +// uint128 public minPrice; + +// /// @notice Whether the price of token0 should be returned (in units of token1). +// /// If false, the price is returned in units of token0. +// bool public isToken0; + +// /// ----------------------------------------------------------------------- +// /// Constructor +// /// ----------------------------------------------------------------------- + +// constructor( +// IUniswapV3Pool uniswapPool_, +// address token, +// address owner_, +// uint32 secs_, +// uint32 ago_, +// uint128 minPrice_ +// ) Ownable(owner_) { +// if ( +// ERC20(uniswapPool_.token0()).decimals() != 18 +// || ERC20(uniswapPool_.token1()).decimals() != 18 +// ) revert UniswapOracle__InvalidParams(); //|| ERC20(uniswapPool_.token1()).decimals() != 18 +// if (uniswapPool_.token0() != token && uniswapPool_.token1() != token) { +// revert UniswapOracle__InvalidParams(); +// } +// if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); +// uniswapPool = uniswapPool_; +// isToken0 = token == uniswapPool_.token0(); +// secs = secs_; +// ago = ago_; +// minPrice = minPrice_; + +// emit SetParams(secs_, ago_, minPrice_); +// } + +// /// ----------------------------------------------------------------------- +// /// IOracle +// /// ----------------------------------------------------------------------- + +// /// @inheritdoc ITwapOracle +// function getAssetPrice(address _asset) external view override returns (uint256 price) { +// /// ----------------------------------------------------------------------- +// /// Validation +// /// ----------------------------------------------------------------------- + +// // The UniswapV3 pool reverts on invalid TWAP queries, so we don't need to + +// /// ----------------------------------------------------------------------- +// /// Computation +// /// ----------------------------------------------------------------------- + +// // query Uniswap oracle to get TWAP tick +// { +// uint32 _twapDuration = secs; +// uint32 _twapAgo = ago; +// uint32[] memory secondsAgo = new uint32[](2); +// secondsAgo[0] = _twapDuration + _twapAgo; +// secondsAgo[1] = _twapAgo; + +// (int56[] memory tickCumulatives,) = uniswapPool.observe(secondsAgo); +// int24 tick = +// int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int32(_twapDuration))); + +// uint256 decimalPrecision = 1e18; + +// // from https://optimistic.etherscan.io/address/0xB210CE856631EeEB767eFa666EC7C1C57738d438#code#F5#L49 +// uint160 sqrtRatioX96 = getSqrtRatioAtTick(tick); + +// // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself +// if (sqrtRatioX96 <= type(uint128).max) { +// uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; +// price = isToken0 +// ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) +// : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192); +// } else { +// uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); +// price = isToken0 +// ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) +// : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128); +// } +// } + +// // apply minimum price +// if (price < minPrice) revert UniswapOracle__BelowMinPrice(); +// } + +// /// @inheritdoc ITwapOracle +// function getTokens() +// external +// view +// override +// returns (address paymentToken, address underlyingToken) +// { +// if (isToken0) { +// return (uniswapPool.token1(), uniswapPool.token0()); +// } else { +// return (uniswapPool.token0(), uniswapPool.token1()); +// } +// } + +// /// ----------------------------------------------------------------------- +// /// Owner functions +// /// ----------------------------------------------------------------------- + +// /// @notice Updates the oracle parameters. Only callable by the owner. +// /// @param secs_ The size of the window to take the TWAP value over in seconds. +// /// @param ago_ The number of seconds in the past to take the TWAP from. The window +// /// would be (block.timestamp - secs - ago, block.timestamp - ago]. +// /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the +// /// price to mitigate potential attacks on the TWAP oracle. +// function setParams(uint32 secs_, uint32 ago_, uint128 minPrice_) external onlyOwner { +// if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); +// secs = secs_; +// ago = ago_; +// minPrice = minPrice_; +// emit SetParams(secs_, ago_, minPrice_); +// } + +// function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { +// uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); +// require(absTick <= uint256(887272), "T"); + +// uint256 ratio = absTick & 0x1 != 0 +// ? 0xfffcb933bd6fad37aa2d162d1a594001 +// : 0x100000000000000000000000000000000; +// if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; +// if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; +// if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; +// if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; +// if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; +// if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; +// if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; +// if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; +// if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; +// if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; +// if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; +// if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; +// if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; +// if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; +// if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; +// if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; +// if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; +// if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; +// if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; + +// if (tick > 0) ratio = type(uint256).max / ratio; + +// // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. +// // we then downcast because we know the result always fits within 160 bits due to our tick input constraint +// // we round up in the division so getTickAtSqrtRatio of the output price is always consistent +// sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); +// } +// } diff --git a/contracts/protocol/core/twaps/EtherexVolatileTwap.sol b/contracts/protocol/core/twaps/EtherexVolatileTwap.sol index 132340e..afae41e 100644 --- a/contracts/protocol/core/twaps/EtherexVolatileTwap.sol +++ b/contracts/protocol/core/twaps/EtherexVolatileTwap.sol @@ -7,8 +7,10 @@ import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol" import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; +import "forge-std/console2.sol"; + /// @title Oracle using Thena TWAP oracle as data source -/// @author zefram.eth/lookee/Eidolon +/// @author xRave110 /// @notice The oracle contract that provides the current price to purchase /// the underlying token while exercising options. Uses Thena TWAP oracle /// as data source, and then applies a lower bound. @@ -33,7 +35,7 @@ contract EtherexVolatileTwap is ITwapOracle, Ownable { /// Events /// ----------------------------------------------------------------------- - event SetParams(uint56 secs, uint128 minPrice); + event SetParams(uint128 maxPrice, uint128 minPrice); /// ----------------------------------------------------------------------- /// Immutable parameters @@ -49,43 +51,26 @@ contract EtherexVolatileTwap is ITwapOracle, Ownable { /// ----------------------------------------------------------------------- /// @notice The size of the window to take the TWAP value over in seconds. - uint56 public secs; + uint56 public timeWindow; /// @notice The minimum value returned by getPrice(). Maintains a floor for the /// price to mitigate potential attacks on the TWAP oracle. uint128 public minPrice; - - /// @notice Whether the price should be returned in terms of token0. - /// If false, the price is returned in terms of token1. - bool public isToken0; + uint128 public maxPrice; /// ----------------------------------------------------------------------- /// Constructor /// ----------------------------------------------------------------------- - constructor( - IEtherexPair etherexPair_, - address token, - address owner_, - uint56 secs_, - uint128 minPrice_ - ) Ownable(owner_) { - if ( - ERC20(etherexPair_.token0()).decimals() != 18 - || ERC20(etherexPair_.token1()).decimals() != 18 - ) revert ThenaOracle__InvalidParams(); - if (etherexPair_.stable()) revert ThenaOracle__StablePairsUnsupported(); - if (etherexPair_.token0() != token && etherexPair_.token1() != token) { - revert ThenaOracle__InvalidParams(); - } - if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); - - etherexPair = etherexPair_; - isToken0 = etherexPair_.token0() == token; - secs = secs_; - minPrice = minPrice_; + constructor(IEtherexPair _etherexPair, address _owner, uint56 _timeWindow, uint128 _minPrice) + Ownable(_owner) + { + if (_timeWindow < MIN_SECS) revert ThenaOracle__InvalidWindow(); + timeWindow = _timeWindow; + etherexPair = _etherexPair; + minPrice = _minPrice; - emit SetParams(secs_, minPrice_); + emit SetParams(_timeWindow, _minPrice); } /// ----------------------------------------------------------------------- @@ -94,59 +79,47 @@ contract EtherexVolatileTwap is ITwapOracle, Ownable { /// @inheritdoc ITwapOracle function getAssetPrice(address _asset) external view override returns (uint256 price) { - if (_asset != etherexPair.token0() && _asset != etherexPair.token1()) { - revert ThenaOracle__InvalidParams(); + price = etherexPair.current(_asset, 10 ** ERC20(_asset).decimals()); + + if (price < minPrice) revert ThenaOracle__BelowMinPrice(); + } + + /* add only assets in the pool ! */ + function getAssetPriceWithQuote(address _asset) external view returns (uint256 price) { + uint256 granuality = 1; + uint256 _timeWindow = timeWindow; + uint256 timeElapsed = 0; + uint256 length = etherexPair.observationLength(); + for (; timeElapsed < _timeWindow; granuality++) { + timeElapsed = block.timestamp - etherexPair.observations(length - granuality).timestamp; + console2.log("Time elapsed: %s vs timeWindow %s", timeElapsed, timeWindow); } - /// ----------------------------------------------------------------------- - /// Storage loads - /// ----------------------------------------------------------------------- - - uint256 secs_ = secs; - - /// ----------------------------------------------------------------------- - /// Computation - /// ----------------------------------------------------------------------- - - // query Thena oracle to get TWAP value - { - ( - uint256 reserve0CumulativeCurrent, - uint256 reserve1CumulativeCurrent, - uint256 blockTimestampCurrent - ) = etherexPair.currentCumulativePrices(); - uint256 observationLength = etherexPair.observationLength(); - IEtherexPair.Observation memory lastObs = etherexPair.lastObservation(); - - uint32 T = uint32(blockTimestampCurrent - lastObs.timestamp); - if (T < secs_) { - lastObs = etherexPair.observations(observationLength - 2); - T = uint32(blockTimestampCurrent - lastObs.timestamp); - } - uint112 reserve0 = safe112((reserve0CumulativeCurrent - lastObs.reserve0Cumulative) / T); - uint112 reserve1 = safe112((reserve1CumulativeCurrent - lastObs.reserve1Cumulative) / T); - - if (!isToken0) { - price = uint256(reserve0) * WAD / (reserve1); - } else { - price = uint256(reserve1) * WAD / (reserve0); - } + + console2.log("Granuality: ", granuality); + price = etherexPair.quote(_asset, 10 ** ERC20(_asset).decimals(), granuality); + + if (price < minPrice) revert ThenaOracle__BelowMinPrice(); + } + + function getAssetPriceWithSampleWindow(address _asset) external view returns (uint256 price) { + uint256 granuality = 1; + uint256 _timeWindow = timeWindow; + uint256 timeElapsed = 0; + uint256 length = etherexPair.observationLength(); + for (; timeElapsed < _timeWindow; granuality++) { + timeElapsed = block.timestamp - etherexPair.observations(length - granuality).timestamp; + console2.log("Time elapsed: %s vs timeWindow %s", timeElapsed, timeWindow); } + console2.log("Granuality: ", granuality); + price = etherexPair.sample(_asset, 10 ** ERC20(_asset).decimals(), 1, granuality)[0]; + if (price < minPrice) revert ThenaOracle__BelowMinPrice(); } /// @inheritdoc ITwapOracle - function getTokens() - external - view - override - returns (address paymentToken, address underlyingToken) - { - if (isToken0) { - return (etherexPair.token1(), etherexPair.token0()); - } else { - return (etherexPair.token0(), etherexPair.token1()); - } + function getTokens() external view override returns (address token0, address token1) { + return (etherexPair.token0(), etherexPair.token1()); } /// ----------------------------------------------------------------------- @@ -154,22 +127,17 @@ contract EtherexVolatileTwap is ITwapOracle, Ownable { /// ----------------------------------------------------------------------- /// @notice Updates the oracle parameters. Only callable by the owner. - /// @param secs_ The size of the window to take the TWAP value over in seconds. - /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the + /// @param _maxPrice The maximum value returned by getAssetPrice(). + /// @param _minPrice The minimum value returned by getAssetPrice(). Maintains a floor for the /// price to mitigate potential attacks on the TWAP oracle. - function setParams(uint56 secs_, uint128 minPrice_) external onlyOwner { - if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); - secs = secs_; - minPrice = minPrice_; - emit SetParams(secs_, minPrice_); + function setMinMaxPrice(uint128 _maxPrice, uint128 _minPrice) external onlyOwner { + maxPrice = _maxPrice; + minPrice = _minPrice; + emit SetParams(_maxPrice, _minPrice); } - /// ----------------------------------------------------------------------- - /// Util functions - /// ----------------------------------------------------------------------- - - function safe112(uint256 n) internal pure returns (uint112) { - if (n >= 2 ** 112) revert ThenaOracle__Overflow(); - return uint112(n); + function settimeWindow(uint56 _timeWindow) external onlyOwner { + if (_timeWindow < MIN_SECS) revert ThenaOracle__InvalidWindow(); + timeWindow = _timeWindow; } } diff --git a/contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol b/contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol new file mode 100644 index 0000000..db243f3 --- /dev/null +++ b/contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.13; + +import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; +import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; + +/// @title Oracle using Thena TWAP oracle as data source +/// @author zefram.eth/lookee/Eidolon +/// @notice The oracle contract that provides the current price to purchase +/// the underlying token while exercising options. Uses Thena TWAP oracle +/// as data source, and then applies a lower bound. +contract EtherexVolatileTwapOld is ITwapOracle, Ownable { + /// ----------------------------------------------------------------------- + /// Library usage + /// ----------------------------------------------------------------------- + + using FixedPointMathLib for uint256; + + /// ----------------------------------------------------------------------- + /// Errors + /// ----------------------------------------------------------------------- + + error ThenaOracle__InvalidParams(); + error ThenaOracle__InvalidWindow(); + error ThenaOracle__StablePairsUnsupported(); + error ThenaOracle__Overflow(); + error ThenaOracle__BelowMinPrice(); + + /// ----------------------------------------------------------------------- + /// Events + /// ----------------------------------------------------------------------- + + event SetParams(uint56 secs, uint128 minPrice); + + /// ----------------------------------------------------------------------- + /// Immutable parameters + /// ----------------------------------------------------------------------- + uint256 internal constant WAD = 1e18; + uint256 internal constant MIN_SECS = 20 minutes; + + /// @notice The Thena TWAP oracle contract (usually a pool with oracle support) + IEtherexPair public immutable etherexPair; + + /// ----------------------------------------------------------------------- + /// Storage variables + /// ----------------------------------------------------------------------- + + /// @notice The size of the window to take the TWAP value over in seconds. + uint56 public secs; + + /// @notice The minimum value returned by getPrice(). Maintains a floor for the + /// price to mitigate potential attacks on the TWAP oracle. + uint128 public minPrice; + + /// @notice Whether the price should be returned in terms of token0. + /// If false, the price is returned in terms of token1. + bool public isToken0; + + /// ----------------------------------------------------------------------- + /// Constructor + /// ----------------------------------------------------------------------- + + constructor( + IEtherexPair etherexPair_, + address token, + address owner_, + uint56 secs_, + uint128 minPrice_ + ) Ownable(owner_) { + if ( + ERC20(etherexPair_.token0()).decimals() != 18 + || ERC20(etherexPair_.token1()).decimals() != 18 + ) revert ThenaOracle__InvalidParams(); + if (etherexPair_.stable()) revert ThenaOracle__StablePairsUnsupported(); + if (etherexPair_.token0() != token && etherexPair_.token1() != token) { + revert ThenaOracle__InvalidParams(); + } + if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); + + etherexPair = etherexPair_; + isToken0 = etherexPair_.token0() == token; + secs = secs_; + minPrice = minPrice_; + + emit SetParams(secs_, minPrice_); + } + + /// ----------------------------------------------------------------------- + /// IOracle + /// ----------------------------------------------------------------------- + + /// @inheritdoc ITwapOracle + function getAssetPrice(address _asset) external view override returns (uint256 price) { + if ( + (isToken0 && _asset != etherexPair.token0()) + && (!isToken0 && _asset != etherexPair.token1()) + ) { + revert ThenaOracle__InvalidParams(); + } + /// ----------------------------------------------------------------------- + /// Storage loads + /// ----------------------------------------------------------------------- + + uint256 secs_ = secs; + + /// ----------------------------------------------------------------------- + /// Computation + /// ----------------------------------------------------------------------- + + // query Thena oracle to get TWAP value + { + ( + uint256 reserve0CumulativeCurrent, + uint256 reserve1CumulativeCurrent, + uint256 blockTimestampCurrent + ) = etherexPair.currentCumulativePrices(); + uint256 observationLength = etherexPair.observationLength(); + IEtherexPair.Observation memory lastObs = etherexPair.lastObservation(); + + uint32 T = uint32(blockTimestampCurrent - lastObs.timestamp); + if (T < secs_) { + lastObs = etherexPair.observations(observationLength - 2); + T = uint32(blockTimestampCurrent - lastObs.timestamp); + } + uint112 reserve0 = safe112((reserve0CumulativeCurrent - lastObs.reserve0Cumulative) / T); + uint112 reserve1 = safe112((reserve1CumulativeCurrent - lastObs.reserve1Cumulative) / T); + + if (!isToken0) { + price = uint256(reserve0) * WAD / (reserve1); + } else { + price = uint256(reserve1) * WAD / (reserve0); + } + } + + if (price < minPrice) revert ThenaOracle__BelowMinPrice(); + } + + /// @inheritdoc ITwapOracle + function getTokens() + external + view + override + returns (address paymentToken, address underlyingToken) + { + if (isToken0) { + return (etherexPair.token1(), etherexPair.token0()); + } else { + return (etherexPair.token0(), etherexPair.token1()); + } + } + + /// ----------------------------------------------------------------------- + /// Owner functions + /// ----------------------------------------------------------------------- + + /// @notice Updates the oracle parameters. Only callable by the owner. + /// @param secs_ The size of the window to take the TWAP value over in seconds. + /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the + /// price to mitigate potential attacks on the TWAP oracle. + function setParams(uint56 secs_, uint128 minPrice_) external onlyOwner { + if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); + secs = secs_; + minPrice = minPrice_; + emit SetParams(secs_, minPrice_); + } + + /// ----------------------------------------------------------------------- + /// Util functions + /// ----------------------------------------------------------------------- + + function safe112(uint256 n) internal pure returns (uint112) { + if (n >= 2 ** 112) revert ThenaOracle__Overflow(); + return uint112(n); + } +} diff --git a/tests/foundry/TwapLinea.t.sol b/tests/foundry/TwapLinea.t.sol new file mode 100644 index 0000000..93c8ddf --- /dev/null +++ b/tests/foundry/TwapLinea.t.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {EtherexVolatileTwap} from "contracts/protocol/core/twaps/EtherexVolatileTwap.sol"; +import {EtherexVolatileTwapOld} from "contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol"; +import {IRouter} from "contracts/interfaces/IRouter.sol"; + +import {console2} from "forge-std/console2.sol"; + +contract TwapLineaTest is Test { + ERC20 constant ASUSD = ERC20(0xa500000000e482752f032eA387390b6025a2377b); + ERC20 constant USDC = ERC20(0x176211869cA2b568f2A7D4EE941E073a821EE1ff); + ERC20 constant REX33 = ERC20(0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4); + IEtherexPair constant ASUSD_USDC_PAIR = IEtherexPair(0x7b930713103A964c12E8b808c83F57E40d9ad495); + IEtherexPair constant REX33_USDC_PAIR = IEtherexPair(0xeacD56565aB642FB0Dc2820b51547fE416EE8697); + uint256 constant TIME_WINDOW = 100 minutes; + uint256 constant LOG_WINDOW = 7 days; + uint256 constant MIN_PRICE = 0; + EtherexVolatileTwap asUsdEtherexVolatileTwap; + EtherexVolatileTwap rex33EtherexVolatileTwap; + + address constant ETHEREX_ROUTER = 0x32dB39c56C171b4c96e974dDeDe8E42498929c54; + + function setUp() public { + // LINEA setup + uint256 opFork = vm.createSelectFork( + "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d" + ); + assertEq(vm.activeFork(), opFork); + asUsdEtherexVolatileTwap = new EtherexVolatileTwap( + ASUSD_USDC_PAIR, address(this), uint56(TIME_WINDOW), uint128(MIN_PRICE) + ); + + rex33EtherexVolatileTwap = new EtherexVolatileTwap( + REX33_USDC_PAIR, address(this), uint56(TIME_WINDOW), uint128(MIN_PRICE) + ); + } + + function testAssetPriceAfterSwaps() public { + console2.log( + "The USDC price current: ", asUsdEtherexVolatileTwap.getAssetPrice(address(ASUSD)) + ); + console2.log( + "The USDC price quote: ", + asUsdEtherexVolatileTwap.getAssetPriceWithQuote(address(ASUSD)) + ); + console2.log( + "The USDC price sampleWindow: ", + asUsdEtherexVolatileTwap.getAssetPriceWithSampleWindow(address(ASUSD)) + ); + + console2.log( + "The REX33 price current: ", rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + console2.log( + "The REX33 price quote: ", + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) + ); + console2.log( + "The REX33 price sampleWindow: ", + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) + ); + } + + function testCompabilityWithOracle() public {} + + function test_singleBlockManipulation() public { + address manipulator = makeAddr("manipulator"); + deal(address(REX33), manipulator, 1000000 ether); + + // register initial oracle price + uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); + uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); + uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); + + // perform a large swap + vm.startPrank(manipulator); + REX33.approve(ETHEREX_ROUTER, 1000000 ether); + + IRouter.route[] memory swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 10, + 0, + swapRoute, + manipulator, + type(uint32).max + ); + vm.stopPrank(); + + // price should not have changed + assertEq( + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), + price_1, + "single block price variation" + ); + assertEq( + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), + price_2, + "single block price variation" + ); + assertEq( + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), + price_3, + "single block price variation" + ); + } + + function test_priceManipulation(uint256 skipTime) public { + skipTime = 30 minutes; + + // clean twap for test + skip(1 hours); + REX33_USDC_PAIR.sync(); + skip(1 hours); + REX33_USDC_PAIR.sync(); + skip(1 hours); + + uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); + uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); + uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); + + // perform a large swap + address manipulator = makeAddr("manipulator"); + deal(address(REX33), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); + uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; + REX33.approve(ETHEREX_ROUTER, amountIn); + + IRouter.route[] memory swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + vm.stopPrank(); + + // wait + skip(skipTime); + + console2.log( + "price_1: %s vs The REX33 price current: %s", + price_1, + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + console2.log( + "price_2: %s The REX33 price quote: %s", + price_2, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) + ); + console2.log( + "price_3: %s The REX33 price sampleWindow: %s", + price_3, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) + ); + assert(false); + } + + // function test_PriceManipulationWithLoop(uint256 secs) public { + // // string memory path = "oracleSim.txt"; + + // //secs = bound(secs, 1, 1 days); + // secs = 2880 minutes; + // uint256 granuality = 60 minutes; + // uint256 period = secs / granuality; + // _default.secs = uint32(secs); + // uint256 skipTime; + // skipTime = bound(skipTime, 1, _default.secs); + // ThenaOracle oracle = new ThenaOracle( + // _default.pair, _default.token, _default.owner, _default.secs, _default.minPrice + // ); + + // // clean twap for test + // skip(1 hours); + // _default.pair.sync(); + // skip(1 hours); + // _default.pair.sync(); + // skip(1 hours); + + // // register initial oracle price + // uint256 price_1 = oracle.getPrice(); + // console.log("Initial price after stabilization: %s", price_1); + + // // perform a large swap + // address manipulator = makeAddr("manipulator"); + // deal(TOKEN_ADDRESS, manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (uint256 reserve0, uint256 reserve1,) = _default.pair.getReserves(); + // uint256 amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve0 : reserve1) / 4; + // IERC20(TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); + // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( + // amountIn, 0, TOKEN_ADDRESS, PAYMENT_TOKEN_ADDRESS, false, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // uint256 timeElapsed = 0; + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // console.log( + // "Time: %s, Twap: %s, Spot: %s", + // timeElapsed / 1 minutes, + // oracle.getPrice(), + // getSpotPrice(_default.pair, _default.token) + // ); + // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // // vm.writeFile(path, data); + // } + // assert(false); + // } + + // function test_PriceManipulationWithLoopMulti(uint256 secs) public { + // // string memory path = "oracleSim.txt"; + + // //secs = bound(secs, 1, 1 days); + // secs = 2 hours; + // uint256 granuality = 30 minutes; + // uint256 period = 16; + // _default.secs = uint32(secs); + // uint256 skipTime; + // skipTime = bound(skipTime, 1, _default.secs); + // ThenaOracle oracle = new ThenaOracle( + // _default.pair, _default.token, _default.owner, _default.secs, _default.minPrice + // ); + + // // clean twap for test + // _default.pair.sync(); + // skip(1 hours); + // _default.pair.sync(); + // skip(1 hours); + + // // register initial oracle price + // uint256 price_1 = oracle.getPrice(); + // console.log("Initial price after stabilization: %s", price_1); + + // // perform a large swap + // address manipulator = makeAddr("manipulator"); + // deal(TOKEN_ADDRESS, manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (uint256 reserve0, uint256 reserve1,) = _default.pair.getReserves(); + // uint256 amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve0 : reserve1) / 4; + // IERC20(TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); + // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( + // amountIn, 0, TOKEN_ADDRESS, PAYMENT_TOKEN_ADDRESS, false, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // uint256 timeElapsed = 0; + // for (uint256 idx = 0; idx < period; idx++) { + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // console.log( + // "Time: %s, Twap: %s, Spot: %s", + // timeElapsed / 1 minutes, + // oracle.getPrice(), + // getSpotPrice(_default.pair, _default.token) + // ); + // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // // vm.writeFile(path, data); + // skip(granuality); + // } + // // perform a large swap + // deal(PAYMENT_TOKEN_ADDRESS, manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = _default.pair.getReserves(); + // amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve1 : reserve0) / 4; + // IERC20(PAYMENT_TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); + // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( + // amountIn, 0, PAYMENT_TOKEN_ADDRESS, TOKEN_ADDRESS, false, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // console.log( + // "Time: %s, Twap: %s, Spot: %s", + // timeElapsed / 1 minutes, + // oracle.getPrice(), + // getSpotPrice(_default.pair, _default.token) + // ); + // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // // vm.writeFile(path, data); + // } + + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = _default.pair.getReserves(); + // amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve0 : reserve1) / 10; + // IERC20(TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); + // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( + // amountIn, 0, TOKEN_ADDRESS, PAYMENT_TOKEN_ADDRESS, false, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // console.log( + // "Time: %s, Twap: %s, Spot: %s", + // timeElapsed / 1 minutes, + // oracle.getPrice(), + // getSpotPrice(_default.pair, _default.token) + // ); + // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // // vm.writeFile(path, data); + // } + + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = _default.pair.getReserves(); + // amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve1 : reserve0) / 4; + // IERC20(PAYMENT_TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); + // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( + // amountIn, 0, PAYMENT_TOKEN_ADDRESS, TOKEN_ADDRESS, false, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // console.log( + // "Time: %s, Twap: %s, Spot: %s", + // timeElapsed / 1 minutes, + // oracle.getPrice(), + // getSpotPrice(_default.pair, _default.token) + // ); + // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // // vm.writeFile(path, data); + // } + + // assert(false); + // } +} From d990961f85941ec2d9569e9c7f7b26dcc7c0ad89 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Fri, 19 Sep 2025 13:19:51 +0200 Subject: [PATCH 07/13] New tests --- oracleAnalysis.py | 59 +++++ tests/foundry/TwapLinea.t.sol | 465 +++++++++++++++++++++------------- 2 files changed, 341 insertions(+), 183 deletions(-) create mode 100644 oracleAnalysis.py diff --git a/oracleAnalysis.py b/oracleAnalysis.py new file mode 100644 index 0000000..45f51b1 --- /dev/null +++ b/oracleAnalysis.py @@ -0,0 +1,59 @@ +import matplotlib.pyplot as plt +from matplotlib.ticker import MultipleLocator + +# Read and print the contents of a text file +def read_text_file(file_path): + x_time = [] + y_twap = [] + y_spot = [] + + try: + with open(file_path, 'r') as file: + for line in file: + splitted_list = line.split(", ") + #print(splitted_list) + print(int(splitted_list[0].split(": ")[-1]) / 60) + if(int(splitted_list[0].split(": ")[-1]) % 60 == 0): + time_s = str(int(int(splitted_list[0].split(": ")[-1]) / 60)) + "hr" + else: + time_s = splitted_list[0].split(": ")[-1] + + x_time.append(time_s) + y_twap.append(int(splitted_list[1].split(": ")[-1])/1e18) + y_spot.append(int(splitted_list[2].split(": ")[-1])/1e18) + except FileNotFoundError: + print(f"The file {file_path} does not exist.") + except Exception as err: + print(f"An error occurred: {err}") + print(x_time) + print(y_twap) + print(y_spot) + return x_time, y_twap, y_spot + +# Example usage +file_path = "./Data120MinEtherex1.txt" +x_time, y_twap, y_spot = read_text_file(file_path) + + +# Create a figure and axis +fig, ax = plt.subplots() + +# Plot data +ax.plot(x_time, y_twap, label='Twap', color='blue') + +ax.plot(x_time, y_spot, label='Spot', color='red') + +# Set title and labels +ax.set_title('Oracle price ({})'.format(file_path)) +ax.set_xlabel('Time [min]') +ax.set_ylabel('Pirces') + +plt.gca().xaxis.set_major_locator(MultipleLocator(2)) +plt.gca().yaxis.set_major_locator(MultipleLocator(0.01)) +ax.legend() +plt.grid(True) + +# Show the plot +plt.savefig('plot.png') # Save instead of show +plt.close() # Free memory +# plt.show(block=False) # Non-blocking show \ No newline at end of file diff --git a/tests/foundry/TwapLinea.t.sol b/tests/foundry/TwapLinea.t.sol index 93c8ddf..7daaf68 100644 --- a/tests/foundry/TwapLinea.t.sol +++ b/tests/foundry/TwapLinea.t.sol @@ -16,11 +16,12 @@ contract TwapLineaTest is Test { ERC20 constant REX33 = ERC20(0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4); IEtherexPair constant ASUSD_USDC_PAIR = IEtherexPair(0x7b930713103A964c12E8b808c83F57E40d9ad495); IEtherexPair constant REX33_USDC_PAIR = IEtherexPair(0xeacD56565aB642FB0Dc2820b51547fE416EE8697); - uint256 constant TIME_WINDOW = 100 minutes; + uint256 constant TIME_WINDOW = 120 minutes; uint256 constant LOG_WINDOW = 7 days; uint256 constant MIN_PRICE = 0; EtherexVolatileTwap asUsdEtherexVolatileTwap; EtherexVolatileTwap rex33EtherexVolatileTwap; + bool constant USE_QUOTE = true; address constant ETHEREX_ROUTER = 0x32dB39c56C171b4c96e974dDeDe8E42498929c54; @@ -157,189 +158,287 @@ contract TwapLineaTest is Test { price_3, rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) ); + // assert(false); + } + + function test_PriceManipulationWithLoop() public { + // string memory path = "oracleSim.txt"; + + uint256 granuality = 10 minutes; + uint256 period = (TIME_WINDOW + 2 * granuality) / granuality; + uint256 skipTime; + + // clean twap for test + skip(1 hours); + rex33EtherexVolatileTwap.etherexPair().sync(); + skip(1 hours); + rex33EtherexVolatileTwap.etherexPair().sync(); + skip(1 hours); + + // register initial oracle price + + uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); + uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); + uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); + console.log("1.Initial price after stabilization: %s", price_1); + console.log("2.Initial price after stabilization: %s", price_2); + console.log("3.Initial price after stabilization: %s", price_3); + + // perform a large swap + address manipulator = makeAddr("manipulator"); + deal(address(REX33), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); + uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; + REX33.approve(ETHEREX_ROUTER, amountIn); + + IRouter.route[] memory swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + vm.stopPrank(); + + // wait + skip(skipTime); + + console2.log( + "price_1: %s vs The REX33 price current: %s", + price_1, + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + console2.log( + "price_2: %s The REX33 price quote: %s", + price_2, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) + ); + console2.log( + "price_3: %s The REX33 price sampleWindow: %s", + price_3, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) + ); + deal(address(REX33), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); + amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 40; + REX33.approve(ETHEREX_ROUTER, amountIn); + + swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + // wait + uint256 timeElapsed = 0; + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + if (USE_QUOTE) { + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } else { + console2.log( + "Time: %s, Twap2: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } + + // vm.writeFile(path, data); + } assert(false); } - // function test_PriceManipulationWithLoop(uint256 secs) public { - // // string memory path = "oracleSim.txt"; - - // //secs = bound(secs, 1, 1 days); - // secs = 2880 minutes; - // uint256 granuality = 60 minutes; - // uint256 period = secs / granuality; - // _default.secs = uint32(secs); - // uint256 skipTime; - // skipTime = bound(skipTime, 1, _default.secs); - // ThenaOracle oracle = new ThenaOracle( - // _default.pair, _default.token, _default.owner, _default.secs, _default.minPrice - // ); - - // // clean twap for test - // skip(1 hours); - // _default.pair.sync(); - // skip(1 hours); - // _default.pair.sync(); - // skip(1 hours); - - // // register initial oracle price - // uint256 price_1 = oracle.getPrice(); - // console.log("Initial price after stabilization: %s", price_1); - - // // perform a large swap - // address manipulator = makeAddr("manipulator"); - // deal(TOKEN_ADDRESS, manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (uint256 reserve0, uint256 reserve1,) = _default.pair.getReserves(); - // uint256 amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve0 : reserve1) / 4; - // IERC20(TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); - // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( - // amountIn, 0, TOKEN_ADDRESS, PAYMENT_TOKEN_ADDRESS, false, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // uint256 timeElapsed = 0; - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // console.log( - // "Time: %s, Twap: %s, Spot: %s", - // timeElapsed / 1 minutes, - // oracle.getPrice(), - // getSpotPrice(_default.pair, _default.token) - // ); - // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // // vm.writeFile(path, data); - // } - // assert(false); - // } - - // function test_PriceManipulationWithLoopMulti(uint256 secs) public { - // // string memory path = "oracleSim.txt"; - - // //secs = bound(secs, 1, 1 days); - // secs = 2 hours; - // uint256 granuality = 30 minutes; - // uint256 period = 16; - // _default.secs = uint32(secs); - // uint256 skipTime; - // skipTime = bound(skipTime, 1, _default.secs); - // ThenaOracle oracle = new ThenaOracle( - // _default.pair, _default.token, _default.owner, _default.secs, _default.minPrice - // ); - - // // clean twap for test - // _default.pair.sync(); - // skip(1 hours); - // _default.pair.sync(); - // skip(1 hours); - - // // register initial oracle price - // uint256 price_1 = oracle.getPrice(); - // console.log("Initial price after stabilization: %s", price_1); - - // // perform a large swap - // address manipulator = makeAddr("manipulator"); - // deal(TOKEN_ADDRESS, manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (uint256 reserve0, uint256 reserve1,) = _default.pair.getReserves(); - // uint256 amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve0 : reserve1) / 4; - // IERC20(TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); - // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( - // amountIn, 0, TOKEN_ADDRESS, PAYMENT_TOKEN_ADDRESS, false, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // uint256 timeElapsed = 0; - // for (uint256 idx = 0; idx < period; idx++) { - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // console.log( - // "Time: %s, Twap: %s, Spot: %s", - // timeElapsed / 1 minutes, - // oracle.getPrice(), - // getSpotPrice(_default.pair, _default.token) - // ); - // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // // vm.writeFile(path, data); - // skip(granuality); - // } - // // perform a large swap - // deal(PAYMENT_TOKEN_ADDRESS, manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = _default.pair.getReserves(); - // amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve1 : reserve0) / 4; - // IERC20(PAYMENT_TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); - // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( - // amountIn, 0, PAYMENT_TOKEN_ADDRESS, TOKEN_ADDRESS, false, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // console.log( - // "Time: %s, Twap: %s, Spot: %s", - // timeElapsed / 1 minutes, - // oracle.getPrice(), - // getSpotPrice(_default.pair, _default.token) - // ); - // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // // vm.writeFile(path, data); - // } - - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = _default.pair.getReserves(); - // amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve0 : reserve1) / 10; - // IERC20(TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); - // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( - // amountIn, 0, TOKEN_ADDRESS, PAYMENT_TOKEN_ADDRESS, false, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // console.log( - // "Time: %s, Twap: %s, Spot: %s", - // timeElapsed / 1 minutes, - // oracle.getPrice(), - // getSpotPrice(_default.pair, _default.token) - // ); - // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // // vm.writeFile(path, data); - // } - - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = _default.pair.getReserves(); - // amountIn = (TOKEN_ADDRESS == _default.pair.token0() ? reserve1 : reserve0) / 4; - // IERC20(PAYMENT_TOKEN_ADDRESS).approve(THENA_ROUTER, amountIn); - // IThenaRouter(THENA_ROUTER).swapExactTokensForTokensSimple( - // amountIn, 0, PAYMENT_TOKEN_ADDRESS, TOKEN_ADDRESS, false, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // console.log( - // "Time: %s, Twap: %s, Spot: %s", - // timeElapsed / 1 minutes, - // oracle.getPrice(), - // getSpotPrice(_default.pair, _default.token) - // ); - // //("Time: %s, Twap: %s, Spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // // vm.writeFile(path, data); - // } - - // assert(false); - // } + function test_MultiplePriceManipulationWithLoop() public { + uint256 granuality = 10 minutes; + uint256 period = TIME_WINDOW / granuality; + uint256 skipTime; + + // clean twap for test + skip(1 hours); + rex33EtherexVolatileTwap.etherexPair().sync(); + skip(1 hours); + rex33EtherexVolatileTwap.etherexPair().sync(); + skip(1 hours); + + // register initial oracle price + + uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); + uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); + uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); + console.log("1.Initial price after stabilization: %s", price_1); + console.log("2.Initial price after stabilization: %s", price_2); + console.log("3.Initial price after stabilization: %s", price_3); + + // perform a large swap + address manipulator = makeAddr("manipulator"); + deal(address(REX33), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); + uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; + REX33.approve(ETHEREX_ROUTER, amountIn); + + IRouter.route[] memory swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + vm.stopPrank(); + + // wait + skip(skipTime); + + console2.log( + "price_1: %s vs The REX33 price current: %s", + price_1, + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + console2.log( + "price_2: %s The REX33 price quote: %s", + price_2, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) + ); + console2.log( + "price_3: %s The REX33 price sampleWindow: %s", + price_3, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) + ); + + // wait + uint256 timeElapsed = 0; + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + if (USE_QUOTE) { + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } else { + console2.log( + "Time: %s, Twap2: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } + } + + // perform a large swap + deal(address(USDC), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); + amountIn = (address(USDC) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; + USDC.approve(ETHEREX_ROUTER, amountIn); + + swapRoute[0] = IRouter.route({from: address(USDC), to: address(REX33), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + vm.stopPrank(); + + // wait + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + if (USE_QUOTE) { + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } else { + console2.log( + "Time: %s, Twap2: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } + } + + // perform a large swap + manipulator = makeAddr("manipulator"); + deal(address(REX33), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); + amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 10; + REX33.approve(ETHEREX_ROUTER, amountIn); + + swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + vm.stopPrank(); + + // wait + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + if (USE_QUOTE) { + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } else { + console2.log( + "Time: %s, Twap2: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } + } + + // perform a large swap + deal(address(USDC), manipulator, 2 ** 128); + vm.startPrank(manipulator); + (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); + amountIn = (address(USDC) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; + USDC.approve(ETHEREX_ROUTER, amountIn); + + swapRoute[0] = IRouter.route({from: address(USDC), to: address(REX33), stable: false}); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + amountIn, 0, swapRoute, manipulator, type(uint32).max + ); + vm.stopPrank(); + + // wait + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + if (USE_QUOTE) { + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } else { + console2.log( + "Time: %s, Twap2: %s, Spot: %s", + timeElapsed / 1 minutes, + rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), + rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + ); + } + } + + assert(false); + } } From 97765cd667cd90a963ef24802607d2ba8b9313e7 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Wed, 24 Sep 2025 14:35:15 +0200 Subject: [PATCH 08/13] Developed EtherexTwap --- contracts/protocol/core/twaps/EtherexTwap.sol | 244 ++++++++++++++++++ oracleAnalysis.py | 9 +- 2 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 contracts/protocol/core/twaps/EtherexTwap.sol diff --git a/contracts/protocol/core/twaps/EtherexTwap.sol b/contracts/protocol/core/twaps/EtherexTwap.sol new file mode 100644 index 0000000..141c99e --- /dev/null +++ b/contracts/protocol/core/twaps/EtherexTwap.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.13; + +// import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; +import {IChainlinkAggregator} from "contracts/interfaces/base/IChainlinkAggregator.sol"; + +/** + * @title Oracle using Etherex TWAP oracle as data source + * @author xRave110 + * @notice The oracle contract that provides the current price to purchase the asset for astera. + * Uses Etherex TWAP oracle as data source, and then applies a lower bound. + */ +contract EtherexTwap is IChainlinkAggregator, Ownable { + /* Errors */ + error EtherexTwap__InvalidAddress(); + error EtherexTwap__InvalidWindow(); + error EtherexTwap__BelowMinPrice(); + error EtherexTwap__WrongAsset(); + error EtherexTwap_InvalidParams(); + error EtherexTwap__StablePairsUnsupported(); + error EtherexTwap__ServiceNotAvailable(); + + /* Events */ + event SetParams(uint128 minPrice); + event SetTimeWindow(uint256 timeWindow); + + uint256 internal constant MIN_TIME_WINDOW = 20 minutes; + uint256 internal constant WAD = 1e18; + + /** + * @notice The Etherex TWAP oracle contract (pair pool with oracle support) + */ + IEtherexPair public immutable etherexPair; + + /** + * @notice The size of the window to take the TWAP value over in seconds. + */ + uint56 public timeWindow; + + /** + * @notice The minimum value returned by getAssetPrice(). Maintains a floor for the + * price to mitigate potential attacks on the TWAP oracle. + */ + uint128 public minPrice; + + address public token; + + /// @notice Whether the price should be returned in terms of token0. + /// If false, the price is returned in terms of token1. + bool isToken0; + + IChainlinkAggregator priceFeed; + + constructor( + IEtherexPair _etherexPair, + address _owner, + uint56 _timeWindow, + uint128 _minPrice, + address _priceFeed, + address _token + ) Ownable(_owner) { + /* Checks */ + if (address(_etherexPair) == address(0) || _priceFeed == address(0) || _token == address(0)) + { + revert EtherexTwap__InvalidAddress(); + } + if (_timeWindow < MIN_TIME_WINDOW) { + revert EtherexTwap__InvalidWindow(); + } + if ( + ERC20(_etherexPair.token0()).decimals() != 18 + || ERC20(_etherexPair.token1()).decimals() != 18 + ) revert EtherexTwap_InvalidParams(); + if (_etherexPair.stable()) revert EtherexTwap__StablePairsUnsupported(); + + /* Assignment */ + timeWindow = _timeWindow; + etherexPair = _etherexPair; + minPrice = _minPrice; + isToken0 = _etherexPair.token0() == _token; + priceFeed = IChainlinkAggregator(_priceFeed); + token = _token; + + emit SetParams(_minPrice); + emit SetTimeWindow(_timeWindow); + } + + /* ---- Inherited --- */ + + /** + * @inheritdoc IChainlinkAggregator + */ + function latestAnswer() external view returns (int256) { + uint256 twapPrice = _getTwapPrice(); + int256 answer = priceFeed.latestAnswer(); + answer = int256(uint256(answer) * twapPrice / WAD); // 8 decimals; + return answer; + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function latestTimestamp() external view returns (uint256) { + uint256 lastestTimestamp = priceFeed.latestTimestamp(); + return lastestTimestamp; + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function latestRound() external view returns (uint256 roundId) { + uint256 lastestTimestamp = priceFeed.latestRound(); + return lastestTimestamp; + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function getAnswer(uint256 roundId) external pure returns (int256) { + revert EtherexTwap__ServiceNotAvailable(); + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function getTimestamp(uint256 roundId) external pure returns (uint256) { + revert EtherexTwap__ServiceNotAvailable(); + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function getRoundData(uint80 _roundId) + external + pure + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + revert EtherexTwap__ServiceNotAvailable(); + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + uint256 twapPrice = _getTwapPrice(); + (roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.latestRoundData(); + answer = int256(uint256(answer) * twapPrice / WAD); // 8 decimals; + } + + /** + * @inheritdoc IChainlinkAggregator + */ + function decimals() external pure returns (uint8) { + return 8; + } + + /* --- Additional getters --- */ + function getTokens() external view returns (address token0, address token1) { + return (etherexPair.token0(), etherexPair.token1()); + } + + function getPairAddress() external view returns (address) { + return address(etherexPair); + } + + function getSpotPrice() external view returns (int256) { + uint256 spotPrice = _getSpotPrice(); + int256 answer = priceFeed.latestAnswer(); + answer = int256(uint256(answer) * spotPrice / WAD); // 8 decimals; + return answer; + } + + /* --- Private --- */ + + function _getSpotPrice() private view returns (uint256 price) { + (uint112 _reserve0, uint112 _reserve1,) = etherexPair.getReserves(); + if (!isToken0) { + price = uint256(_reserve0) * WAD / (_reserve1); + } else { + price = uint256(_reserve1) * WAD / (_reserve0); + } + } + + function _getTwapPrice() private view returns (uint256) { + IEtherexPair.Observation memory _observation = etherexPair.lastObservation(); + (uint256 reserve0Cumulative, uint256 reserve1Cumulative,) = + etherexPair.currentCumulativePrices(); + uint256 timeElapsed = block.timestamp - _observation.timestamp; + if (timeElapsed < timeWindow) { + _observation = etherexPair.observations(etherexPair.observationLength() - 2); + timeElapsed = block.timestamp - _observation.timestamp; + } + uint112 _reserve0 = + uint112((reserve0Cumulative - _observation.reserve0Cumulative) / timeElapsed); + uint112 _reserve1 = + uint112((reserve1Cumulative - _observation.reserve1Cumulative) / timeElapsed); + uint256 twapPrice; + if (!isToken0) { + twapPrice = uint256(_reserve0) * WAD / (_reserve1); + } else { + twapPrice = uint256(_reserve1) * WAD / (_reserve0); + } + return twapPrice; + } + + /** + * @notice Updates the oracle parameters. Only callable by the owner. + * @param _minPrice The minimum value returned by getAssetPrice(). Maintains a floor for the + * price to mitigate potential attacks on the TWAP oracle. + */ + function setMinPrice(uint128 _minPrice) external onlyOwner { + minPrice = _minPrice; + emit SetParams(_minPrice); + } + + /** + * @notice Updates the oracle parameters. Only callable by the owner. + * @param _timeWindow new time window to set + */ + function setTimeWindow(uint56 _timeWindow) external onlyOwner { + if (_timeWindow < MIN_TIME_WINDOW) revert EtherexTwap__InvalidWindow(); + timeWindow = _timeWindow; + emit SetTimeWindow(timeWindow); + } +} diff --git a/oracleAnalysis.py b/oracleAnalysis.py index 45f51b1..4c5eda9 100644 --- a/oracleAnalysis.py +++ b/oracleAnalysis.py @@ -1,5 +1,6 @@ import matplotlib.pyplot as plt from matplotlib.ticker import MultipleLocator +import matplotlib.ticker as ticker # Read and print the contents of a text file def read_text_file(file_path): @@ -19,8 +20,8 @@ def read_text_file(file_path): time_s = splitted_list[0].split(": ")[-1] x_time.append(time_s) - y_twap.append(int(splitted_list[1].split(": ")[-1])/1e18) - y_spot.append(int(splitted_list[2].split(": ")[-1])/1e18) + y_twap.append(int(splitted_list[1].split(": ")[-1])/1e8) + y_spot.append(int(splitted_list[2].split(": ")[-1])/1e8) except FileNotFoundError: print(f"The file {file_path} does not exist.") except Exception as err: @@ -48,8 +49,8 @@ def read_text_file(file_path): ax.set_xlabel('Time [min]') ax.set_ylabel('Pirces') -plt.gca().xaxis.set_major_locator(MultipleLocator(2)) -plt.gca().yaxis.set_major_locator(MultipleLocator(0.01)) +plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(nbins=20)) +plt.gca().yaxis.set_major_locator(ticker.MaxNLocator(nbins=20)) ax.legend() plt.grid(True) From 01fb28ce12bced7778e0e05be4ea2de93b9c91b7 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Wed, 24 Sep 2025 15:21:02 +0200 Subject: [PATCH 09/13] Tests refactor --- tests/foundry/TwapLinea.t.sol | 681 +++++++++++++++++++++------------- 1 file changed, 417 insertions(+), 264 deletions(-) diff --git a/tests/foundry/TwapLinea.t.sol b/tests/foundry/TwapLinea.t.sol index 7daaf68..c661789 100644 --- a/tests/foundry/TwapLinea.t.sol +++ b/tests/foundry/TwapLinea.t.sol @@ -4,31 +4,39 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {EtherexTwap} from "contracts/protocol/core/twaps/EtherexTwap.sol"; import {EtherexVolatileTwap} from "contracts/protocol/core/twaps/EtherexVolatileTwap.sol"; import {EtherexVolatileTwapOld} from "contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol"; import {IRouter} from "contracts/interfaces/IRouter.sol"; - +import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; +import {IOracle} from "contracts/interfaces/IOracle.sol"; import {console2} from "forge-std/console2.sol"; contract TwapLineaTest is Test { + ERC20 constant REX = ERC20(0xEfD81eeC32B9A8222D1842ec3d99c7532C31e348); + ERC20 constant WSTETH = ERC20(0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F); ERC20 constant ASUSD = ERC20(0xa500000000e482752f032eA387390b6025a2377b); ERC20 constant USDC = ERC20(0x176211869cA2b568f2A7D4EE941E073a821EE1ff); ERC20 constant REX33 = ERC20(0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4); IEtherexPair constant ASUSD_USDC_PAIR = IEtherexPair(0x7b930713103A964c12E8b808c83F57E40d9ad495); IEtherexPair constant REX33_USDC_PAIR = IEtherexPair(0xeacD56565aB642FB0Dc2820b51547fE416EE8697); - uint256 constant TIME_WINDOW = 120 minutes; + IEtherexPair constant REX_WSTETH_PAIR = IEtherexPair(0x97a51bAEF69335b6248AFEfEBD95E90399D37b0a); + uint256 constant TIME_WINDOW = 30 minutes; uint256 constant LOG_WINDOW = 7 days; uint256 constant MIN_PRICE = 0; + address WSTETH_USDC_PRICE_FEED = 0x8eCE1AbA32716FdDe8D6482bfd88E9a0ee01f565; EtherexVolatileTwap asUsdEtherexVolatileTwap; EtherexVolatileTwap rex33EtherexVolatileTwap; - bool constant USE_QUOTE = true; + EtherexTwap wstEthRexTwap; + bool constant USE_QUOTE = false; + IOracle oracle = IOracle(0xd971e9EC7357e9306c2a138E5c4eAfC04d241C87); address constant ETHEREX_ROUTER = 0x32dB39c56C171b4c96e974dDeDe8E42498929c54; function setUp() public { // LINEA setup uint256 opFork = vm.createSelectFork( - "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d" + "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d", 23687274 ); assertEq(vm.activeFork(), opFork); asUsdEtherexVolatileTwap = new EtherexVolatileTwap( @@ -38,6 +46,23 @@ contract TwapLineaTest is Test { rex33EtherexVolatileTwap = new EtherexVolatileTwap( REX33_USDC_PAIR, address(this), uint56(TIME_WINDOW), uint128(MIN_PRICE) ); + + wstEthRexTwap = new EtherexTwap( + REX_WSTETH_PAIR, + address(this), + uint56(TIME_WINDOW), + 0, + WSTETH_USDC_PRICE_FEED, + address(REX) + ); + address[] memory assets = new address[](1); + assets[0] = address(REX); + address[] memory sources = new address[](1); + sources[0] = address(wstEthRexTwap); + uint256[] memory timeouts = new uint256[](1); + timeouts[0] = 86400; + vm.prank(0x7D66a2e916d79c0988D41F1E50a1429074ec53a4); + oracle.setAssetSources(assets, sources, timeouts); } function testAssetPriceAfterSwaps() public { @@ -46,11 +71,11 @@ contract TwapLineaTest is Test { ); console2.log( "The USDC price quote: ", - asUsdEtherexVolatileTwap.getAssetPriceWithQuote(address(ASUSD)) + asUsdEtherexVolatileTwap.getAssetPriceWithSample(address(ASUSD)) ); console2.log( "The USDC price sampleWindow: ", - asUsdEtherexVolatileTwap.getAssetPriceWithSampleWindow(address(ASUSD)) + asUsdEtherexVolatileTwap.getAssetPriceOriginal(address(ASUSD)) ); console2.log( @@ -58,61 +83,50 @@ contract TwapLineaTest is Test { ); console2.log( "The REX33 price quote: ", - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) + rex33EtherexVolatileTwap.getAssetPriceWithSample(address(REX33)) ); console2.log( "The REX33 price sampleWindow: ", - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) + rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)) ); } - function testCompabilityWithOracle() public {} + function testCompabilityWithOracle() public { + console.log(oracle.getAssetPrice(address(REX))); + } function test_singleBlockManipulation() public { address manipulator = makeAddr("manipulator"); - deal(address(REX33), manipulator, 1000000 ether); + deal(address(REX), manipulator, 1000000 ether); // register initial oracle price - uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); - uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); - uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); + int256 price_1 = wstEthRexTwap.latestAnswer(); + (, int256 price_2,,,) = wstEthRexTwap.latestRoundData(); // perform a large swap vm.startPrank(manipulator); - REX33.approve(ETHEREX_ROUTER, 1000000 ether); + REX.approve(ETHEREX_ROUTER, 1000000 ether); IRouter.route[] memory swapRoute = new IRouter.route[](1); - swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); - (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); + swapRoute[0] = IRouter.route({from: address(REX), to: address(WSTETH), stable: false}); + (uint256 reserve0, uint256 reserve1,) = wstEthRexTwap.etherexPair().getReserves(); IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 10, + (address(REX) == wstEthRexTwap.etherexPair().token0() ? reserve0 : reserve1) / 10, 0, swapRoute, manipulator, type(uint32).max ); vm.stopPrank(); - + (, int256 tmpAnswer,,,) = wstEthRexTwap.latestRoundData(); // price should not have changed - assertEq( - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), - price_1, - "single block price variation" - ); - assertEq( - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), - price_2, - "single block price variation" - ); - assertEq( - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), - price_3, - "single block price variation" - ); + assertEq(wstEthRexTwap.latestAnswer(), price_1, "single block price variation"); + assertEq(tmpAnswer, price_2, "single block price variation"); + assertEq(price_1, price_2); } function test_priceManipulation(uint256 skipTime) public { - skipTime = 30 minutes; + skipTime = bound(skipTime, 20 minutes, 70 minutes); // clean twap for test skip(1 hours); @@ -121,226 +135,106 @@ contract TwapLineaTest is Test { REX33_USDC_PAIR.sync(); skip(1 hours); - uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); - uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); - uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); + int256 price_1 = wstEthRexTwap.latestAnswer(); + (, int256 price_2,,,) = wstEthRexTwap.latestRoundData(); // perform a large swap address manipulator = makeAddr("manipulator"); - deal(address(REX33), manipulator, 2 ** 128); vm.startPrank(manipulator); - (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); - uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; - REX33.approve(ETHEREX_ROUTER, amountIn); + REX.approve(ETHEREX_ROUTER, 1000000 ether); IRouter.route[] memory swapRoute = new IRouter.route[](1); - swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + swapRoute[0] = IRouter.route({from: address(REX), to: address(WSTETH), stable: false}); + (uint256 reserve0, uint256 reserve1,) = wstEthRexTwap.etherexPair().getReserves(); IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - amountIn, 0, swapRoute, manipulator, type(uint32).max + (address(REX) == wstEthRexTwap.etherexPair().token0() ? reserve0 : reserve1) / 10, + 0, + swapRoute, + manipulator, + type(uint32).max ); vm.stopPrank(); // wait skip(skipTime); - console2.log( - "price_1: %s vs The REX33 price current: %s", - price_1, - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - console2.log( - "price_2: %s The REX33 price quote: %s", - price_2, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) - ); - console2.log( - "price_3: %s The REX33 price sampleWindow: %s", - price_3, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) - ); + (, int256 tmpAnswer,,,) = wstEthRexTwap.latestRoundData(); + // price should not have changed + assertGt(wstEthRexTwap.latestAnswer(), price_1, "single block price variation"); + assertGt(tmpAnswer, price_2, "single block price variation"); + assertEq(price_1, price_2); // assert(false); } - function test_PriceManipulationWithLoop() public { - // string memory path = "oracleSim.txt"; + function test_EtherexTwapMultiplePriceManipulationsAll18Decimals() public { + multiplePriceManipulationWithLoopChainlink(5 minutes, wstEthRexTwap); + assert(false); + } + + function multiplePriceManipulationWithLoopChainlink(uint256 granuality, EtherexTwap twap) + internal + { + uint256 period = 2 * TIME_WINDOW / granuality; - uint256 granuality = 10 minutes; - uint256 period = (TIME_WINDOW + 2 * granuality) / granuality; - uint256 skipTime; + IEtherexPair etherexPair = IEtherexPair(twap.getPairAddress()); // clean twap for test skip(1 hours); - rex33EtherexVolatileTwap.etherexPair().sync(); + etherexPair.sync(); skip(1 hours); - rex33EtherexVolatileTwap.etherexPair().sync(); + etherexPair.sync(); skip(1 hours); - // register initial oracle price - - uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); - uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); - uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); - console.log("1.Initial price after stabilization: %s", price_1); - console.log("2.Initial price after stabilization: %s", price_2); - console.log("3.Initial price after stabilization: %s", price_3); + ERC20 token0 = ERC20(etherexPair.token0()); + ERC20 token1 = ERC20(etherexPair.token1()); // perform a large swap address manipulator = makeAddr("manipulator"); - deal(address(REX33), manipulator, 2 ** 128); - vm.startPrank(manipulator); - (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); - uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; - REX33.approve(ETHEREX_ROUTER, amountIn); - - IRouter.route[] memory swapRoute = new IRouter.route[](1); - swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); - IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - amountIn, 0, swapRoute, manipulator, type(uint32).max - ); - vm.stopPrank(); - - // wait - skip(skipTime); - - console2.log( - "price_1: %s vs The REX33 price current: %s", - price_1, - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - console2.log( - "price_2: %s The REX33 price quote: %s", - price_2, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) - ); - console2.log( - "price_3: %s The REX33 price sampleWindow: %s", - price_3, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) - ); - deal(address(REX33), manipulator, 2 ** 128); + deal(address(token0), manipulator, 2 ** 128); vm.startPrank(manipulator); - (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); - amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 40; - REX33.approve(ETHEREX_ROUTER, amountIn); + (uint256 reserve0, uint256 reserve1,) = etherexPair.getReserves(); + uint256 amountIn = (address(token0) == etherexPair.token0() ? reserve0 : reserve1) / 4; + token0.approve(ETHEREX_ROUTER, amountIn); - swapRoute = new IRouter.route[](1); - swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); - IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - amountIn, 0, swapRoute, manipulator, type(uint32).max - ); - // wait uint256 timeElapsed = 0; - for (uint256 idx = 0; idx < period; idx++) { - skip(granuality); - timeElapsed += granuality; - //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - if (USE_QUOTE) { - console.log( - "Time: %s, Twap1: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } else { - console2.log( - "Time: %s, Twap2: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } - - // vm.writeFile(path, data); - } - assert(false); - } - function test_MultiplePriceManipulationWithLoop() public { - uint256 granuality = 10 minutes; - uint256 period = TIME_WINDOW / granuality; - uint256 skipTime; - - // clean twap for test - skip(1 hours); - rex33EtherexVolatileTwap.etherexPair().sync(); - skip(1 hours); - rex33EtherexVolatileTwap.etherexPair().sync(); - skip(1 hours); - - // register initial oracle price - - uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); - uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)); - uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)); - console.log("1.Initial price after stabilization: %s", price_1); - console.log("2.Initial price after stabilization: %s", price_2); - console.log("3.Initial price after stabilization: %s", price_3); - - // perform a large swap - address manipulator = makeAddr("manipulator"); - deal(address(REX33), manipulator, 2 ** 128); - vm.startPrank(manipulator); - (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); - uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; - REX33.approve(ETHEREX_ROUTER, amountIn); + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); IRouter.route[] memory swapRoute = new IRouter.route[](1); - swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + swapRoute[0] = IRouter.route({from: address(token0), to: address(token1), stable: false}); IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( amountIn, 0, swapRoute, manipulator, type(uint32).max ); vm.stopPrank(); // wait - skip(skipTime); - - console2.log( - "price_1: %s vs The REX33 price current: %s", - price_1, - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - console2.log( - "price_2: %s The REX33 price quote: %s", - price_2, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)) - ); - console2.log( - "price_3: %s The REX33 price sampleWindow: %s", - price_3, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)) - ); - - // wait - uint256 timeElapsed = 0; for (uint256 idx = 0; idx < period; idx++) { skip(granuality); + etherexPair.sync(); timeElapsed += granuality; //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - if (USE_QUOTE) { - console.log( - "Time: %s, Twap1: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } else { - console2.log( - "Time: %s, Twap2: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } + + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); } // perform a large swap - deal(address(USDC), manipulator, 2 ** 128); + deal(address(token1), manipulator, 2 ** 128); vm.startPrank(manipulator); - (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); - amountIn = (address(USDC) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; - USDC.approve(ETHEREX_ROUTER, amountIn); + (reserve0, reserve1,) = etherexPair.getReserves(); + amountIn = (address(token1) == etherexPair.token0() ? reserve0 : reserve1) / 4; + token1.approve(ETHEREX_ROUTER, amountIn); - swapRoute[0] = IRouter.route({from: address(USDC), to: address(REX33), stable: false}); + swapRoute[0] = IRouter.route({from: address(token1), to: address(token0), stable: false}); IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( amountIn, 0, swapRoute, manipulator, type(uint32).max ); @@ -350,33 +244,26 @@ contract TwapLineaTest is Test { for (uint256 idx = 0; idx < period; idx++) { skip(granuality); timeElapsed += granuality; + etherexPair.sync(); //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - if (USE_QUOTE) { - console.log( - "Time: %s, Twap1: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } else { - console2.log( - "Time: %s, Twap2: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } + + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); } // perform a large swap manipulator = makeAddr("manipulator"); - deal(address(REX33), manipulator, 2 ** 128); + deal(address(token0), manipulator, 2 ** 128); vm.startPrank(manipulator); - (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); - amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 10; - REX33.approve(ETHEREX_ROUTER, amountIn); + (reserve0, reserve1,) = etherexPair.getReserves(); + amountIn = (address(token0) == etherexPair.token0() ? reserve0 : reserve1) / 10; + token0.approve(ETHEREX_ROUTER, amountIn); - swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + swapRoute[0] = IRouter.route({from: address(token0), to: address(token1), stable: false}); IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( amountIn, 0, swapRoute, manipulator, type(uint32).max ); @@ -385,33 +272,26 @@ contract TwapLineaTest is Test { // wait for (uint256 idx = 0; idx < period; idx++) { skip(granuality); + etherexPair.sync(); timeElapsed += granuality; //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - if (USE_QUOTE) { - console.log( - "Time: %s, Twap1: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } else { - console2.log( - "Time: %s, Twap2: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } + + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); } // perform a large swap - deal(address(USDC), manipulator, 2 ** 128); + deal(address(token1), manipulator, 2 ** 128); vm.startPrank(manipulator); - (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); - amountIn = (address(USDC) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; - USDC.approve(ETHEREX_ROUTER, amountIn); + (reserve0, reserve1,) = etherexPair.getReserves(); + amountIn = (address(token1) == etherexPair.token0() ? reserve0 : reserve1) / 4; + token1.approve(ETHEREX_ROUTER, amountIn); - swapRoute[0] = IRouter.route({from: address(USDC), to: address(REX33), stable: false}); + swapRoute[0] = IRouter.route({from: address(token1), to: address(token0), stable: false}); IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( amountIn, 0, swapRoute, manipulator, type(uint32).max ); @@ -420,25 +300,298 @@ contract TwapLineaTest is Test { // wait for (uint256 idx = 0; idx < period; idx++) { skip(granuality); + etherexPair.sync(); timeElapsed += granuality; //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - if (USE_QUOTE) { - console.log( - "Time: %s, Twap1: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithQuote(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } else { - console2.log( - "Time: %s, Twap2: %s, Spot: %s", - timeElapsed / 1 minutes, - rex33EtherexVolatileTwap.getAssetPriceWithSampleWindow(address(REX33)), - rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - } - } - assert(false); + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); + } } + + // function test_EtherexTwapMultiplePriceManipulations6Decimals() public { + // address twap = address( + // new EtherexTwap(address(REX33_USDC_PAIR), address(this), uint56(TIME_WINDOW), 0) + // ); + // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); + // assert(false); + // } + + // function test_EtherexTwapMultiplePriceManipulationsOld() public { + // address twap = address( + // new EtherexVolatileTwapOld( + // REX_WSTETH_PAIR, address(WSTETH), address(this), uint56(TIME_WINDOW), 0 + // ) + // ); + // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); + // assert(false); + // } + + // function test_EtherexTwapMultiplePriceManipulationsExperiment() public { + // address twap = + // address(new EtherexVolatileTwap(REX_WSTETH_PAIR, address(this), uint56(TIME_WINDOW), 0)); + // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); + // assert(false); + // } + + // function test_PriceManipulationWithLoop() public { + // // string memory path = "oracleSim.txt"; + + // uint256 granuality = 5 minutes; + // uint256 period = (2 * TIME_WINDOW) / granuality; + // uint256 skipTime = 1 hours; + + // // clean twap for test + // skip(skipTime); + // rex33EtherexVolatileTwap.etherexPair().sync(); + // skip(skipTime); + // rex33EtherexVolatileTwap.etherexPair().sync(); + // skip(skipTime); + + // // register initial oracle price + + // uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); + // uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithSample(address(REX33)); + // uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)); + // console.log("1.Initial price after stabilization: %s", price_1); + // console.log("2.Initial price after stabilization: %s", price_2); + // console.log("3.Initial price after stabilization: %s", price_3); + + // uint256 timeElapsed = 0; + // if (USE_QUOTE) { + // console.log( + // "0.Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), + // rex33EtherexVolatileTwap.getSpotPrice(address(REX33)) + // ); + // } else { + // console2.log( + // "0.Time: %s, Twap2: %s, Spot: %s", + // timeElapsed / 1 minutes, + // rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)), + // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + // ); + // } + + // // perform a large swap + // address manipulator = makeAddr("manipulator"); + // deal(address(REX33), manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); + // uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; + // REX33.approve(ETHEREX_ROUTER, amountIn); + + // IRouter.route[] memory swapRoute = new IRouter.route[](1); + // swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + // amountIn, 0, swapRoute, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // rex33EtherexVolatileTwap.etherexPair().sync(); + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // if (USE_QUOTE) { + // console.log( + // "1. Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), + // rex33EtherexVolatileTwap.getSpotPrice(address(REX33)) + // ); + // } else { + // console2.log( + // "1.Time: %s, Twap2: %s, Spot: %s", + // timeElapsed / 1 minutes, + // rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)), + // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + // ); + // } + + // // vm.writeFile(path, data); + // } + // deal(address(REX33), manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); + // amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 40; + // REX33.approve(ETHEREX_ROUTER, amountIn); + + // swapRoute = new IRouter.route[](1); + // swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); + // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + // amountIn, 0, swapRoute, manipulator, type(uint32).max + // ); + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // rex33EtherexVolatileTwap.etherexPair().sync(); + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + // if (USE_QUOTE) { + // console.log( + // "2. Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), + // rex33EtherexVolatileTwap.getSpotPrice(address(REX33)) + // ); + // } else { + // console2.log( + // "2. Time: %s, Twap2: %s, Spot: %s", + // timeElapsed / 1 minutes, + // rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)), + // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) + // ); + // } + + // // vm.writeFile(path, data); + // } + // assert(false); + // } + + // function multiplePriceManipulationWithLoop(uint256 granuality, ITwapOracle twap) internal { + // uint256 period = 2 * TIME_WINDOW / granuality; + + // IEtherexPair etherexPair = IEtherexPair(twap.getPairAddress()); + + // // clean twap for test + // skip(1 hours); + // etherexPair.sync(); + // skip(1 hours); + // etherexPair.sync(); + // skip(1 hours); + + // ERC20 token0 = ERC20(etherexPair.token0()); + // ERC20 token1 = ERC20(etherexPair.token1()); + + // // perform a large swap + // address manipulator = makeAddr("manipulator"); + // deal(address(token0), manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (uint256 reserve0, uint256 reserve1,) = etherexPair.getReserves(); + // uint256 amountIn = (address(token0) == etherexPair.token0() ? reserve0 : reserve1) / 4; + // token0.approve(ETHEREX_ROUTER, amountIn); + + // uint256 timeElapsed = 0; + + // console.log( + // "Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // twap.getAssetPrice(address(token1)), + // twap.getSpotPrice(address(token1)) + // ); + + // IRouter.route[] memory swapRoute = new IRouter.route[](1); + // swapRoute[0] = IRouter.route({from: address(token0), to: address(token1), stable: false}); + // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + // amountIn, 0, swapRoute, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // etherexPair.sync(); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + // console.log( + // "Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // twap.getAssetPrice(address(token1)), + // twap.getSpotPrice(address(token1)) + // ); + // } + + // // perform a large swap + // deal(address(token1), manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = etherexPair.getReserves(); + // amountIn = (address(token1) == etherexPair.token0() ? reserve0 : reserve1) / 4; + // token1.approve(ETHEREX_ROUTER, amountIn); + + // swapRoute[0] = IRouter.route({from: address(token1), to: address(token0), stable: false}); + // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + // amountIn, 0, swapRoute, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // timeElapsed += granuality; + // etherexPair.sync(); + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + // console.log( + // "Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // twap.getAssetPrice(address(token1)), + // twap.getSpotPrice(address(token1)) + // ); + // } + + // // perform a large swap + // manipulator = makeAddr("manipulator"); + // deal(address(token0), manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = etherexPair.getReserves(); + // amountIn = (address(token0) == etherexPair.token0() ? reserve0 : reserve1) / 10; + // token0.approve(ETHEREX_ROUTER, amountIn); + + // swapRoute[0] = IRouter.route({from: address(token0), to: address(token1), stable: false}); + // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + // amountIn, 0, swapRoute, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // etherexPair.sync(); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + // console.log( + // "Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // twap.getAssetPrice(address(token1)), + // twap.getSpotPrice(address(token1)) + // ); + // } + + // // perform a large swap + // deal(address(token1), manipulator, 2 ** 128); + // vm.startPrank(manipulator); + // (reserve0, reserve1,) = etherexPair.getReserves(); + // amountIn = (address(token1) == etherexPair.token0() ? reserve0 : reserve1) / 4; + // token1.approve(ETHEREX_ROUTER, amountIn); + + // swapRoute[0] = IRouter.route({from: address(token1), to: address(token0), stable: false}); + // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + // amountIn, 0, swapRoute, manipulator, type(uint32).max + // ); + // vm.stopPrank(); + + // // wait + // for (uint256 idx = 0; idx < period; idx++) { + // skip(granuality); + // etherexPair.sync(); + // timeElapsed += granuality; + // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + // console.log( + // "Time: %s, Twap1: %s, Spot: %s", + // timeElapsed / 1 minutes, + // twap.getAssetPrice(address(token1)), + // twap.getSpotPrice(address(token1)) + // ); + // } + // } } From 46d517d32ee3df35d878d08c2967623427d1d947 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Thu, 25 Sep 2025 09:21:23 +0200 Subject: [PATCH 10/13] Cleaning and testing --- contracts/interfaces/ITwapOracle.sol | 7 - contracts/protocol/core/twaps/EtherexTwap.sol | 45 +- .../core/twaps/EtherexVolatileTwap.sol | 143 ----- .../core/twaps/EtherexVolatileTwapOld.sol | 178 ------ oracleAnalysis.py | 2 +- tests/foundry/TwapLinea.t.sol | 541 +++++++----------- tests/foundry/TwapOracle.t.sol | 68 --- 7 files changed, 263 insertions(+), 721 deletions(-) delete mode 100644 contracts/interfaces/ITwapOracle.sol delete mode 100644 contracts/protocol/core/twaps/EtherexVolatileTwap.sol delete mode 100644 contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol delete mode 100644 tests/foundry/TwapOracle.t.sol diff --git a/contracts/interfaces/ITwapOracle.sol b/contracts/interfaces/ITwapOracle.sol deleted file mode 100644 index 33b99fc..0000000 --- a/contracts/interfaces/ITwapOracle.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.13; - -interface ITwapOracle { - function getAssetPrice(address _asset) external view returns (uint256); - function getTokens() external view returns (address token0, address token1); -} diff --git a/contracts/protocol/core/twaps/EtherexTwap.sol b/contracts/protocol/core/twaps/EtherexTwap.sol index 141c99e..d9f79cb 100644 --- a/contracts/protocol/core/twaps/EtherexTwap.sol +++ b/contracts/protocol/core/twaps/EtherexTwap.sol @@ -22,6 +22,7 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { error EtherexTwap_InvalidParams(); error EtherexTwap__StablePairsUnsupported(); error EtherexTwap__ServiceNotAvailable(); + error EtherexTwap_WrongPriceFeedDecimals(); /* Events */ event SetParams(uint128 minPrice); @@ -46,13 +47,18 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { */ uint128 public minPrice; + /** + * @notice Token for which the price is given + */ address public token; - /// @notice Whether the price should be returned in terms of token0. - /// If false, the price is returned in terms of token1. - bool isToken0; + /** + * @notice Whether the price should be returned in terms of token0. + * If false, the price is returned in terms of token1. + */ + bool public isToken0; - IChainlinkAggregator priceFeed; + IChainlinkAggregator public priceFeed; constructor( IEtherexPair _etherexPair, @@ -70,12 +76,15 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { if (_timeWindow < MIN_TIME_WINDOW) { revert EtherexTwap__InvalidWindow(); } + if (_etherexPair.stable()) revert EtherexTwap__StablePairsUnsupported(); if ( ERC20(_etherexPair.token0()).decimals() != 18 || ERC20(_etherexPair.token1()).decimals() != 18 ) revert EtherexTwap_InvalidParams(); - if (_etherexPair.stable()) revert EtherexTwap__StablePairsUnsupported(); + if (IChainlinkAggregator(_priceFeed).decimals() != 8) { + revert EtherexTwap_WrongPriceFeedDecimals(); + } /* Assignment */ timeWindow = _timeWindow; etherexPair = _etherexPair; @@ -174,14 +183,30 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { } /* --- Additional getters --- */ + /** + * @notice Returns the underlying token addresses of the Etherex pair. + * @dev Fetches token0 and token1 from the EtherexPair contract. + * @return token0 The address of token0. + * @return token1 The address of token1. + */ function getTokens() external view returns (address token0, address token1) { return (etherexPair.token0(), etherexPair.token1()); } + /** + * @notice Returns the address of the associated Etherex pair contract. + * @dev This is the actual pair contract address used for pricing and reserves. + * @return The address of the EtherexPair contract. + */ function getPairAddress() external view returns (address) { return address(etherexPair); } + /** + * @notice Gets the current spot price from the Etherex pair in 8 decimals precision. + * @dev The price is scaled by the Chainlink price feed and normalized to 8 decimals. + * @return Spot price as an int256 value (8 decimals). + */ function getSpotPrice() external view returns (int256) { uint256 spotPrice = _getSpotPrice(); int256 answer = priceFeed.latestAnswer(); @@ -191,6 +216,10 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { /* --- Private --- */ + /** + * @notice Internal spot price calculation from current reserves. + * @return price The raw spot price (scaled by WAD). + */ function _getSpotPrice() private view returns (uint256 price) { (uint112 _reserve0, uint112 _reserve1,) = etherexPair.getReserves(); if (!isToken0) { @@ -200,6 +229,12 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { } } + /** + * @notice Calculates the Time-Weighted Average Price (TWAP) for the configured time window. + * @dev Fetches observations, handles edge case if timeElapsed is insufficient, + * and calculates the twap price with chain-specific decimals. + * @return TWAP value as a uint256 (scaled by WAD). + */ function _getTwapPrice() private view returns (uint256) { IEtherexPair.Observation memory _observation = etherexPair.lastObservation(); (uint256 reserve0Cumulative, uint256 reserve1Cumulative,) = diff --git a/contracts/protocol/core/twaps/EtherexVolatileTwap.sol b/contracts/protocol/core/twaps/EtherexVolatileTwap.sol deleted file mode 100644 index afae41e..0000000 --- a/contracts/protocol/core/twaps/EtherexVolatileTwap.sol +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.13; - -import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; -import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; -import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; -import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; - -import "forge-std/console2.sol"; - -/// @title Oracle using Thena TWAP oracle as data source -/// @author xRave110 -/// @notice The oracle contract that provides the current price to purchase -/// the underlying token while exercising options. Uses Thena TWAP oracle -/// as data source, and then applies a lower bound. -contract EtherexVolatileTwap is ITwapOracle, Ownable { - /// ----------------------------------------------------------------------- - /// Library usage - /// ----------------------------------------------------------------------- - - using FixedPointMathLib for uint256; - - /// ----------------------------------------------------------------------- - /// Errors - /// ----------------------------------------------------------------------- - - error ThenaOracle__InvalidParams(); - error ThenaOracle__InvalidWindow(); - error ThenaOracle__StablePairsUnsupported(); - error ThenaOracle__Overflow(); - error ThenaOracle__BelowMinPrice(); - - /// ----------------------------------------------------------------------- - /// Events - /// ----------------------------------------------------------------------- - - event SetParams(uint128 maxPrice, uint128 minPrice); - - /// ----------------------------------------------------------------------- - /// Immutable parameters - /// ----------------------------------------------------------------------- - uint256 internal constant WAD = 1e18; - uint256 internal constant MIN_SECS = 20 minutes; - - /// @notice The Thena TWAP oracle contract (usually a pool with oracle support) - IEtherexPair public immutable etherexPair; - - /// ----------------------------------------------------------------------- - /// Storage variables - /// ----------------------------------------------------------------------- - - /// @notice The size of the window to take the TWAP value over in seconds. - uint56 public timeWindow; - - /// @notice The minimum value returned by getPrice(). Maintains a floor for the - /// price to mitigate potential attacks on the TWAP oracle. - uint128 public minPrice; - uint128 public maxPrice; - - /// ----------------------------------------------------------------------- - /// Constructor - /// ----------------------------------------------------------------------- - - constructor(IEtherexPair _etherexPair, address _owner, uint56 _timeWindow, uint128 _minPrice) - Ownable(_owner) - { - if (_timeWindow < MIN_SECS) revert ThenaOracle__InvalidWindow(); - timeWindow = _timeWindow; - etherexPair = _etherexPair; - minPrice = _minPrice; - - emit SetParams(_timeWindow, _minPrice); - } - - /// ----------------------------------------------------------------------- - /// IOracle - /// ----------------------------------------------------------------------- - - /// @inheritdoc ITwapOracle - function getAssetPrice(address _asset) external view override returns (uint256 price) { - price = etherexPair.current(_asset, 10 ** ERC20(_asset).decimals()); - - if (price < minPrice) revert ThenaOracle__BelowMinPrice(); - } - - /* add only assets in the pool ! */ - function getAssetPriceWithQuote(address _asset) external view returns (uint256 price) { - uint256 granuality = 1; - uint256 _timeWindow = timeWindow; - uint256 timeElapsed = 0; - uint256 length = etherexPair.observationLength(); - for (; timeElapsed < _timeWindow; granuality++) { - timeElapsed = block.timestamp - etherexPair.observations(length - granuality).timestamp; - console2.log("Time elapsed: %s vs timeWindow %s", timeElapsed, timeWindow); - } - - console2.log("Granuality: ", granuality); - price = etherexPair.quote(_asset, 10 ** ERC20(_asset).decimals(), granuality); - - if (price < minPrice) revert ThenaOracle__BelowMinPrice(); - } - - function getAssetPriceWithSampleWindow(address _asset) external view returns (uint256 price) { - uint256 granuality = 1; - uint256 _timeWindow = timeWindow; - uint256 timeElapsed = 0; - uint256 length = etherexPair.observationLength(); - for (; timeElapsed < _timeWindow; granuality++) { - timeElapsed = block.timestamp - etherexPair.observations(length - granuality).timestamp; - console2.log("Time elapsed: %s vs timeWindow %s", timeElapsed, timeWindow); - } - - console2.log("Granuality: ", granuality); - price = etherexPair.sample(_asset, 10 ** ERC20(_asset).decimals(), 1, granuality)[0]; - - if (price < minPrice) revert ThenaOracle__BelowMinPrice(); - } - - /// @inheritdoc ITwapOracle - function getTokens() external view override returns (address token0, address token1) { - return (etherexPair.token0(), etherexPair.token1()); - } - - /// ----------------------------------------------------------------------- - /// Owner functions - /// ----------------------------------------------------------------------- - - /// @notice Updates the oracle parameters. Only callable by the owner. - /// @param _maxPrice The maximum value returned by getAssetPrice(). - /// @param _minPrice The minimum value returned by getAssetPrice(). Maintains a floor for the - /// price to mitigate potential attacks on the TWAP oracle. - function setMinMaxPrice(uint128 _maxPrice, uint128 _minPrice) external onlyOwner { - maxPrice = _maxPrice; - minPrice = _minPrice; - emit SetParams(_maxPrice, _minPrice); - } - - function settimeWindow(uint56 _timeWindow) external onlyOwner { - if (_timeWindow < MIN_SECS) revert ThenaOracle__InvalidWindow(); - timeWindow = _timeWindow; - } -} diff --git a/contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol b/contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol deleted file mode 100644 index db243f3..0000000 --- a/contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.13; - -import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; -import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; -import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; -import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; - -/// @title Oracle using Thena TWAP oracle as data source -/// @author zefram.eth/lookee/Eidolon -/// @notice The oracle contract that provides the current price to purchase -/// the underlying token while exercising options. Uses Thena TWAP oracle -/// as data source, and then applies a lower bound. -contract EtherexVolatileTwapOld is ITwapOracle, Ownable { - /// ----------------------------------------------------------------------- - /// Library usage - /// ----------------------------------------------------------------------- - - using FixedPointMathLib for uint256; - - /// ----------------------------------------------------------------------- - /// Errors - /// ----------------------------------------------------------------------- - - error ThenaOracle__InvalidParams(); - error ThenaOracle__InvalidWindow(); - error ThenaOracle__StablePairsUnsupported(); - error ThenaOracle__Overflow(); - error ThenaOracle__BelowMinPrice(); - - /// ----------------------------------------------------------------------- - /// Events - /// ----------------------------------------------------------------------- - - event SetParams(uint56 secs, uint128 minPrice); - - /// ----------------------------------------------------------------------- - /// Immutable parameters - /// ----------------------------------------------------------------------- - uint256 internal constant WAD = 1e18; - uint256 internal constant MIN_SECS = 20 minutes; - - /// @notice The Thena TWAP oracle contract (usually a pool with oracle support) - IEtherexPair public immutable etherexPair; - - /// ----------------------------------------------------------------------- - /// Storage variables - /// ----------------------------------------------------------------------- - - /// @notice The size of the window to take the TWAP value over in seconds. - uint56 public secs; - - /// @notice The minimum value returned by getPrice(). Maintains a floor for the - /// price to mitigate potential attacks on the TWAP oracle. - uint128 public minPrice; - - /// @notice Whether the price should be returned in terms of token0. - /// If false, the price is returned in terms of token1. - bool public isToken0; - - /// ----------------------------------------------------------------------- - /// Constructor - /// ----------------------------------------------------------------------- - - constructor( - IEtherexPair etherexPair_, - address token, - address owner_, - uint56 secs_, - uint128 minPrice_ - ) Ownable(owner_) { - if ( - ERC20(etherexPair_.token0()).decimals() != 18 - || ERC20(etherexPair_.token1()).decimals() != 18 - ) revert ThenaOracle__InvalidParams(); - if (etherexPair_.stable()) revert ThenaOracle__StablePairsUnsupported(); - if (etherexPair_.token0() != token && etherexPair_.token1() != token) { - revert ThenaOracle__InvalidParams(); - } - if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); - - etherexPair = etherexPair_; - isToken0 = etherexPair_.token0() == token; - secs = secs_; - minPrice = minPrice_; - - emit SetParams(secs_, minPrice_); - } - - /// ----------------------------------------------------------------------- - /// IOracle - /// ----------------------------------------------------------------------- - - /// @inheritdoc ITwapOracle - function getAssetPrice(address _asset) external view override returns (uint256 price) { - if ( - (isToken0 && _asset != etherexPair.token0()) - && (!isToken0 && _asset != etherexPair.token1()) - ) { - revert ThenaOracle__InvalidParams(); - } - /// ----------------------------------------------------------------------- - /// Storage loads - /// ----------------------------------------------------------------------- - - uint256 secs_ = secs; - - /// ----------------------------------------------------------------------- - /// Computation - /// ----------------------------------------------------------------------- - - // query Thena oracle to get TWAP value - { - ( - uint256 reserve0CumulativeCurrent, - uint256 reserve1CumulativeCurrent, - uint256 blockTimestampCurrent - ) = etherexPair.currentCumulativePrices(); - uint256 observationLength = etherexPair.observationLength(); - IEtherexPair.Observation memory lastObs = etherexPair.lastObservation(); - - uint32 T = uint32(blockTimestampCurrent - lastObs.timestamp); - if (T < secs_) { - lastObs = etherexPair.observations(observationLength - 2); - T = uint32(blockTimestampCurrent - lastObs.timestamp); - } - uint112 reserve0 = safe112((reserve0CumulativeCurrent - lastObs.reserve0Cumulative) / T); - uint112 reserve1 = safe112((reserve1CumulativeCurrent - lastObs.reserve1Cumulative) / T); - - if (!isToken0) { - price = uint256(reserve0) * WAD / (reserve1); - } else { - price = uint256(reserve1) * WAD / (reserve0); - } - } - - if (price < minPrice) revert ThenaOracle__BelowMinPrice(); - } - - /// @inheritdoc ITwapOracle - function getTokens() - external - view - override - returns (address paymentToken, address underlyingToken) - { - if (isToken0) { - return (etherexPair.token1(), etherexPair.token0()); - } else { - return (etherexPair.token0(), etherexPair.token1()); - } - } - - /// ----------------------------------------------------------------------- - /// Owner functions - /// ----------------------------------------------------------------------- - - /// @notice Updates the oracle parameters. Only callable by the owner. - /// @param secs_ The size of the window to take the TWAP value over in seconds. - /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the - /// price to mitigate potential attacks on the TWAP oracle. - function setParams(uint56 secs_, uint128 minPrice_) external onlyOwner { - if (secs_ < MIN_SECS) revert ThenaOracle__InvalidWindow(); - secs = secs_; - minPrice = minPrice_; - emit SetParams(secs_, minPrice_); - } - - /// ----------------------------------------------------------------------- - /// Util functions - /// ----------------------------------------------------------------------- - - function safe112(uint256 n) internal pure returns (uint112) { - if (n >= 2 ** 112) revert ThenaOracle__Overflow(); - return uint112(n); - } -} diff --git a/oracleAnalysis.py b/oracleAnalysis.py index 4c5eda9..dc6f8f2 100644 --- a/oracleAnalysis.py +++ b/oracleAnalysis.py @@ -32,7 +32,7 @@ def read_text_file(file_path): return x_time, y_twap, y_spot # Example usage -file_path = "./Data120MinEtherex1.txt" +file_path = "./REX_USD_5Min.txt" x_time, y_twap, y_spot = read_text_file(file_path) diff --git a/tests/foundry/TwapLinea.t.sol b/tests/foundry/TwapLinea.t.sol index c661789..f33527a 100644 --- a/tests/foundry/TwapLinea.t.sol +++ b/tests/foundry/TwapLinea.t.sol @@ -5,10 +5,7 @@ import "forge-std/Test.sol"; import {IEtherexPair} from "contracts/interfaces/IEtherexPair.sol"; import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {EtherexTwap} from "contracts/protocol/core/twaps/EtherexTwap.sol"; -import {EtherexVolatileTwap} from "contracts/protocol/core/twaps/EtherexVolatileTwap.sol"; -import {EtherexVolatileTwapOld} from "contracts/protocol/core/twaps/EtherexVolatileTwapOld.sol"; import {IRouter} from "contracts/interfaces/IRouter.sol"; -import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; import {IOracle} from "contracts/interfaces/IOracle.sol"; import {console2} from "forge-std/console2.sol"; @@ -25,8 +22,6 @@ contract TwapLineaTest is Test { uint256 constant LOG_WINDOW = 7 days; uint256 constant MIN_PRICE = 0; address WSTETH_USDC_PRICE_FEED = 0x8eCE1AbA32716FdDe8D6482bfd88E9a0ee01f565; - EtherexVolatileTwap asUsdEtherexVolatileTwap; - EtherexVolatileTwap rex33EtherexVolatileTwap; EtherexTwap wstEthRexTwap; bool constant USE_QUOTE = false; @@ -39,13 +34,6 @@ contract TwapLineaTest is Test { "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d", 23687274 ); assertEq(vm.activeFork(), opFork); - asUsdEtherexVolatileTwap = new EtherexVolatileTwap( - ASUSD_USDC_PAIR, address(this), uint56(TIME_WINDOW), uint128(MIN_PRICE) - ); - - rex33EtherexVolatileTwap = new EtherexVolatileTwap( - REX33_USDC_PAIR, address(this), uint56(TIME_WINDOW), uint128(MIN_PRICE) - ); wstEthRexTwap = new EtherexTwap( REX_WSTETH_PAIR, @@ -65,34 +53,34 @@ contract TwapLineaTest is Test { oracle.setAssetSources(assets, sources, timeouts); } - function testAssetPriceAfterSwaps() public { - console2.log( - "The USDC price current: ", asUsdEtherexVolatileTwap.getAssetPrice(address(ASUSD)) - ); - console2.log( - "The USDC price quote: ", - asUsdEtherexVolatileTwap.getAssetPriceWithSample(address(ASUSD)) - ); - console2.log( - "The USDC price sampleWindow: ", - asUsdEtherexVolatileTwap.getAssetPriceOriginal(address(ASUSD)) - ); + function testCompabilityWithOracle() public { + address manipulator = makeAddr("manipulator"); + deal(address(REX), manipulator, 1000000 ether); - console2.log( - "The REX33 price current: ", rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - ); - console2.log( - "The REX33 price quote: ", - rex33EtherexVolatileTwap.getAssetPriceWithSample(address(REX33)) - ); - console2.log( - "The REX33 price sampleWindow: ", - rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)) - ); - } + // register initial oracle price + uint256 price_1 = oracle.getAssetPrice(address(REX)); + (, int256 price_2,,,) = wstEthRexTwap.latestRoundData(); - function testCompabilityWithOracle() public { - console.log(oracle.getAssetPrice(address(REX))); + // perform a large swap + vm.startPrank(manipulator); + REX.approve(ETHEREX_ROUTER, 1000000 ether); + + IRouter.route[] memory swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX), to: address(WSTETH), stable: false}); + (uint256 reserve0, uint256 reserve1,) = wstEthRexTwap.etherexPair().getReserves(); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + (address(REX) == wstEthRexTwap.etherexPair().token0() ? reserve0 : reserve1) / 10, + 0, + swapRoute, + manipulator, + type(uint32).max + ); + vm.stopPrank(); + (, int256 tmpAnswer,,,) = wstEthRexTwap.latestRoundData(); + // price should not have changed + assertEq(oracle.getAssetPrice(address(REX)), price_1, "single block price variation"); + assertEq(tmpAnswer, price_2, "single block price variation"); + assertEq(price_1, uint256(price_2)); } function test_singleBlockManipulation() public { @@ -119,6 +107,7 @@ contract TwapLineaTest is Test { ); vm.stopPrank(); (, int256 tmpAnswer,,,) = wstEthRexTwap.latestRoundData(); + // price should not have changed assertEq(wstEthRexTwap.latestAnswer(), price_1, "single block price variation"); assertEq(tmpAnswer, price_2, "single block price variation"); @@ -130,9 +119,9 @@ contract TwapLineaTest is Test { // clean twap for test skip(1 hours); - REX33_USDC_PAIR.sync(); + wstEthRexTwap.etherexPair().sync(); skip(1 hours); - REX33_USDC_PAIR.sync(); + wstEthRexTwap.etherexPair().sync(); skip(1 hours); int256 price_1 = wstEthRexTwap.latestAnswer(); @@ -140,6 +129,7 @@ contract TwapLineaTest is Test { // perform a large swap address manipulator = makeAddr("manipulator"); + deal(address(REX), manipulator, 1000000 ether); vm.startPrank(manipulator); REX.approve(ETHEREX_ROUTER, 1000000 ether); @@ -155,20 +145,122 @@ contract TwapLineaTest is Test { ); vm.stopPrank(); - // wait - skip(skipTime); + skip(5 minutes); (, int256 tmpAnswer,,,) = wstEthRexTwap.latestRoundData(); + int256 spotPrice = wstEthRexTwap.getSpotPrice(); // price should not have changed - assertGt(wstEthRexTwap.latestAnswer(), price_1, "single block price variation"); - assertGt(tmpAnswer, price_2, "single block price variation"); - assertEq(price_1, price_2); - // assert(false); + assertLt(wstEthRexTwap.latestAnswer(), price_1, "Price not less after REX sell"); + assertLt(tmpAnswer, price_2, "Price not less after REX sell"); + assertEq(price_1, price_2, "Prices are not equal"); + assertEq(wstEthRexTwap.latestAnswer(), tmpAnswer, "Prices are not equal"); + assertLt(spotPrice, tmpAnswer, "Twap is not delayed"); + + // wait + skip(skipTime); + wstEthRexTwap.etherexPair().sync(); + + (, price_2,,,) = wstEthRexTwap.latestRoundData(); + assertLt(price_2, tmpAnswer, "Price is not lower after some time"); + } + + function test_priceManipulationChart(uint256 skipTime) public { + skipTime = bound(skipTime, 20 minutes, 70 minutes); + + // clean twap for test + skip(1 hours); + wstEthRexTwap.etherexPair().sync(); + skip(1 hours); + wstEthRexTwap.etherexPair().sync(); + skip(1 hours); + + uint256 timeElapsed = 0; + uint256 granuality = 5 minutes; + uint256 period = 2 * TIME_WINDOW / granuality; + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + wstEthRexTwap.etherexPair().sync(); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(wstEthRexTwap.latestAnswer()), + uint256(wstEthRexTwap.getSpotPrice()) + ); + } + + // perform a large swap + address manipulator = makeAddr("manipulator"); + deal(address(REX), manipulator, 1000000 ether); + vm.startPrank(manipulator); + REX.approve(ETHEREX_ROUTER, 1000000 ether); + + IRouter.route[] memory swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(REX), to: address(WSTETH), stable: false}); + (uint256 reserve0, uint256 reserve1,) = wstEthRexTwap.etherexPair().getReserves(); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + (address(REX) == wstEthRexTwap.etherexPair().token0() ? reserve0 : reserve1) / 10, + 0, + swapRoute, + manipulator, + type(uint32).max + ); + vm.stopPrank(); + + for (uint256 idx = 0; idx < 2; idx++) { + skip(granuality); + wstEthRexTwap.etherexPair().sync(); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(wstEthRexTwap.latestAnswer()), + uint256(wstEthRexTwap.getSpotPrice()) + ); + } + + // perform a large swap + deal(address(WSTETH), manipulator, 1000000 ether); + vm.startPrank(manipulator); + WSTETH.approve(ETHEREX_ROUTER, 1000000 ether); + + swapRoute = new IRouter.route[](1); + swapRoute[0] = IRouter.route({from: address(WSTETH), to: address(REX), stable: false}); + (reserve0, reserve1,) = wstEthRexTwap.etherexPair().getReserves(); + IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( + (address(WSTETH) == wstEthRexTwap.etherexPair().token0() ? reserve0 : reserve1) / 10, + 0, + swapRoute, + manipulator, + type(uint32).max + ); + vm.stopPrank(); + + // wait + + for (uint256 idx = 0; idx < period; idx++) { + skip(granuality); + wstEthRexTwap.etherexPair().sync(); + timeElapsed += granuality; + //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); + + console.log( + "Time: %s, Twap1: %s, Spot: %s", + timeElapsed / 1 minutes, + uint256(wstEthRexTwap.latestAnswer()), + uint256(wstEthRexTwap.getSpotPrice()) + ); + } + // assert(false); - for chart with -vv } function test_EtherexTwapMultiplePriceManipulationsAll18Decimals() public { multiplePriceManipulationWithLoopChainlink(5 minutes, wstEthRexTwap); - assert(false); + // assert(false); - for chart with -vv } function multiplePriceManipulationWithLoopChainlink(uint256 granuality, EtherexTwap twap) @@ -313,285 +405,96 @@ contract TwapLineaTest is Test { } } - // function test_EtherexTwapMultiplePriceManipulations6Decimals() public { - // address twap = address( - // new EtherexTwap(address(REX33_USDC_PAIR), address(this), uint56(TIME_WINDOW), 0) - // ); - // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); - // assert(false); - // } - - // function test_EtherexTwapMultiplePriceManipulationsOld() public { - // address twap = address( - // new EtherexVolatileTwapOld( - // REX_WSTETH_PAIR, address(WSTETH), address(this), uint56(TIME_WINDOW), 0 - // ) - // ); - // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); - // assert(false); - // } - - // function test_EtherexTwapMultiplePriceManipulationsExperiment() public { - // address twap = - // address(new EtherexVolatileTwap(REX_WSTETH_PAIR, address(this), uint56(TIME_WINDOW), 0)); - // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); - // assert(false); - // } - - // function test_PriceManipulationWithLoop() public { - // // string memory path = "oracleSim.txt"; - - // uint256 granuality = 5 minutes; - // uint256 period = (2 * TIME_WINDOW) / granuality; - // uint256 skipTime = 1 hours; - - // // clean twap for test - // skip(skipTime); - // rex33EtherexVolatileTwap.etherexPair().sync(); - // skip(skipTime); - // rex33EtherexVolatileTwap.etherexPair().sync(); - // skip(skipTime); - - // // register initial oracle price - - // uint256 price_1 = rex33EtherexVolatileTwap.getAssetPrice(address(REX33)); - // uint256 price_2 = rex33EtherexVolatileTwap.getAssetPriceWithSample(address(REX33)); - // uint256 price_3 = rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)); - // console.log("1.Initial price after stabilization: %s", price_1); - // console.log("2.Initial price after stabilization: %s", price_2); - // console.log("3.Initial price after stabilization: %s", price_3); - - // uint256 timeElapsed = 0; - // if (USE_QUOTE) { - // console.log( - // "0.Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), - // rex33EtherexVolatileTwap.getSpotPrice(address(REX33)) - // ); - // } else { - // console2.log( - // "0.Time: %s, Twap2: %s, Spot: %s", - // timeElapsed / 1 minutes, - // rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)), - // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - // ); - // } - - // // perform a large swap - // address manipulator = makeAddr("manipulator"); - // deal(address(REX33), manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (uint256 reserve0, uint256 reserve1,) = REX33_USDC_PAIR.getReserves(); - // uint256 amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 4; - // REX33.approve(ETHEREX_ROUTER, amountIn); - - // IRouter.route[] memory swapRoute = new IRouter.route[](1); - // swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); - // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - // amountIn, 0, swapRoute, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // rex33EtherexVolatileTwap.etherexPair().sync(); - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // if (USE_QUOTE) { - // console.log( - // "1. Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), - // rex33EtherexVolatileTwap.getSpotPrice(address(REX33)) - // ); - // } else { - // console2.log( - // "1.Time: %s, Twap2: %s, Spot: %s", - // timeElapsed / 1 minutes, - // rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)), - // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - // ); - // } - - // // vm.writeFile(path, data); - // } - // deal(address(REX33), manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = REX33_USDC_PAIR.getReserves(); - // amountIn = (address(REX33) == REX33_USDC_PAIR.token0() ? reserve0 : reserve1) / 40; - // REX33.approve(ETHEREX_ROUTER, amountIn); - - // swapRoute = new IRouter.route[](1); - // swapRoute[0] = IRouter.route({from: address(REX33), to: address(USDC), stable: false}); - // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - // amountIn, 0, swapRoute, manipulator, type(uint32).max - // ); - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // rex33EtherexVolatileTwap.etherexPair().sync(); - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - // if (USE_QUOTE) { - // console.log( - // "2. Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)), - // rex33EtherexVolatileTwap.getSpotPrice(address(REX33)) - // ); - // } else { - // console2.log( - // "2. Time: %s, Twap2: %s, Spot: %s", - // timeElapsed / 1 minutes, - // rex33EtherexVolatileTwap.getAssetPriceOriginal(address(REX33)), - // rex33EtherexVolatileTwap.getAssetPrice(address(REX33)) - // ); - // } - - // // vm.writeFile(path, data); - // } - // assert(false); - // } - - // function multiplePriceManipulationWithLoop(uint256 granuality, ITwapOracle twap) internal { - // uint256 period = 2 * TIME_WINDOW / granuality; + function testRevertsOnInvalidAddress() public { + // etherexPair == 0 + vm.expectRevert(EtherexTwap.EtherexTwap__InvalidAddress.selector); + EtherexTwap newTwap = new EtherexTwap( + IEtherexPair(address(0)), + address(this), + uint56(TIME_WINDOW), + 0, + WSTETH_USDC_PRICE_FEED, + address(REX) + ); + // priceFeed == 0 + vm.expectRevert(EtherexTwap.EtherexTwap__InvalidAddress.selector); + newTwap = new EtherexTwap( + IEtherexPair(REX_WSTETH_PAIR), + address(this), + uint56(TIME_WINDOW), + 0, + address(0), + address(REX) + ); + // token == 0 + vm.expectRevert(EtherexTwap.EtherexTwap__InvalidAddress.selector); + newTwap = new EtherexTwap( + IEtherexPair(REX_WSTETH_PAIR), + address(this), + uint56(TIME_WINDOW), + 0, + address(0), + address(0) + ); + } - // IEtherexPair etherexPair = IEtherexPair(twap.getPairAddress()); + function testTimeWindowChecks() public { + vm.expectRevert(EtherexTwap.EtherexTwap__InvalidWindow.selector); + new EtherexTwap( + IEtherexPair(REX_WSTETH_PAIR), + address(this), + 10 minutes, + 0, + WSTETH_USDC_PRICE_FEED, + address(REX) + ); + vm.expectRevert(EtherexTwap.EtherexTwap__InvalidWindow.selector); + wstEthRexTwap.setTimeWindow(10 minutes); - // // clean twap for test - // skip(1 hours); - // etherexPair.sync(); - // skip(1 hours); - // etherexPair.sync(); - // skip(1 hours); + wstEthRexTwap.setTimeWindow(55 minutes); - // ERC20 token0 = ERC20(etherexPair.token0()); - // ERC20 token1 = ERC20(etherexPair.token1()); + assertEq(wstEthRexTwap.timeWindow(), 55 minutes); + } - // // perform a large swap - // address manipulator = makeAddr("manipulator"); - // deal(address(token0), manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (uint256 reserve0, uint256 reserve1,) = etherexPair.getReserves(); - // uint256 amountIn = (address(token0) == etherexPair.token0() ? reserve0 : reserve1) / 4; - // token0.approve(ETHEREX_ROUTER, amountIn); + function testRevertsOnInvalidDecimals() public { + vm.expectRevert(EtherexTwap.EtherexTwap_InvalidParams.selector); + new EtherexTwap( + IEtherexPair(REX33_USDC_PAIR), // wrong decimals + address(this), + uint56(TIME_WINDOW), + 0, + WSTETH_USDC_PRICE_FEED, + address(REX) + ); - // uint256 timeElapsed = 0; + vm.expectRevert(EtherexTwap.EtherexTwap_WrongPriceFeedDecimals.selector); + new EtherexTwap( + IEtherexPair(REX_WSTETH_PAIR), // good decimals + address(this), + uint56(TIME_WINDOW), + 0, + 0x5C5Ee01b351b7ef0b16Cfd59E93F743E0679d7bC, // wrong decimals + address(REX) + ); + } - // console.log( - // "Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // twap.getAssetPrice(address(token1)), - // twap.getSpotPrice(address(token1)) - // ); + function testRevertsOnStablePair() public { + vm.expectRevert(EtherexTwap.EtherexTwap__StablePairsUnsupported.selector); + new EtherexTwap( + IEtherexPair(ASUSD_USDC_PAIR), + address(this), + uint56(TIME_WINDOW), + 0, + WSTETH_USDC_PRICE_FEED, + address(REX) + ); + } - // IRouter.route[] memory swapRoute = new IRouter.route[](1); - // swapRoute[0] = IRouter.route({from: address(token0), to: address(token1), stable: false}); - // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - // amountIn, 0, swapRoute, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // etherexPair.sync(); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - - // console.log( - // "Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // twap.getAssetPrice(address(token1)), - // twap.getSpotPrice(address(token1)) - // ); - // } - - // // perform a large swap - // deal(address(token1), manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = etherexPair.getReserves(); - // amountIn = (address(token1) == etherexPair.token0() ? reserve0 : reserve1) / 4; - // token1.approve(ETHEREX_ROUTER, amountIn); - - // swapRoute[0] = IRouter.route({from: address(token1), to: address(token0), stable: false}); - // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - // amountIn, 0, swapRoute, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // timeElapsed += granuality; - // etherexPair.sync(); - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - - // console.log( - // "Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // twap.getAssetPrice(address(token1)), - // twap.getSpotPrice(address(token1)) - // ); - // } - - // // perform a large swap - // manipulator = makeAddr("manipulator"); - // deal(address(token0), manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = etherexPair.getReserves(); - // amountIn = (address(token0) == etherexPair.token0() ? reserve0 : reserve1) / 10; - // token0.approve(ETHEREX_ROUTER, amountIn); - - // swapRoute[0] = IRouter.route({from: address(token0), to: address(token1), stable: false}); - // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - // amountIn, 0, swapRoute, manipulator, type(uint32).max - // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // etherexPair.sync(); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - - // console.log( - // "Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // twap.getAssetPrice(address(token1)), - // twap.getSpotPrice(address(token1)) - // ); - // } - - // // perform a large swap - // deal(address(token1), manipulator, 2 ** 128); - // vm.startPrank(manipulator); - // (reserve0, reserve1,) = etherexPair.getReserves(); - // amountIn = (address(token1) == etherexPair.token0() ? reserve0 : reserve1) / 4; - // token1.approve(ETHEREX_ROUTER, amountIn); - - // swapRoute[0] = IRouter.route({from: address(token1), to: address(token0), stable: false}); - // IRouter(ETHEREX_ROUTER).swapExactTokensForTokens( - // amountIn, 0, swapRoute, manipulator, type(uint32).max + // function test_EtherexTwapMultiplePriceManipulations6Decimals() public { + // address twap = address( + // new EtherexTwap(address(REX33_USDC_PAIR), address(this), uint56(TIME_WINDOW), 0) // ); - // vm.stopPrank(); - - // // wait - // for (uint256 idx = 0; idx < period; idx++) { - // skip(granuality); - // etherexPair.sync(); - // timeElapsed += granuality; - // //console.log("Price after %s min: %s vs spot: %s", timeElapsed / 1 minutes, oracle.getPrice(), getSpotPrice(_default.pair, _default.token)); - - // console.log( - // "Time: %s, Twap1: %s, Spot: %s", - // timeElapsed / 1 minutes, - // twap.getAssetPrice(address(token1)), - // twap.getSpotPrice(address(token1)) - // ); - // } + // multiplePriceManipulationWithLoop(5 minutes, ITwapOracle(twap)); + // assert(false); // } } diff --git a/tests/foundry/TwapOracle.t.sol b/tests/foundry/TwapOracle.t.sol deleted file mode 100644 index c83a3af..0000000 --- a/tests/foundry/TwapOracle.t.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "./Common.sol"; -import "contracts/protocol/libraries/helpers/Errors.sol"; -import {WadRayMath} from "contracts/protocol/libraries/math/WadRayMath.sol"; -import {EtherexVolatileTwap} from "contracts/protocol/core/twaps/EtherexVolatileTwap.sol"; - -contract TwapOracle is Common { - using WadRayMath for uint256; - - ERC20[] erc20Tokens; - DeployedContracts deployedContracts; - ConfigAddresses configAddresses; - - function setUp() public { - opFork = vm.createSelectFork(RPC, FORK_BLOCK); - assertEq(vm.activeFork(), opFork); - deployedContracts = fixture_deployProtocol(); - configAddresses = ConfigAddresses( - address(deployedContracts.asteraDataProvider), - address(deployedContracts.stableStrategy), - address(deployedContracts.volatileStrategy), - address(deployedContracts.treasury), - address(deployedContracts.rewarder), - address(deployedContracts.aTokensAndRatesHelper) - ); - fixture_configureProtocol( - address(deployedContracts.lendingPool), - address(commonContracts.aToken), - configAddresses, - deployedContracts.lendingPoolConfigurator, - deployedContracts.lendingPoolAddressesProvider - ); - commonContracts.mockedVaults = - fixture_deployReaperVaultMocks(tokens, address(deployedContracts.treasury)); - erc20Tokens = fixture_getErc20Tokens(tokens); - fixture_transferTokensToTestContract(erc20Tokens, 100_000 ether, address(this)); - } - - function testSetFallbackOracle() public { - ERC20[] memory erc20tokens = fixture_getErc20Tokens(tokens); - int256[] memory prices = new int256[](4); - uint256[] memory timeouts = new uint256[](4); - // All chainlink price feeds have 8 decimals - prices[0] = int256(95 * 10 ** PRICE_FEED_DECIMALS - 1); // USDC - prices[1] = int256(63_000 * 10 ** PRICE_FEED_DECIMALS); // WBTC - prices[2] = int256(3300 * 10 ** PRICE_FEED_DECIMALS); // ETH - prices[3] = int256(95 * 10 ** PRICE_FEED_DECIMALS - 1); // DAI - (, commonContracts.aggregators, timeouts) = fixture_getTokenPriceFeeds(erc20tokens, prices); - - // EtherexVolatileTwap etherexVolatileTwap = - // new EtherexVolatileTwap(address(tokens[0]), address(tokens[1]), 30 minutes, 1e18, true); - - Oracle oracle = new Oracle( - tokens, - commonContracts.aggregators, - timeouts, - address(0), - address(0), - BASE_CURRENCY_UNIT, - address(deployedContracts.lendingPoolAddressesProvider) - ); - - // commonContracts.oracle.setFallbackOracle(address(etherexVolatileTwap)); - // assertEq(address(etherexVolatileTwap), commonContracts.oracle.getFallbackOracle()); - } -} From 640b1043d0a2ce4d04c233480aa6e90048054a19 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Thu, 25 Sep 2025 09:30:11 +0200 Subject: [PATCH 11/13] Additional test and removed not used file --- .../protocol/core/twaps/EtherexClTwap.sol | 212 ------------------ tests/foundry/TwapLinea.t.sol | 22 ++ 2 files changed, 22 insertions(+), 212 deletions(-) delete mode 100644 contracts/protocol/core/twaps/EtherexClTwap.sol diff --git a/contracts/protocol/core/twaps/EtherexClTwap.sol b/contracts/protocol/core/twaps/EtherexClTwap.sol deleted file mode 100644 index 429c193..0000000 --- a/contracts/protocol/core/twaps/EtherexClTwap.sol +++ /dev/null @@ -1,212 +0,0 @@ -// // SPDX-License-Identifier: AGPL-3.0 -// pragma solidity ^0.8.13; - -// import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; -// import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol"; -// import {ITwapOracle} from "contracts/interfaces/ITwapOracle.sol"; -// import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -// import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; -// // import {TickMath} from "v3-core/libraries/TickMath.sol"; -// import {FullMath} from "v3-core/libraries/FullMath.sol"; - -// /// @title Oracle using Uniswap TWAP oracle as data source -// /// @author zefram.eth & lookeey -// /// @notice The oracle contract that provides the current price to purchase -// /// the underlying token while exercising options. Uses UniswapV3 TWAP oracle -// /// as data source, and then applies a multiplier & lower bound. -// contract EtherexClTwap is ITwapOracle, Ownable { -// /// ----------------------------------------------------------------------- -// /// Library usage -// /// ----------------------------------------------------------------------- - -// using FixedPointMathLib for uint256; - -// /// ----------------------------------------------------------------------- -// /// Errors -// /// ----------------------------------------------------------------------- - -// error UniswapOracle__InvalidParams(); -// error UniswapOracle__InvalidWindow(); -// error UniswapOracle__BelowMinPrice(); - -// /// ----------------------------------------------------------------------- -// /// Events -// /// ----------------------------------------------------------------------- - -// event SetParams(uint56 secs, uint56 ago, uint128 minPrice); - -// /// ----------------------------------------------------------------------- -// /// Immutable parameters -// /// ----------------------------------------------------------------------- - -// uint256 internal constant MIN_SECS = 20 minutes; - -// /// @notice The UniswapV3 Pool contract (provides the oracle) -// IUniswapV3Pool public immutable uniswapPool; - -// /// ----------------------------------------------------------------------- -// /// Storage variables -// /// ----------------------------------------------------------------------- - -// /// @notice The size of the window to take the TWAP value over in seconds. -// uint32 public secs; - -// /// @notice The number of seconds in the past to take the TWAP from. The window -// /// would be (block.timestamp - secs - ago, block.timestamp - ago]. -// uint32 public ago; - -// /// @notice The minimum value returned by getPrice(). Maintains a floor for the -// /// price to mitigate potential attacks on the TWAP oracle. -// uint128 public minPrice; - -// /// @notice Whether the price of token0 should be returned (in units of token1). -// /// If false, the price is returned in units of token0. -// bool public isToken0; - -// /// ----------------------------------------------------------------------- -// /// Constructor -// /// ----------------------------------------------------------------------- - -// constructor( -// IUniswapV3Pool uniswapPool_, -// address token, -// address owner_, -// uint32 secs_, -// uint32 ago_, -// uint128 minPrice_ -// ) Ownable(owner_) { -// if ( -// ERC20(uniswapPool_.token0()).decimals() != 18 -// || ERC20(uniswapPool_.token1()).decimals() != 18 -// ) revert UniswapOracle__InvalidParams(); //|| ERC20(uniswapPool_.token1()).decimals() != 18 -// if (uniswapPool_.token0() != token && uniswapPool_.token1() != token) { -// revert UniswapOracle__InvalidParams(); -// } -// if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); -// uniswapPool = uniswapPool_; -// isToken0 = token == uniswapPool_.token0(); -// secs = secs_; -// ago = ago_; -// minPrice = minPrice_; - -// emit SetParams(secs_, ago_, minPrice_); -// } - -// /// ----------------------------------------------------------------------- -// /// IOracle -// /// ----------------------------------------------------------------------- - -// /// @inheritdoc ITwapOracle -// function getAssetPrice(address _asset) external view override returns (uint256 price) { -// /// ----------------------------------------------------------------------- -// /// Validation -// /// ----------------------------------------------------------------------- - -// // The UniswapV3 pool reverts on invalid TWAP queries, so we don't need to - -// /// ----------------------------------------------------------------------- -// /// Computation -// /// ----------------------------------------------------------------------- - -// // query Uniswap oracle to get TWAP tick -// { -// uint32 _twapDuration = secs; -// uint32 _twapAgo = ago; -// uint32[] memory secondsAgo = new uint32[](2); -// secondsAgo[0] = _twapDuration + _twapAgo; -// secondsAgo[1] = _twapAgo; - -// (int56[] memory tickCumulatives,) = uniswapPool.observe(secondsAgo); -// int24 tick = -// int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int32(_twapDuration))); - -// uint256 decimalPrecision = 1e18; - -// // from https://optimistic.etherscan.io/address/0xB210CE856631EeEB767eFa666EC7C1C57738d438#code#F5#L49 -// uint160 sqrtRatioX96 = getSqrtRatioAtTick(tick); - -// // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself -// if (sqrtRatioX96 <= type(uint128).max) { -// uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; -// price = isToken0 -// ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) -// : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192); -// } else { -// uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); -// price = isToken0 -// ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) -// : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128); -// } -// } - -// // apply minimum price -// if (price < minPrice) revert UniswapOracle__BelowMinPrice(); -// } - -// /// @inheritdoc ITwapOracle -// function getTokens() -// external -// view -// override -// returns (address paymentToken, address underlyingToken) -// { -// if (isToken0) { -// return (uniswapPool.token1(), uniswapPool.token0()); -// } else { -// return (uniswapPool.token0(), uniswapPool.token1()); -// } -// } - -// /// ----------------------------------------------------------------------- -// /// Owner functions -// /// ----------------------------------------------------------------------- - -// /// @notice Updates the oracle parameters. Only callable by the owner. -// /// @param secs_ The size of the window to take the TWAP value over in seconds. -// /// @param ago_ The number of seconds in the past to take the TWAP from. The window -// /// would be (block.timestamp - secs - ago, block.timestamp - ago]. -// /// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the -// /// price to mitigate potential attacks on the TWAP oracle. -// function setParams(uint32 secs_, uint32 ago_, uint128 minPrice_) external onlyOwner { -// if (secs_ < MIN_SECS) revert UniswapOracle__InvalidWindow(); -// secs = secs_; -// ago = ago_; -// minPrice = minPrice_; -// emit SetParams(secs_, ago_, minPrice_); -// } - -// function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { -// uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); -// require(absTick <= uint256(887272), "T"); - -// uint256 ratio = absTick & 0x1 != 0 -// ? 0xfffcb933bd6fad37aa2d162d1a594001 -// : 0x100000000000000000000000000000000; -// if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; -// if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; -// if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; -// if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; -// if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; -// if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; -// if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; -// if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; -// if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; -// if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; -// if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; -// if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; -// if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; -// if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; -// if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; -// if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; -// if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; -// if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; -// if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; - -// if (tick > 0) ratio = type(uint256).max / ratio; - -// // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. -// // we then downcast because we know the result always fits within 160 bits due to our tick input constraint -// // we round up in the division so getTickAtSqrtRatio of the output price is always consistent -// sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); -// } -// } diff --git a/tests/foundry/TwapLinea.t.sol b/tests/foundry/TwapLinea.t.sol index f33527a..4cf3c91 100644 --- a/tests/foundry/TwapLinea.t.sol +++ b/tests/foundry/TwapLinea.t.sol @@ -83,6 +83,28 @@ contract TwapLineaTest is Test { assertEq(price_1, uint256(price_2)); } + function test_latestRoundData() public view { + ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) = wstEthRexTwap.latestRoundData(); + + console2.log("roundId:", roundId); + console2.log("answer:", answer); + console2.log("startedAt:", startedAt); + console2.log("updatedAt:", updatedAt); + console2.log("answeredInRound:", answeredInRound); + + assertNotEq(roundId, 0, "roundId is 0"); + assertGt(answer, 0, "answer is not greater than 0"); + assertNotEq(startedAt, 0, "startedAt is 0"); + assertLe(updatedAt, block.timestamp, "updatedAt greater than current timestamp"); + assertNotEq(answeredInRound, 0, "answeredInRound is 0"); + } + function test_singleBlockManipulation() public { address manipulator = makeAddr("manipulator"); deal(address(REX), manipulator, 1000000 ether); From 2166e63cc497b743624bb578b36ab8c27b7f04ab Mon Sep 17 00:00:00 2001 From: xRave110 Date: Thu, 25 Sep 2025 09:36:29 +0200 Subject: [PATCH 12/13] Cleaning --- foundry.toml | 2 +- tests/foundry/MiniPoolDeploymentHelper.t.sol | 162 ------------------- 2 files changed, 1 insertion(+), 163 deletions(-) delete mode 100644 tests/foundry/MiniPoolDeploymentHelper.t.sol diff --git a/foundry.toml b/foundry.toml index d705aa6..a4ad598 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,7 @@ ignored_warnings_from = ["tests/"] optimizer = true optimizer_runs = 500 solc_version = '0.8.23' -evm_version = "paris" # uncomment when problems with PUSH0 +#evm_version = "paris" # uncomment when problems with PUSH0 show_progress = true fs_permissions = [{ access = "read-write", path = "./"}] ignored_error_codes = [3628] diff --git a/tests/foundry/MiniPoolDeploymentHelper.t.sol b/tests/foundry/MiniPoolDeploymentHelper.t.sol deleted file mode 100644 index c917372..0000000 --- a/tests/foundry/MiniPoolDeploymentHelper.t.sol +++ /dev/null @@ -1,162 +0,0 @@ -// // SPDX-License-Identifier: BUSL-1.1 -// pragma solidity ^0.8.0; - -// import { -// MiniPoolDeploymentHelper, -// IMiniPoolConfigurator -// } from "contracts/deployments/MiniPoolDeploymentHelper.sol"; -// import {Test} from "forge-std/Test.sol"; -// import {console2} from "forge-std/console2.sol"; - -// // Tests all the functions in MiniPoolDeploymentHelper -// contract MiniPoolDeploymentHelperTest is Test { -// address constant ORACLE = 0xd971e9EC7357e9306c2a138E5c4eAfC04d241C87; -// address constant MINI_POOL_ADDRESS_PROVIDER = 0x9399aF805e673295610B17615C65b9d0cE1Ed306; -// address constant MINI_POOL_CONFIGURATOR = 0x41296B58279a81E20aF1c05D32b4f132b72b1B01; -// address constant DATA_PROVIDER = 0xE4FeC590F1Cf71B36c0A782Aac2E4589aFdaD88e; -// MiniPoolDeploymentHelper helper; - -// function setUp() public { -// // LINEA setup -// uint256 opFork = vm.createSelectFork( -// "https://linea-mainnet.infura.io/v3/f47a8617e11b481fbf52c08d4e9ecf0d" -// ); -// assertEq(vm.activeFork(), opFork); -// helper = new MiniPoolDeploymentHelper( -// ORACLE, MINI_POOL_ADDRESS_PROVIDER, MINI_POOL_CONFIGURATOR, DATA_PROVIDER -// ); -// } - -// function testCurrentDeployments() public view { -// MiniPoolDeploymentHelper.HelperPoolReserversConfig[] memory desiredReserves = -// new MiniPoolDeploymentHelper.HelperPoolReserversConfig[](6); -// desiredReserves[0] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 7500, -// borrowingEnabled: true, -// interestStrat: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, -// liquidationBonus: 10800, -// liquidationThreshold: 8000, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 1, -// tokenAddress: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b -// }); -// desiredReserves[1] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 7500, -// borrowingEnabled: true, -// interestStrat: 0xE27379F420990791a56159D54F9bad8864F217b8, -// liquidationBonus: 10800, -// liquidationThreshold: 8000, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 0, -// tokenAddress: 0x9A4cA144F38963007cFAC645d77049a1Dd4b209A -// }); -// desiredReserves[2] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 8500, -// borrowingEnabled: true, -// interestStrat: 0x499685b9A2438D0aBc36EBedaf966A2c9B18C3c0, -// liquidationBonus: 10800, -// liquidationThreshold: 9000, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 0, -// tokenAddress: 0xa500000000e482752f032eA387390b6025a2377b -// }); -// desiredReserves[3] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 5000, -// borrowingEnabled: true, -// interestStrat: 0xc3012640D1d6cE061632f4cea7f52360d50cbeD4, -// liquidationBonus: 11500, -// liquidationThreshold: 6500, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 2500000, -// tokenAddress: 0xe4eEB461Ad1e4ef8b8EF71a33694CCD84Af051C4 -// }); -// desiredReserves[4] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 8500, -// borrowingEnabled: true, -// interestStrat: 0x488D8e33f20bDc1C698632617331e68647128311, -// liquidationBonus: 10800, -// liquidationThreshold: 9000, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 0, -// tokenAddress: 0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944 -// }); -// desiredReserves[5] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 8500, -// borrowingEnabled: true, -// interestStrat: 0x6c24D7aF724E1F73CE2D26c6c6b4044f4a9d0a43, -// liquidationBonus: 10800, -// liquidationThreshold: 9000, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 0, -// tokenAddress: 0x1579072d23FB3f545016Ac67E072D37e1281624C -// }); -// (uint256 errCode, uint8 idx) = helper.checkDeploymentParams( -// 0x65559abECD1227Cc1779F500453Da1f9fcADd928, desiredReserves -// ); -// console2.log("Err code: %s idx: %s", errCode, idx); -// assertEq(errCode, 0); -// } - -// function testDeployNewMiniPoolInitAndConfigure() public view { -// IMiniPoolConfigurator.InitReserveInput[] memory _initInputParams = -// new IMiniPoolConfigurator.InitReserveInput[](4); -// _initInputParams[0] = IMiniPoolConfigurator.InitReserveInput({ -// underlyingAssetDecimals: 8, -// interestRateStrategyAddress: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, -// underlyingAsset: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b, -// underlyingAssetName: "Wrapped Astera WBTC", -// underlyingAssetSymbol: "was-WBTC" -// }); - -// _initInputParams[1] = IMiniPoolConfigurator.InitReserveInput({ -// underlyingAssetDecimals: 18, -// interestRateStrategyAddress: 0xE27379F420990791a56159D54F9bad8864F217b8, -// underlyingAsset: 0x9A4cA144F38963007cFAC645d77049a1Dd4b209A, -// underlyingAssetName: "Wrapped Astera WETH", -// underlyingAssetSymbol: "was-WETH" -// }); - -// _initInputParams[2] = IMiniPoolConfigurator.InitReserveInput({ -// underlyingAssetDecimals: 6, -// interestRateStrategyAddress: 0x488D8e33f20bDc1C698632617331e68647128311, -// underlyingAsset: 0xAD7b51293DeB2B7dbCef4C5c3379AfaF63ef5944, -// underlyingAssetName: "Wrapped Astera USDC", -// underlyingAssetSymbol: "was-USDC" -// }); - -// _initInputParams[3] = IMiniPoolConfigurator.InitReserveInput({ -// underlyingAssetDecimals: 6, -// interestRateStrategyAddress: 0x6c24D7aF724E1F73CE2D26c6c6b4044f4a9d0a43, -// underlyingAsset: 0x1579072d23FB3f545016Ac67E072D37e1281624C, -// underlyingAssetName: "Wrapped Astera USDT", -// underlyingAssetSymbol: "was-USDT" -// }); - -// MiniPoolDeploymentHelper.HelperPoolReserversConfig[] memory _reservesConfig = -// new MiniPoolDeploymentHelper.HelperPoolReserversConfig[](4); -// _reservesConfig[0] = MiniPoolDeploymentHelper.HelperPoolReserversConfig({ -// baseLtv: 7500, -// borrowingEnabled: true, -// interestStrat: 0x47968bf518FB5A3f4360DE36B67497e11b6C0872, -// liquidationBonus: 10800, -// liquidationThreshold: 8000, -// miniPoolOwnerFee: 0, -// reserveFactor: 2000, -// depositCap: 1, -// tokenAddress: 0x7dfd2F6d984CA9A2d2ffAb7350E6948E4315047b -// }); -// // helper.deployNewMiniPoolInitAndConfigure( -// // 0xfe3eA78Ec5E8D04d8992c84e43aaF508dE484646, -// // 0xD3dEe63342D0b2Ba5b508271008A81ac0114241C, -// // 0xF1D6ab29d12cF2bee25A195579F544BFcC3dD78f, -// // _initInputParams, -// // _reservesConfig -// // ); -// } -// } From b65ca3ac73688ede8ff2f0d89def6dbdadf547c2 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Thu, 25 Sep 2025 09:47:57 +0200 Subject: [PATCH 13/13] Updates --- contracts/protocol/core/twaps/EtherexTwap.sol | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/contracts/protocol/core/twaps/EtherexTwap.sol b/contracts/protocol/core/twaps/EtherexTwap.sol index d9f79cb..c4aade0 100644 --- a/contracts/protocol/core/twaps/EtherexTwap.sol +++ b/contracts/protocol/core/twaps/EtherexTwap.sol @@ -23,6 +23,7 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { error EtherexTwap__StablePairsUnsupported(); error EtherexTwap__ServiceNotAvailable(); error EtherexTwap_WrongPriceFeedDecimals(); + error EtherexTwap__Overflow(); /* Events */ event SetParams(uint128 minPrice); @@ -190,7 +191,11 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { * @return token1 The address of token1. */ function getTokens() external view returns (address token0, address token1) { - return (etherexPair.token0(), etherexPair.token1()); + if (isToken0) { + return (etherexPair.token1(), etherexPair.token0()); + } else { + return (etherexPair.token0(), etherexPair.token1()); + } } /** @@ -245,15 +250,16 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { timeElapsed = block.timestamp - _observation.timestamp; } uint112 _reserve0 = - uint112((reserve0Cumulative - _observation.reserve0Cumulative) / timeElapsed); + safe112((reserve0Cumulative - _observation.reserve0Cumulative) / timeElapsed); uint112 _reserve1 = - uint112((reserve1Cumulative - _observation.reserve1Cumulative) / timeElapsed); + safe112((reserve1Cumulative - _observation.reserve1Cumulative) / timeElapsed); uint256 twapPrice; if (!isToken0) { twapPrice = uint256(_reserve0) * WAD / (_reserve1); } else { twapPrice = uint256(_reserve1) * WAD / (_reserve0); } + if (twapPrice < minPrice) revert EtherexTwap__BelowMinPrice(); return twapPrice; } @@ -276,4 +282,9 @@ contract EtherexTwap is IChainlinkAggregator, Ownable { timeWindow = _timeWindow; emit SetTimeWindow(timeWindow); } + + function safe112(uint256 n) internal pure returns (uint112) { + if (n >= 2 ** 112) revert EtherexTwap__Overflow(); + return uint112(n); + } }