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/interfaces/IEtherexPair.sol b/contracts/interfaces/IEtherexPair.sol new file mode 100644 index 0000000..2207f4c --- /dev/null +++ b/contracts/interfaces/IEtherexPair.sol @@ -0,0 +1,135 @@ +// 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 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 + 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/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/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/contracts/protocol/core/twaps/EtherexTwap.sol b/contracts/protocol/core/twaps/EtherexTwap.sol new file mode 100644 index 0000000..c4aade0 --- /dev/null +++ b/contracts/protocol/core/twaps/EtherexTwap.sol @@ -0,0 +1,290 @@ +// 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(); + error EtherexTwap_WrongPriceFeedDecimals(); + error EtherexTwap__Overflow(); + + /* 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; + + /** + * @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 public isToken0; + + IChainlinkAggregator public 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 (_etherexPair.stable()) revert EtherexTwap__StablePairsUnsupported(); + if ( + ERC20(_etherexPair.token0()).decimals() != 18 + || ERC20(_etherexPair.token1()).decimals() != 18 + ) revert EtherexTwap_InvalidParams(); + + if (IChainlinkAggregator(_priceFeed).decimals() != 8) { + revert EtherexTwap_WrongPriceFeedDecimals(); + } + /* 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 --- */ + /** + * @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) { + if (isToken0) { + return (etherexPair.token1(), etherexPair.token0()); + } else { + 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(); + answer = int256(uint256(answer) * spotPrice / WAD); // 8 decimals; + return answer; + } + + /* --- 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) { + price = uint256(_reserve0) * WAD / (_reserve1); + } else { + price = uint256(_reserve1) * WAD / (_reserve0); + } + } + + /** + * @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,) = + etherexPair.currentCumulativePrices(); + uint256 timeElapsed = block.timestamp - _observation.timestamp; + if (timeElapsed < timeWindow) { + _observation = etherexPair.observations(etherexPair.observationLength() - 2); + timeElapsed = block.timestamp - _observation.timestamp; + } + uint112 _reserve0 = + safe112((reserve0Cumulative - _observation.reserve0Cumulative) / timeElapsed); + uint112 _reserve1 = + 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; + } + + /** + * @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); + } + + function safe112(uint256 n) internal pure returns (uint112) { + if (n >= 2 ** 112) revert EtherexTwap__Overflow(); + return uint112(n); + } +} diff --git a/oracleAnalysis.py b/oracleAnalysis.py new file mode 100644 index 0000000..dc6f8f2 --- /dev/null +++ b/oracleAnalysis.py @@ -0,0 +1,60 @@ +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): + 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])/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: + 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 = "./REX_USD_5Min.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(ticker.MaxNLocator(nbins=20)) +plt.gca().yaxis.set_major_locator(ticker.MaxNLocator(nbins=20)) +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/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/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 ae15b0b..cf05b94 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; @@ -183,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/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/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/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); diff --git a/tests/foundry/MiniPoolDeploymentHelper.t.sol b/tests/foundry/MiniPoolDeploymentHelper.t.sol deleted file mode 100644 index 284c4c7..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 - // ); - } -} diff --git a/tests/foundry/MiniPoolRewarderLinea.t.sol b/tests/foundry/MiniPoolRewarderLinea.t.sol new file mode 100644 index 0000000..3e904c8 --- /dev/null +++ b/tests/foundry/MiniPoolRewarderLinea.t.sol @@ -0,0 +1,243 @@ +// 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); + rewardTokens[idx].mint(600 ether); + rewardTokens[idx].transfer(address(rewardsVault), 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(); + + assertGt(user1Rewards[0], 0, "wrong user1 rewards0"); + assertGt(user1Rewards[1], 0, "wrong user1 rewards1"); + assertEq(user2Rewards[0], 0 ether, "wrong user2 rewards"); + } + + // 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); + // } +} diff --git a/tests/foundry/TwapLinea.t.sol b/tests/foundry/TwapLinea.t.sol new file mode 100644 index 0000000..4cf3c91 --- /dev/null +++ b/tests/foundry/TwapLinea.t.sol @@ -0,0 +1,522 @@ +// 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 {EtherexTwap} from "contracts/protocol/core/twaps/EtherexTwap.sol"; +import {IRouter} from "contracts/interfaces/IRouter.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); + 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; + 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", 23687274 + ); + assertEq(vm.activeFork(), opFork); + + 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 testCompabilityWithOracle() public { + address manipulator = makeAddr("manipulator"); + deal(address(REX), manipulator, 1000000 ether); + + // register initial oracle price + uint256 price_1 = oracle.getAssetPrice(address(REX)); + (, int256 price_2,,,) = wstEthRexTwap.latestRoundData(); + + // 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_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); + + // register initial oracle price + int256 price_1 = wstEthRexTwap.latestAnswer(); + (, int256 price_2,,,) = wstEthRexTwap.latestRoundData(); + + // 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(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 = 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); + + int256 price_1 = wstEthRexTwap.latestAnswer(); + (, int256 price_2,,,) = wstEthRexTwap.latestRoundData(); + + // 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(); + + skip(5 minutes); + + (, int256 tmpAnswer,,,) = wstEthRexTwap.latestRoundData(); + int256 spotPrice = wstEthRexTwap.getSpotPrice(); + // price should not have changed + 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); - for chart with -vv + } + + function multiplePriceManipulationWithLoopChainlink(uint256 granuality, EtherexTwap 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, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); + + 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, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); + } + + // 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, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); + } + + // 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, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); + } + + // 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, + uint256(twap.latestAnswer()), + uint256(twap.getSpotPrice()) + ); + } + } + + 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) + ); + } + + 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); + + wstEthRexTwap.setTimeWindow(55 minutes); + + assertEq(wstEthRexTwap.timeWindow(), 55 minutes); + } + + 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) + ); + + 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) + ); + } + + 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) + ); + } + + // 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); + // } +}