From dd2719091787ff5623716df804ab7d4d1ba2f005 Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 6 Sep 2024 11:42:31 -0300 Subject: [PATCH 01/23] aerodrome initial draft --- common/configuration.ts | 3 + .../aerodrome/AerodromeGaugeWrapper.sol | 49 +++ .../assets/aerodrome/AerodromePoolTokens.sol | 231 +++++++++++ .../aerodrome/AerodromeStableCollateral.sol | 153 +++++++ .../assets/aerodrome/vendor/IAeroGauge.sol | 36 ++ .../assets/aerodrome/vendor/IAeroPool.sol | 39 ++ .../assets/aerodrome/vendor/IAeroRouter.sol | 59 +++ .../aerodrome/AerodromeGaugeWrapper.test.ts | 386 ++++++++++++++++++ .../aerodrome/constants.ts | 40 ++ .../aerodrome/helpers.ts | 76 ++++ 10 files changed, 1072 insertions(+) create mode 100644 contracts/plugins/assets/aerodrome/AerodromeGaugeWrapper.sol create mode 100644 contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol create mode 100644 contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol create mode 100644 contracts/plugins/assets/aerodrome/vendor/IAeroGauge.sol create mode 100644 contracts/plugins/assets/aerodrome/vendor/IAeroPool.sol create mode 100644 contracts/plugins/assets/aerodrome/vendor/IAeroRouter.sol create mode 100644 test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts create mode 100644 test/plugins/individual-collateral/aerodrome/constants.ts create mode 100644 test/plugins/individual-collateral/aerodrome/helpers.ts diff --git a/common/configuration.ts b/common/configuration.ts index 928d4b96a..91b59421c 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -516,6 +516,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca', wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df', + AERO: '0x940181a94A35A4569E4529A3CDfB74e38FD98631', + eUSD: '0xCfA3Ef56d303AE4fAabA0592388F19d7C3399FB4', }, chainlinkFeeds: { DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr @@ -532,6 +534,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h + eUSD: '0x9b2C948dbA5952A1f5Ab6fA16101c1392b8da1ab', }, GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock COMET_REWARDS: '0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1', diff --git a/contracts/plugins/assets/aerodrome/AerodromeGaugeWrapper.sol b/contracts/plugins/assets/aerodrome/AerodromeGaugeWrapper.sol new file mode 100644 index 000000000..dc6ff7f5a --- /dev/null +++ b/contracts/plugins/assets/aerodrome/AerodromeGaugeWrapper.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../erc20/RewardableERC20Wrapper.sol"; +import "./vendor/IAeroGauge.sol"; + +// Note: Only supports AERO rewards. +contract AerodromeGaugeWrapper is RewardableERC20Wrapper { + using SafeERC20 for IERC20; + + IAeroGauge public immutable gauge; + + /// @param _lpToken The Aerodrome LP token, transferrable + constructor( + ERC20 _lpToken, + string memory _name, + string memory _symbol, + ERC20 _aero, + IAeroGauge _gauge + ) RewardableERC20Wrapper(_lpToken, _name, _symbol, _aero) { + require( + address(_aero) != address(0) && + address(_gauge) != address(0) && + address(_lpToken) != address(0), + "invalid address" + ); + + require(address(_aero) == address(_gauge.rewardToken()), "wrong Aero"); + + gauge = _gauge; + } + + // deposit an Aerodrome LP token + function _afterDeposit(uint256 _amount, address) internal override { + underlying.approve(address(gauge), _amount); + gauge.deposit(_amount); + } + + // withdraw to Aerodrome LP token + function _beforeWithdraw(uint256 _amount, address) internal override { + gauge.withdraw(_amount); + } + + // claim rewards - only supports AERO rewards + function _claimAssetRewards() internal virtual override { + gauge.getReward(address(this)); + } +} diff --git a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol new file mode 100644 index 000000000..62a04bed8 --- /dev/null +++ b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: ISC +pragma solidity 0.8.19; + +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "contracts/plugins/assets/OracleLib.sol"; +import "contracts/libraries/Fixed.sol"; +import "./vendor/IAeroPool.sol"; + +/// Supports Aerodrome stable pools (2 tokens) +contract AerodromePoolTokens { + using OracleLib for AggregatorV3Interface; + using FixLib for uint192; + + error WrongIndex(uint8 maxLength); + error NoToken(uint8 tokenNumber); + + uint8 internal constant nTokens = 2; + + enum AeroPoolType { + Stable, + Volatile // not supported in this version + } + + // === State (Immutable) === + + IAeroPool public immutable pool; + + IERC20Metadata internal immutable token0; + IERC20Metadata internal immutable token1; + + // For each token, we maintain up to two feeds/timeouts/errors + // The data below would normally be a struct, but we want bytecode substitution + + AggregatorV3Interface internal immutable _t0feed0; + AggregatorV3Interface internal immutable _t0feed1; + uint48 internal immutable _t0timeout0; // {s} + uint48 internal immutable _t0timeout1; // {s} + uint192 internal immutable _t0error0; // {1} + uint192 internal immutable _t0error1; // {1} + + AggregatorV3Interface internal immutable _t1feed0; + AggregatorV3Interface internal immutable _t1feed1; + uint48 internal immutable _t1timeout0; // {s} + uint48 internal immutable _t1timeout1; // {s} + uint192 internal immutable _t1error0; // {1} + uint192 internal immutable _t1error1; // {1} + + // === Config === + + struct APTConfiguration { + IAeroPool pool; + AeroPoolType poolType; + AggregatorV3Interface[][] feeds; // row should multiply to give {UoA/ref}; max columns is 2 + uint48[][] oracleTimeouts; // {s} same order as feeds + uint192[][] oracleErrors; // {1} same order as feeds + } + + constructor(APTConfiguration memory config) { + require(maxFeedsLength(config.feeds) <= 2, "price feeds limited to 2"); + require( + config.feeds.length == nTokens && minFeedsLength(config.feeds) != 0, + "each token needs at least 1 price feed" + ); + require(address(config.pool) != address(0), "pool address is zero"); + + pool = config.pool; + + // Solidity does not support immutable arrays. This is a hack to get the equivalent of + // an immutable array so we do not have store the token feeds in the blockchain. This is + // a gas optimization since it is significantly more expensive to read and write on the + // blockchain than it is to use embedded values in the bytecode. + + // === Tokens === + + if (config.poolType != AeroPoolType.Stable) { + revert("invalid poolType"); + } + + token0 = IERC20Metadata(pool.token0()); + token1 = IERC20Metadata(pool.token1()); + + // === Feeds + timeouts === + // I know this section at-first looks verbose and silly, but it's actually well-justified: + // - immutable variables cannot be conditionally written to + // - a struct or an array would not be able to be immutable + // - immutable variables means values get in-lined in the bytecode + + // token0 + bool more = config.feeds[0].length != 0; + // untestable: + // more will always be true based on previous feeds validations + _t0feed0 = more ? config.feeds[0][0] : AggregatorV3Interface(address(0)); + _t0timeout0 = more && config.oracleTimeouts[0].length != 0 + ? config.oracleTimeouts[0][0] + : 0; + _t0error0 = more && config.oracleErrors[0].length != 0 ? config.oracleErrors[0][0] : 0; + if (more) { + require(address(_t0feed0) != address(0), "t0feed0 empty"); + require(_t0timeout0 != 0, "t0timeout0 zero"); + require(_t0error0 < FIX_ONE, "t0error0 too large"); + } + + more = config.feeds[0].length > 1; + _t0feed1 = more ? config.feeds[0][1] : AggregatorV3Interface(address(0)); + _t0timeout1 = more && config.oracleTimeouts[0].length > 1 ? config.oracleTimeouts[0][1] : 0; + _t0error1 = more && config.oracleErrors[0].length > 1 ? config.oracleErrors[0][1] : 0; + if (more) { + require(address(_t0feed1) != address(0), "t0feed1 empty"); + require(_t0timeout1 != 0, "t0timeout1 zero"); + require(_t0error1 < FIX_ONE, "t0error1 too large"); + } + + // token1 + // untestable: + // more will always be true based on previous feeds validations + more = config.feeds[1].length != 0; + _t1feed0 = more ? config.feeds[1][0] : AggregatorV3Interface(address(0)); + _t1timeout0 = more && config.oracleTimeouts[1].length != 0 + ? config.oracleTimeouts[1][0] + : 0; + _t1error0 = more && config.oracleErrors[1].length != 0 ? config.oracleErrors[1][0] : 0; + if (more) { + require(address(_t1feed0) != address(0), "t1feed0 empty"); + require(_t1timeout0 != 0, "t1timeout0 zero"); + require(_t1error0 < FIX_ONE, "t1error0 too large"); + } + + more = config.feeds[1].length > 1; + _t1feed1 = more ? config.feeds[1][1] : AggregatorV3Interface(address(0)); + _t1timeout1 = more && config.oracleTimeouts[1].length > 1 ? config.oracleTimeouts[1][1] : 0; + _t1error1 = more && config.oracleErrors[1].length > 1 ? config.oracleErrors[1][1] : 0; + if (more) { + require(address(_t1feed1) != address(0), "t1feed1 empty"); + require(_t1timeout1 != 0, "t1timeout1 zero"); + require(_t1error1 < FIX_ONE, "t1error1 too large"); + } + } + + /// @dev Warning: Can revert + /// @param index The index of the token: 0 or 1 + /// @return low {UoA/ref_index} + /// @return high {UoA/ref_index} + function tokenPrice(uint8 index) public view virtual returns (uint192 low, uint192 high) { + if (index >= nTokens) revert WrongIndex(nTokens - 1); + + // Use only 1 feed if 2nd feed not defined + // otherwise: multiply feeds together, e.g; {UoA/ref} = {UoA/target} * {target/ref} + uint192 x; + uint192 y = FIX_ONE; + uint192 xErr; // {1} + uint192 yErr; // {1} + // if only 1 feed: `y` is FIX_ONE and `yErr` is 0 + + if (index == 0) { + x = _t0feed0.price(_t0timeout0); + xErr = _t0error0; + if (address(_t0feed1) != address(0)) { + y = _t0feed1.price(_t0timeout1); + yErr = _t0error1; + } + } else { + x = _t1feed0.price(_t1timeout0); + xErr = _t1error0; + if (address(_t1feed1) != address(0)) { + y = _t1feed1.price(_t1timeout1); + yErr = _t1error1; + } + } + + return toRange(x, y, xErr, yErr); + } + + /// @param index The index of the token: 0 or 1 + /// @return [{ref_index}] + function tokenReserve(uint8 index) public view virtual returns (uint256) { + if (index >= nTokens) revert WrongIndex(nTokens - 1); + if (index == 0) return pool.reserve0(); + return pool.reserve1(); + } + + // === Internal === + + function maxPoolOracleTimeout() internal view virtual returns (uint48) { + return + uint48( + Math.max(Math.max(_t0timeout0, _t1timeout0), Math.max(_t0timeout1, _t1timeout1)) + ); + } + + // === Private === + + function getToken(uint8 index) private view returns (IERC20Metadata) { + // untestable: + // getToken is always called with a valid index + if (index >= nTokens) revert WrongIndex(nTokens - 1); + if (index == 0) return token0; + return token1; + } + + function minFeedsLength(AggregatorV3Interface[][] memory feeds) private pure returns (uint8) { + uint8 minLength = type(uint8).max; + for (uint8 i = 0; i < feeds.length; ++i) { + minLength = uint8(Math.min(minLength, feeds[i].length)); + } + return minLength; + } + + function maxFeedsLength(AggregatorV3Interface[][] memory feeds) private pure returns (uint8) { + uint8 maxLength; + for (uint8 i = 0; i < feeds.length; ++i) { + maxLength = uint8(Math.max(maxLength, feeds[i].length)); + } + return maxLength; + } + + /// x and y can be any two fixes that can be multiplied + /// @param xErr {1} error associated with x + /// @param yErr {1} error associated with y + /// returns low and high extremes of x * y, given errors + function toRange( + uint192 x, + uint192 y, + uint192 xErr, + uint192 yErr + ) private pure returns (uint192 low, uint192 high) { + low = x.mul(FIX_ONE - xErr).mul(y.mul(FIX_ONE - yErr), FLOOR); + high = x.mul(FIX_ONE + xErr).mul(y.mul(FIX_ONE + yErr), CEIL); + } +} diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol new file mode 100644 index 000000000..81311a800 --- /dev/null +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "contracts/interfaces/IAsset.sol"; +import "contracts/libraries/Fixed.sol"; +import "contracts/plugins/assets/FiatCollateral.sol"; +import "../../../interfaces/IRewardable.sol"; +import "./AerodromePoolTokens.sol"; + +// This plugin only works on Base +IERC20 constant AERO = IERC20(0x940181a94A35A4569E4529A3CDfB74e38FD98631); + +/** + * @title AerodromeStableCollateral + * This plugin contract is designed for Aerodrome stable pools + * Each token in the pool can have between 1 and 2 oracles per each token. + * + * tok = AerodromeStakingWrapper(stablePool) + * ref = 1e18 (fixed) + * tar = USD + * UoA = USD + * + */ +contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { + using OracleLib for AggregatorV3Interface; + using FixLib for uint192; + + /// @dev config Unused members: chainlinkFeed, oracleError, oracleTimeout + /// @dev No revenue hiding (refPerTok() == FIX_ONE) + /// @dev config.erc20 should be an AerodromeStakingWrapper + constructor(CollateralConfig memory config, APTConfiguration memory aptConfig) + FiatCollateral(config) + AerodromePoolTokens(aptConfig) + { + require(config.defaultThreshold != 0, "defaultThreshold zero"); + maxOracleTimeout = uint48(Math.max(maxOracleTimeout, maxPoolOracleTimeout())); + } + + /// Can revert, used by other contract functions in order to catch errors + /// Should not return FIX_MAX for low + /// Should only return FIX_MAX for high if low is 0 + /// Should NOT be manipulable by MEV + /// @return low {UoA/tok} The low price estimate + /// @return high {UoA/tok} The high price estimate + /// @return pegPrice {target/ref} The actual price observed in the peg + function tryPrice() + external + view + virtual + override + returns ( + uint192 low, + uint192 high, + uint192 pegPrice + ) + { + // get reserves + uint192 r0 = shiftl_toFix(tokenReserve(0), -int8(token0.decimals()), FLOOR); + uint192 r1 = shiftl_toFix(tokenReserve(1), -int8(token1.decimals()), FLOOR); + uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); + uint192 sqrtK = (r0.sqrt()).mulDiv(r1.sqrt(), totalSupply); + + // get token prices + (uint192 p0_low, uint192 p0_high) = tokenPrice(0); + (uint192 p1_low, uint192 p1_high) = tokenPrice(1); + + // {UoA/tok} + low = sqrtK.mul(2).mul(((p0_low.mul(p1_low)).sqrt())); + high = sqrtK.mul(2).mul(((p0_high.mul(p1_high)).sqrt())); + + assert(low <= high); //obviously true just by inspection + pegPrice = FIX_ONE; + } + + /// Should not revert + /// Refresh exchange rates and update default status. + /// Have to override to add custom default checks + function refresh() public virtual override { + CollateralStatus oldStatus = status(); + + // Check for soft default + save prices + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { + // {UoA/tok}, {UoA/tok}, {UoA/tok} + // (0, 0) is a valid price; (0, FIX_MAX) is unpriced + + // Save prices if priced + if (high != FIX_MAX) { + savedLowPrice = low; + savedHighPrice = high; + savedPegPrice = pegPrice; + lastSave = uint48(block.timestamp); + } else { + // must be unpriced + // untested: + // validated in other plugins, cost to test here is high + assert(low == 0); + } + + // If the price is below the default-threshold price, default eventually + // uint192(+/-) is the same as Fix.plus/minus + if (low == 0 || _anyDepeggedInPool()) { + markStatus(CollateralStatus.IFFY); + } else { + markStatus(CollateralStatus.SOUND); + } + } catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + if (errData.length == 0) revert(); // solhint-disable-line reason-string + markStatus(CollateralStatus.IFFY); + } + + CollateralStatus newStatus = status(); + if (oldStatus != newStatus) { + emit CollateralStatusChanged(oldStatus, newStatus); + } + } + + /// Claim rewards earned by holding a balance of the ERC20 token + /// @custom:delegate-call + function claimRewards() external virtual override(Asset, IRewardable) { + uint256 aeroBal = AERO.balanceOf(address(this)); + IRewardable(address(erc20)).claimRewards(); + emit RewardsClaimed(AERO, AERO.balanceOf(address(this)) - aeroBal); + } + + // === Internal === + + // Override this later to implement non-stable pools + function _anyDepeggedInPool() internal view virtual returns (bool) { + // Check reference token oracles + for (uint8 i = 0; i < nTokens; ++i) { + try this.tokenPrice(i) returns (uint192 low, uint192 high) { + // {UoA/tok} = {UoA/tok} + {UoA/tok} + uint192 mid = (low + high) / 2; + + // If the price is below the default-threshold price, default eventually + // uint192(+/-) is the same as Fix.plus/minus + if (mid < pegBottom || mid > pegTop) return true; + } catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + // untested: + // pattern validated in other plugins, cost to test is high + if (errData.length == 0) revert(); // solhint-disable-line reason-string + return true; + } + } + + return false; + } +} diff --git a/contracts/plugins/assets/aerodrome/vendor/IAeroGauge.sol b/contracts/plugins/assets/aerodrome/vendor/IAeroGauge.sol new file mode 100644 index 000000000..4611f5f50 --- /dev/null +++ b/contracts/plugins/assets/aerodrome/vendor/IAeroGauge.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +interface IAeroGauge { + error ZeroAmount(); + + /// @notice Address of the pool LP token which is deposited (staked) for rewards + function stakingToken() external view returns (address); + + /// @notice Address of the token (AERO) rewarded to stakers + function rewardToken() external view returns (address); + + /// @notice Cached amount of rewardToken earned for an account + function rewards(address) external view returns (uint256); + + /// @notice Returns accrued balance to date from last claim / first deposit. + function earned(address _account) external view returns (uint256); + + /// @notice Retrieve rewards for an address. + /// @dev Throws if not called by same address or voter. + /// @param _account . + function getReward(address _account) external; + + /// @notice Deposit LP tokens into gauge for msg.sender + /// @param _amount . + function deposit(uint256 _amount) external; + + /// @notice Deposit LP tokens into gauge for any user + /// @param _amount . + /// @param _recipient Recipient to give balance to + function deposit(uint256 _amount, address _recipient) external; + + /// @notice Withdraw LP tokens for user + /// @param _amount . + function withdraw(uint256 _amount) external; +} diff --git a/contracts/plugins/assets/aerodrome/vendor/IAeroPool.sol b/contracts/plugins/assets/aerodrome/vendor/IAeroPool.sol new file mode 100644 index 000000000..7070453f6 --- /dev/null +++ b/contracts/plugins/assets/aerodrome/vendor/IAeroPool.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +// solhint-disable func-param-name-mixedcase, func-name-mixedcase +interface IAeroPool is IERC20Metadata { + /// @notice Returns [token0, token1] + function tokens() external view returns (address, address); + + /// @notice Address of token in the pool with the lower address value + function token0() external view returns (address); + + /// @notice Address of token in the poool with the higher address value + function token1() external view returns (address); + + /// @notice Amount of token0 in pool + function reserve0() external view returns (uint256); + + /// @notice Amount of token1 in pool + function reserve1() external view returns (uint256); + + function stable() external view returns (bool); + + function mint(address to) external returns (uint256 liquidity); + + /// @notice Update reserves and, on the first call per block, price accumulators + /// @return _reserve0 . + /// @return _reserve1 . + /// @return _blockTimestampLast . + function getReserves() + external + view + returns ( + uint256 _reserve0, + uint256 _reserve1, + uint256 _blockTimestampLast + ); +} diff --git a/contracts/plugins/assets/aerodrome/vendor/IAeroRouter.sol b/contracts/plugins/assets/aerodrome/vendor/IAeroRouter.sol new file mode 100644 index 000000000..c183e56f9 --- /dev/null +++ b/contracts/plugins/assets/aerodrome/vendor/IAeroRouter.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +interface IAeroRouter { + /// @notice Add liquidity of two tokens to a Pool + /// @param tokenA . + /// @param tokenB . + /// @param stable True if pool is stable, false if volatile + /// @param amountADesired Amount of tokenA desired to deposit + /// @param amountBDesired Amount of tokenB desired to deposit + /// @param amountAMin Minimum amount of tokenA to deposit + /// @param amountBMin Minimum amount of tokenB to deposit + /// @param to Recipient of liquidity token + /// @param deadline Deadline to receive liquidity + /// @return amountA Amount of tokenA to actually deposit + /// @return amountB Amount of tokenB to actually deposit + /// @return liquidity Amount of liquidity token returned from deposit + 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 + ); + + // **** REMOVE LIQUIDITY **** + + /// @notice Remove liquidity of two tokens from a Pool + /// @param tokenA . + /// @param tokenB . + /// @param stable True if pool is stable, false if volatile + /// @param liquidity Amount of liquidity to remove + /// @param amountAMin Minimum amount of tokenA to receive + /// @param amountBMin Minimum amount of tokenB to receive + /// @param to Recipient of tokens received + /// @param deadline Deadline to remove liquidity + /// @return amountA Amount of tokenA received + /// @return amountB Amount of tokenB received + function removeLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); +} diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts new file mode 100644 index 000000000..954656fd8 --- /dev/null +++ b/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts @@ -0,0 +1,386 @@ +import { networkConfig } from '#/common/configuration' +import { useEnv } from '#/utils/env' +import hre, { BigNumber, ethers } from 'hardhat' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { makeWUSDCeUSD, mintLpToken, mintWrappedLpToken, resetFork } from './helpers' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { + IAeroPool, + ERC20Mock, + AerodromeGaugeWrapper__factory, + AerodromeGaugeWrapper, + IAeroGauge, +} from '@typechain/index' +import { expect } from 'chai' +import { ZERO_ADDRESS } from '#/common/constants' +import { + AERO, + eUSD, + AERO_USDC_eUSD_GAUGE, + AERO_USDC_eUSD_POOL, + AERO_USDC_eUSD_HOLDER, +} from './constants' +import { bn, fp } from '#/common/numbers' +import { getChainId } from '#/common/blockchain-utils' +import { advanceTime } from '#/test/utils/time' + +const describeFork = useEnv('FORK') ? describe : describe.skip + +const point1Pct = (value: BigNumber): BigNumber => { + return value.div(1000) +} +describeFork('Aerodrome Gauge Wrapper', () => { + let bob: SignerWithAddress + let charles: SignerWithAddress + let don: SignerWithAddress + let token0: ERC20Mock + let token1: ERC20Mock + let aero: ERC20Mock + let gauge: IAeroGauge + let wrapper: AerodromeGaugeWrapper + let lpToken: IAeroPool + let AerodromeGaugeWrapperFactory: AerodromeGaugeWrapper__factory + + let chainId: number + + before(async () => { + await resetFork() + + chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + AerodromeGaugeWrapperFactory = ( + await ethers.getContractFactory('AerodromeGaugeWrapper') + ) + }) + + beforeEach(async () => { + ;[, bob, charles, don] = await ethers.getSigners() + ;({ token0, token1, wrapper, lpToken } = await loadFixture(makeWUSDCeUSD)) + gauge = await ethers.getContractAt('IAeroGauge', AERO_USDC_eUSD_GAUGE) + }) + + describe('Deployment', () => { + it('reverts if deployed with a 0 address for AERO token or staking contract', async () => { + await expect( + AerodromeGaugeWrapperFactory.deploy( + AERO_USDC_eUSD_POOL, + await wrapper.name(), + await wrapper.symbol(), + ZERO_ADDRESS, + AERO_USDC_eUSD_GAUGE + ) + ).to.be.reverted + + await expect( + AerodromeGaugeWrapperFactory.deploy( + AERO_USDC_eUSD_POOL, + await wrapper.name(), + await wrapper.symbol(), + AERO, + ZERO_ADDRESS + ) + ).to.be.reverted + }) + + it('reverts if deployed with invalid pool', async () => { + await expect( + AerodromeGaugeWrapperFactory.deploy( + ZERO_ADDRESS, + await wrapper.name(), + await wrapper.symbol(), + AERO, + AERO_USDC_eUSD_GAUGE + ) + ).to.be.reverted + }) + + it('reverts if deployed with invalid AERO token', async () => { + const INVALID_AERO = eUSD // mock (any erc20) + await expect( + AerodromeGaugeWrapperFactory.deploy( + AERO_USDC_eUSD_POOL, + await wrapper.name(), + await wrapper.symbol(), + INVALID_AERO, + AERO_USDC_eUSD_GAUGE + ) + ).to.be.revertedWith('wrong Aero') + }) + }) + + describe('Deposit', () => { + const amount = fp('0.02') + + beforeEach(async () => { + await mintLpToken(gauge, lpToken, amount, AERO_USDC_eUSD_HOLDER, bob.address) + await lpToken.connect(bob).approve(wrapper.address, ethers.constants.MaxUint256) + }) + + it('deposits correct amount', async () => { + const balanceInLPPrev = await lpToken.balanceOf(bob.address) + + await wrapper.connect(bob).deposit(await lpToken.balanceOf(bob.address), bob.address) + + expect(await lpToken.balanceOf(bob.address)).to.equal(0) + expect(await wrapper.balanceOf(bob.address)).to.equal(balanceInLPPrev) + }) + + it('deposits less than available', async () => { + const depositAmount = await lpToken.balanceOf(bob.address).then((e) => e.div(2)) + + await wrapper.connect(bob).deposit(depositAmount, bob.address) + + expect(await lpToken.balanceOf(bob.address)).to.be.closeTo(depositAmount, 10) + expect(await wrapper.balanceOf(bob.address)).to.closeTo(depositAmount, 10) + }) + + it('has accurate balances when doing multiple deposits', async () => { + const depositAmount = await lpToken.balanceOf(bob.address) + await wrapper.connect(bob).deposit(depositAmount.mul(3).div(4), bob.address) + + await advanceTime(1000) + await wrapper.connect(bob).deposit(depositAmount.mul(1).div(4), bob.address) + + expect(await wrapper.balanceOf(bob.address)).to.closeTo(depositAmount, 10) + }) + + it('updates the totalSupply', async () => { + const totalSupplyBefore = await wrapper.totalSupply() + const expectedAmount = await lpToken.balanceOf(bob.address) + + await wrapper.connect(bob).deposit(expectedAmount, bob.address) + expect(await wrapper.totalSupply()).to.equal(totalSupplyBefore.add(expectedAmount)) + }) + + it('handles deposits with 0 amount', async () => { + const balanceInLPPrev = await lpToken.balanceOf(bob.address) + + await expect(wrapper.connect(bob).deposit(0, bob.address)).to.not.be.reverted + + expect(await lpToken.balanceOf(bob.address)).to.equal(balanceInLPPrev) + expect(await wrapper.balanceOf(bob.address)).to.equal(0) + }) + }) + + describe('Withdraw', () => { + const initAmt = fp('0.02') + + beforeEach(async () => { + await mintWrappedLpToken( + wrapper, + gauge, + lpToken, + initAmt, + AERO_USDC_eUSD_HOLDER, + bob, + bob.address + ) + await mintWrappedLpToken( + wrapper, + gauge, + lpToken, + initAmt, + AERO_USDC_eUSD_HOLDER, + charles, + charles.address + ) + }) + + it('withdraws to own account', async () => { + const initialBal = await wrapper.balanceOf(bob.address) + await wrapper.connect(bob).withdraw(await wrapper.balanceOf(bob.address), bob.address) + const finalBal = await wrapper.balanceOf(bob.address) + + expect(finalBal).to.closeTo(bn('0'), 10) + expect(await lpToken.balanceOf(bob.address)).to.closeTo(initialBal, 10) + }) + + it('withdraws all balance via multiple withdrawals', async () => { + const initialBalance = await wrapper.balanceOf(bob.address) + + const withdrawAmt = initialBalance.div(2) + await wrapper.connect(bob).withdraw(withdrawAmt, bob.address) + expect(await wrapper.balanceOf(bob.address)).to.closeTo(initialBalance.sub(withdrawAmt), 0) + + await advanceTime(1000) + + await wrapper.connect(bob).withdraw(withdrawAmt, bob.address) + expect(await wrapper.balanceOf(bob.address)).to.closeTo(bn('0'), 10) + }) + + it('handles complex withdrawal sequence', async () => { + let bobWithdrawn = bn('0') + let charlesWithdrawn = bn('0') + let donWithdrawn = bn('0') + + const firstWithdrawAmt = await wrapper.balanceOf(charles.address).then((e) => e.div(2)) + + charlesWithdrawn = charlesWithdrawn.add(firstWithdrawAmt) + + await wrapper.connect(charles).withdraw(firstWithdrawAmt, charles.address) + const newBalanceCharles = await lpToken.balanceOf(charles.address) + expect(newBalanceCharles).to.closeTo(firstWithdrawAmt, 10) + + // don deposits + await mintWrappedLpToken( + wrapper, + gauge, + lpToken, + initAmt, + AERO_USDC_eUSD_HOLDER, + don, + don.address + ) + + // bob withdraws SOME + bobWithdrawn = bobWithdrawn.add(bn('12345e6')) + await wrapper.connect(bob).withdraw(bn('12345e6'), bob.address) + + // don withdraws SOME + donWithdrawn = donWithdrawn.add(bn('123e6')) + await wrapper.connect(don).withdraw(bn('123e6'), don.address) + + // charles withdraws ALL + const charlesRemainingBalance = await wrapper.balanceOf(charles.address) + charlesWithdrawn = charlesWithdrawn.add(charlesRemainingBalance) + await wrapper.connect(charles).withdraw(charlesRemainingBalance, charles.address) + + // don withdraws ALL + const donRemainingBalance = await wrapper.balanceOf(don.address) + donWithdrawn = donWithdrawn.add(donRemainingBalance) + await wrapper.connect(don).withdraw(donRemainingBalance, don.address) + + // bob withdraws ALL + const bobRemainingBalance = await wrapper.balanceOf(bob.address) + bobWithdrawn = bobWithdrawn.add(bobRemainingBalance) + await wrapper.connect(bob).withdraw(bobRemainingBalance, bob.address) + + const bal = await wrapper.balanceOf(bob.address) + + expect(bal).to.closeTo(bn('0'), 10) + expect(await lpToken.balanceOf(bob.address)).to.closeTo(bobWithdrawn, 100) + expect(await lpToken.balanceOf(charles.address)).to.closeTo(charlesWithdrawn, 100) + expect(await lpToken.balanceOf(don.address)).to.closeTo(donWithdrawn, 100) + }) + + it('updates the totalSupply', async () => { + const totalSupplyBefore = await wrapper.totalSupply() + const withdrawAmt = bn('15000e6') + const expectedDiff = withdrawAmt + await wrapper.connect(bob).withdraw(withdrawAmt, bob.address) + + expect(await wrapper.totalSupply()).to.be.closeTo(totalSupplyBefore.sub(expectedDiff), 10) + }) + }) + + describe('Rewards', () => { + const initialAmount = fp('0.02') + + beforeEach(async () => { + aero = await ethers.getContractAt('ERC20Mock', AERO) + }) + + it('claims rewards from Aerodrome', async () => { + await mintWrappedLpToken( + wrapper, + gauge, + lpToken, + initialAmount, + AERO_USDC_eUSD_HOLDER, + bob, + bob.address + ) + + const initialAeroBal = await aero.balanceOf(wrapper.address) + + await advanceTime(1000) + + let expectedRewards = await gauge.earned(wrapper.address) + await wrapper.claimRewards() + expect(await gauge.earned(wrapper.address)).to.equal(0) // all claimed + + const updatedAeroBal = await aero.balanceOf(wrapper.address) + expect(updatedAeroBal).to.be.gt(initialAeroBal) + expect(updatedAeroBal.sub(initialAeroBal)).to.be.closeTo( + expectedRewards, + point1Pct(expectedRewards) + ) + + await advanceTime(1000) + + expectedRewards = await gauge.earned(wrapper.address) + await wrapper.claimRewards() + expect(await gauge.earned(wrapper.address)).to.equal(0) // all claimed + + const finalAeroBal = await aero.balanceOf(wrapper.address) + expect(finalAeroBal).to.be.gt(updatedAeroBal) + expect(finalAeroBal.sub(updatedAeroBal)).to.be.closeTo( + expectedRewards, + point1Pct(expectedRewards) + ) + }) + + it('distributes rewards to holder', async () => { + expect(await aero.balanceOf(bob.address)).to.equal(0) + expect(await aero.balanceOf(don.address)).to.equal(0) + + // deposit with bob + await mintWrappedLpToken( + wrapper, + gauge, + lpToken, + initialAmount, + AERO_USDC_eUSD_HOLDER, + bob, + bob.address + ) + + await advanceTime(1000) + + // sync rewards + await wrapper.connect(bob).claimRewards() + + let expectedRewardsBob = await wrapper.accumulatedRewards(bob.address) + + // bob can claim and get rewards + await wrapper.connect(bob).claimRewards() + expect(await aero.balanceOf(bob.address)).to.be.gt(0) + expect(await aero.balanceOf(bob.address)).to.be.closeTo( + expectedRewardsBob, + point1Pct(expectedRewardsBob) + ) + + // don does not have rewards + await wrapper.connect(don).claimRewards() + expect(await aero.balanceOf(don.address)).to.equal(0) + + // transfer some tokens to don + const balToTransfer = (await wrapper.balanceOf(bob.address)).div(2) + await wrapper.connect(bob).transfer(don.address, balToTransfer) + + await advanceTime(1000) + + // Now both have rewards + await wrapper.connect(bob).claimRewards() + expectedRewardsBob = await wrapper.accumulatedRewards(bob.address) + expect(await aero.balanceOf(bob.address)).to.be.closeTo( + expectedRewardsBob, + point1Pct(expectedRewardsBob) + ) + + // Don also gets rewards + await wrapper.connect(don).claimRewards() + const expectedRewardsDon = await wrapper.accumulatedRewards(don.address) + expect(await aero.balanceOf(don.address)).to.be.gt(0) + expect(await aero.balanceOf(don.address)).to.be.closeTo( + expectedRewardsDon, + point1Pct(expectedRewardsDon) + ) + }) + }) + + // TODO: Aerodrome exceptions? +}) diff --git a/test/plugins/individual-collateral/aerodrome/constants.ts b/test/plugins/individual-collateral/aerodrome/constants.ts new file mode 100644 index 000000000..720b5f1fc --- /dev/null +++ b/test/plugins/individual-collateral/aerodrome/constants.ts @@ -0,0 +1,40 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Base Addresses +export const AERO_USDC_eUSD_GAUGE = '0x793F22aB88dC91793E5Ce6ADbd7E733B0BD4733e' +export const AERO_USDC_eUSD_POOL = '0x7A034374C89C463DD65D8C9BCfe63BcBCED41f4F' +export const AERO_USDC_eUSD_HOLDER = '0xB6C8ea53ABA64a4BdE857D3b25d9DEbD0B149a0a' + +export const AERODROME_ROUTER = '0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43' + +// Tokens +export const USDC = networkConfig['8453'].tokens.USDC! +export const eUSD = networkConfig['8453'].tokens.eUSD! +export const AERO = networkConfig['8453'].tokens.AERO! + +// USDC +export const USDC_USD_FEED = networkConfig['8453'].chainlinkFeeds.USDC! +export const USDC_ORACLE_TIMEOUT = bn('86400') +export const USDC_ORACLE_ERROR = fp('0.003') +export const USDC_HOLDER = '0x3304E22DDaa22bCdC5fCa2269b418046aE7b566A' + +// eUSD +export const eUSD_USD_FEED = networkConfig['8453'].chainlinkFeeds.eUSD! +export const eUSD_ORACLE_TIMEOUT = bn('86400') +export const eUSD_ORACLE_ERROR = fp('0.005') +export const eUSD_HOLDER = '0xb5E331615FdbA7DF49e05CdEACEb14Acdd5091c3' + +export const FORK_BLOCK = 19074000 + +// Common +export const FIX_ONE = 1n * 10n ** 18n +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const DEFAULT_THRESHOLD = fp('0.02') // 2% +export const DELAY_UNTIL_DEFAULT = bn('259200') // 72h +export const MAX_TRADE_VOL = fp('1e6') + +export enum AerodromePoolType { + Stable, + Volatile, +} diff --git a/test/plugins/individual-collateral/aerodrome/helpers.ts b/test/plugins/individual-collateral/aerodrome/helpers.ts new file mode 100644 index 000000000..266515a55 --- /dev/null +++ b/test/plugins/individual-collateral/aerodrome/helpers.ts @@ -0,0 +1,76 @@ +import { ERC20Mock } from '@typechain/ERC20Mock' +import { + IAeroPool, + IAeroGauge, + AerodromeGaugeWrapper__factory, + AerodromeGaugeWrapper, +} from '@typechain/index' +import { ethers } from 'hardhat' +import { + USDC, + eUSD, + AERO, + AERO_USDC_eUSD_POOL, + AERO_USDC_eUSD_GAUGE, + FORK_BLOCK, +} from './constants' +import { getResetFork } from '../helpers' +import { whileImpersonating } from '#/test/utils/impersonation' + +interface WrappedAeroFixture { + token0: ERC20Mock + token1: ERC20Mock + wrapper: AerodromeGaugeWrapper + lpToken: IAeroPool +} + +export const makeWUSDCeUSD = async (sAMM_usdc_eUSD?: string): Promise => { + const lpToken = ( + await ethers.getContractAt('IAeroPool', sAMM_usdc_eUSD ?? AERO_USDC_eUSD_POOL) + ) + + const AerodromGaugeWrapperFactory = ( + await ethers.getContractFactory('AerodromeGaugeWrapper') + ) + + const wrapper = await AerodromGaugeWrapperFactory.deploy( + lpToken.address, + 'w' + (await lpToken.name()), + 'w' + (await lpToken.symbol()), + AERO, + AERO_USDC_eUSD_GAUGE + ) + const token0 = await ethers.getContractAt('ERC20Mock', USDC) + const token1 = await ethers.getContractAt('ERC20Mock', eUSD) + + return { token0, token1, wrapper, lpToken } +} + +export const mintLpToken = async ( + gauge: IAeroGauge, + lpToken: IAeroPool, + amount: BigNumberish, + holder: string, + recipient: string +) => { + await whileImpersonating(holder, async (signer) => { + await gauge.connect(signer).withdraw(amount) + await lpToken.connect(signer).transfer(recipient, amount) + }) +} + +export const mintWrappedLpToken = async ( + wrapper: AerodromeGaugeWrapper, + gauge: IAeroGauge, + lpToken: IAeroPool, + amount: BigNumberish, + holder: string, + user: SignerWithAddress, + recipient: string +) => { + await mintLpToken(gauge, lpToken, amount, holder, user.address) + await lpToken.connect(user).approve(wrapper.address, ethers.constants.MaxUint256) + await wrapper.connect(user).deposit(amount, recipient) +} + +export const resetFork = getResetFork(FORK_BLOCK) From f1dfdbc31fb84138315b0b06c16ae3bcec12ffa0 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Fri, 6 Sep 2024 23:07:12 -0400 Subject: [PATCH 02/23] fix and tweak tryPrice --- .../plugins/assets/aerodrome/AerodromeStableCollateral.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index 81311a800..27cecfb41 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -61,15 +61,15 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { uint192 r0 = shiftl_toFix(tokenReserve(0), -int8(token0.decimals()), FLOOR); uint192 r1 = shiftl_toFix(tokenReserve(1), -int8(token1.decimals()), FLOOR); uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); - uint192 sqrtK = (r0.sqrt()).mulDiv(r1.sqrt(), totalSupply); + uint192 sqrtK = r0.mul(r1).sqrt(); // get token prices (uint192 p0_low, uint192 p0_high) = tokenPrice(0); (uint192 p1_low, uint192 p1_high) = tokenPrice(1); // {UoA/tok} - low = sqrtK.mul(2).mul(((p0_low.mul(p1_low)).sqrt())); - high = sqrtK.mul(2).mul(((p0_high.mul(p1_high)).sqrt())); + low = sqrtK.mulu(2).mulDiv(p0_low.mul(p1_low).sqrt(), totalSupply); + high = sqrtK.mulu(2).mulDiv(p0_high.mul(p1_high).sqrt(), totalSupply); assert(low <= high); //obviously true just by inspection pegPrice = FIX_ONE; From b1f10dad5d3c979858c9ba1b21c6fe34ff2dd6f4 Mon Sep 17 00:00:00 2001 From: Julian R Date: Wed, 11 Sep 2024 08:58:22 -0300 Subject: [PATCH 03/23] apply revenue hiding and scripts --- common/configuration.ts | 1 + .../assets/aerodrome/AerodromePoolTokens.sol | 21 + .../aerodrome/AerodromeStableCollateral.sol | 93 ++-- scripts/deploy.ts | 3 +- .../collaterals/deploy_aerodrome_usdc_eusd.ts | 139 ++++++ .../verify_aerodrome_usdc_eusd.ts | 103 +++++ scripts/verify_etherscan.ts | 3 +- .../AerodromeStableCollateral.test.ts | 435 ++++++++++++++++++ .../aerodrome/constants.ts | 3 +- .../aerodrome/helpers.ts | 27 ++ 10 files changed, 789 insertions(+), 39 deletions(-) create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts create mode 100644 scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts create mode 100644 test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts diff --git a/common/configuration.ts b/common/configuration.ts index 91b59421c..58be27aed 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -143,6 +143,7 @@ export interface IPools { crvTriCrypto?: string crvMIM3Pool?: string sdUSDCUSDCPlus?: string + aeroUSDCeUSD?: string } interface INetworkConfig { diff --git a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol index 62a04bed8..426855954 100644 --- a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol +++ b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol @@ -180,6 +180,27 @@ contract AerodromePoolTokens { return pool.reserve1(); } + function sqrtK() public view virtual returns (uint192) { + uint192 r0 = shiftl_toFix(tokenReserve(0), -int8(token0.decimals()), FLOOR); + uint192 r1 = shiftl_toFix(tokenReserve(1), -int8(token1.decimals()), FLOOR); + return r0.mul(r1).sqrt(); + } + + /// @param index The index of the token: 0 or 1 + /// @return [address of chainlink feeds] + function tokenFeeds(uint8 index) public view virtual returns (AggregatorV3Interface[] memory) { + if (index >= nTokens) revert WrongIndex(nTokens - 1); + AggregatorV3Interface[] memory feeds = new AggregatorV3Interface[](2); + if (index == 0) { + feeds[0] = _t0feed0; + feeds[1] = _t0feed1; + } else { + feeds[0] = _t1feed0; + feeds[1] = _t1feed1; + } + return feeds; + } + // === Internal === function maxPoolOracleTimeout() internal view virtual returns (uint48) { diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index 27cecfb41..6b0b0674c 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -6,7 +6,7 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "contracts/interfaces/IAsset.sol"; import "contracts/libraries/Fixed.sol"; -import "contracts/plugins/assets/FiatCollateral.sol"; +import "contracts/plugins/assets/AppreciatingFiatCollateral.sol"; import "../../../interfaces/IRewardable.sol"; import "./AerodromePoolTokens.sol"; @@ -24,17 +24,18 @@ IERC20 constant AERO = IERC20(0x940181a94A35A4569E4529A3CDfB74e38FD98631); * UoA = USD * */ -contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { +contract AerodromeStableCollateral is AppreciatingFiatCollateral, AerodromePoolTokens { using OracleLib for AggregatorV3Interface; using FixLib for uint192; /// @dev config Unused members: chainlinkFeed, oracleError, oracleTimeout /// @dev No revenue hiding (refPerTok() == FIX_ONE) /// @dev config.erc20 should be an AerodromeStakingWrapper - constructor(CollateralConfig memory config, APTConfiguration memory aptConfig) - FiatCollateral(config) - AerodromePoolTokens(aptConfig) - { + constructor( + CollateralConfig memory config, + uint192 revenueHiding, + APTConfiguration memory aptConfig + ) AppreciatingFiatCollateral(config, revenueHiding) AerodromePoolTokens(aptConfig) { require(config.defaultThreshold != 0, "defaultThreshold zero"); maxOracleTimeout = uint48(Math.max(maxOracleTimeout, maxPoolOracleTimeout())); } @@ -57,19 +58,15 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { uint192 pegPrice ) { - // get reserves - uint192 r0 = shiftl_toFix(tokenReserve(0), -int8(token0.decimals()), FLOOR); - uint192 r1 = shiftl_toFix(tokenReserve(1), -int8(token1.decimals()), FLOOR); - uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); - uint192 sqrtK = r0.mul(r1).sqrt(); - // get token prices (uint192 p0_low, uint192 p0_high) = tokenPrice(0); (uint192 p1_low, uint192 p1_high) = tokenPrice(1); + uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); + // {UoA/tok} - low = sqrtK.mulu(2).mulDiv(p0_low.mul(p1_low).sqrt(), totalSupply); - high = sqrtK.mulu(2).mulDiv(p0_high.mul(p1_high).sqrt(), totalSupply); + low = sqrtK().mulu(2).mulDiv(p0_low.mul(p1_low).sqrt(), totalSupply); + high = sqrtK().mulu(2).mulDiv(p0_high.mul(p1_high).sqrt(), totalSupply); assert(low <= high); //obviously true just by inspection pegPrice = FIX_ONE; @@ -81,35 +78,53 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { function refresh() public virtual override { CollateralStatus oldStatus = status(); - // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { - // {UoA/tok}, {UoA/tok}, {UoA/tok} - // (0, 0) is a valid price; (0, FIX_MAX) is unpriced - - // Save prices if priced - if (high != FIX_MAX) { - savedLowPrice = low; - savedHighPrice = high; - savedPegPrice = pegPrice; - lastSave = uint48(block.timestamp); - } else { - // must be unpriced - // untested: - // validated in other plugins, cost to test here is high - assert(low == 0); + // Check for hard default + try this.underlyingRefPerTok() returns (uint192 underlyingRefPerTok_) { + // {ref/tok} = {ref/tok} * {1} + uint192 hiddenReferencePrice = underlyingRefPerTok_.mul(revenueShowing); + + // uint192(<) is equivalent to Fix.lt + if (underlyingRefPerTok_ < exposedReferencePrice) { + exposedReferencePrice = underlyingRefPerTok_; + markStatus(CollateralStatus.DISABLED); + } else if (hiddenReferencePrice > exposedReferencePrice) { + exposedReferencePrice = hiddenReferencePrice; } - // If the price is below the default-threshold price, default eventually - // uint192(+/-) is the same as Fix.plus/minus - if (low == 0 || _anyDepeggedInPool()) { + // Check for soft default + save prices + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { + // {UoA/tok}, {UoA/tok}, {UoA/tok} + // (0, 0) is a valid price; (0, FIX_MAX) is unpriced + + // Save prices if priced + if (high != FIX_MAX) { + savedLowPrice = low; + savedHighPrice = high; + savedPegPrice = pegPrice; + lastSave = uint48(block.timestamp); + } else { + // must be unpriced + // untested: + // validated in other plugins, cost to test here is high + assert(low == 0); + } + + // If the price is below the default-threshold price, default eventually + // uint192(+/-) is the same as Fix.plus/minus + if (low == 0 || _anyDepeggedInPool()) { + markStatus(CollateralStatus.IFFY); + } else { + markStatus(CollateralStatus.SOUND); + } + } catch (bytes memory errData) { + // see: docs/solidity-style.md#Catching-Empty-Data + if (errData.length == 0) revert(); // solhint-disable-line reason-string markStatus(CollateralStatus.IFFY); - } else { - markStatus(CollateralStatus.SOUND); } } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data if (errData.length == 0) revert(); // solhint-disable-line reason-string - markStatus(CollateralStatus.IFFY); + markStatus(CollateralStatus.DISABLED); } CollateralStatus newStatus = status(); @@ -126,6 +141,12 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { emit RewardsClaimed(AERO, AERO.balanceOf(address(this)) - aeroBal); } + /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens + function underlyingRefPerTok() public view virtual override returns (uint192) { + uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); + return sqrtK().mulu(2).mulDiv(FIX_ONE, totalSupply); + } + // === Internal === // Override this later to implement non-stable pools diff --git a/scripts/deploy.ts b/scripts/deploy.ts index cc2a51418..9f0387425 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -102,7 +102,8 @@ async function main() { 'phase2-assets/collaterals/deploy_aave_v3_usdc.ts', 'phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts', 'phase2-assets/collaterals/deploy_cbeth_collateral.ts', - 'phase2-assets/assets/deploy_stg.ts' + 'phase2-assets/assets/deploy_stg.ts', + 'phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts' ) } else if (chainId == '42161' || chainId == '421614') { // Arbitrum One diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts b/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts new file mode 100644 index 000000000..a0b866a57 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts @@ -0,0 +1,139 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../../common/configuration' +import { expect } from 'chai' +import { CollateralStatus, ONE_ADDRESS } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { AerodromeStableCollateral, AerodromeGaugeWrapper, IAeroPool } from '../../../../typechain' +import { combinedError, revenueHiding } from '../../utils' +import { + AerodromePoolType, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, + AERO_USDC_eUSD_POOL, + AERO_USDC_eUSD_GAUGE, + AERO, + eUSD_ORACLE_ERROR, + eUSD_ORACLE_TIMEOUT, + eUSD_USD_FEED, +} from '../../../../test/plugins/individual-collateral/aerodrome/constants' + +// Convex Stable Plugin: crvUSD-USDC + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Aerodrome Stable Pool for USDC-eUSD **************************/ + + let collateral: AerodromeStableCollateral + let wusdceusd: AerodromeGaugeWrapper + + // Only for Base + if (baseL2Chains.includes(hre.network.name)) { + const AerodromeStableCollateralFactory = await hre.ethers.getContractFactory( + 'AerodromeStableCollateral' + ) + const AerodromeGaugeWrapperFactory = await ethers.getContractFactory('AerodromeGaugeWrapper') + + // Deploy gauge wrapper + const pool = await ethers.getContractAt('IAeroPool', AERO_USDC_eUSD_POOL) + wusdceusd = ( + await AerodromeGaugeWrapperFactory.deploy( + pool.address, + 'w' + (await pool.name()), + 'w' + (await pool.symbol()), + AERO, + AERO_USDC_eUSD_GAUGE + ) + ) + await wusdceusd.deployed() + + console.log( + `Deployed wrapper for Aerodrome Stable USDC-eUSD pool on ${hre.network.name} (${chainId}): ${wusdceusd.address} ` + ) + + const oracleError = combinedError(USDC_ORACLE_ERROR, eUSD_ORACLE_ERROR) // 0.3% & 0.5% + + collateral = await AerodromeStableCollateralFactory.connect( + deployer + ).deploy( + { + erc20: wusdceusd.address, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: oracleError.toString(), // unused but cannot be zero + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + pool: AERO_USDC_eUSD_POOL, + poolType: AerodromePoolType.Stable, + feeds: [[USDC_USD_FEED], [eUSD_USD_FEED]], + oracleTimeouts: [[USDC_ORACLE_TIMEOUT], [eUSD_ORACLE_TIMEOUT]], + oracleErrors: [[USDC_ORACLE_ERROR], [eUSD_ORACLE_ERROR]], + } + ) + } else { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + `Deployed Aerodrome Stable Collateral for USDC-eUSD to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + + assetCollDeployments.collateral.aeroUSDCeUSD = collateral.address + assetCollDeployments.erc20s.aeroUSDCeUSD = wusdceusd.address + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts b/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts new file mode 100644 index 000000000..26a0f81d2 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts @@ -0,0 +1,103 @@ +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { baseL2Chains, developmentChains, networkConfig } from '../../../common/configuration' +import { ONE_ADDRESS } from '../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { combinedError, revenueHiding } from '../../deployment/utils' +import { IAeroPool } from '@typechain/IAeroPool' +import { + AerodromePoolType, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + AERO_USDC_eUSD_POOL, + AERO_USDC_eUSD_GAUGE, + AERO, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, + eUSD_ORACLE_ERROR, + eUSD_ORACLE_TIMEOUT, + eUSD_USD_FEED, +} from '../../../test/plugins/individual-collateral/aerodrome/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + // Only on base, aways use wrapper + if (baseL2Chains.includes(hre.network.name)) { + const aeroUSDCeUSDPoolCollateral = await ethers.getContractAt( + 'AerodromeStableCollateral', + deployments.collateral.aeroUSDCeUSD as string + ) + + /******** Verify Gauge Wrapper **************************/ + + const pool = await ethers.getContractAt('IAeroPool', AERO_USDC_eUSD_POOL) + await verifyContract( + chainId, + await aeroUSDCeUSDPoolCollateral.erc20(), + [ + pool.address, + 'w' + (await pool.name()), + 'w' + (await pool.symbol()), + AERO, + AERO_USDC_eUSD_GAUGE, + ], + 'contracts/plugins/assets/aerodrome/AerodromeGaugeWrapper.sol:AerodromeGaugeWrapper' + ) + + /******** Verify USDC-eUSD plugin **************************/ + const oracleError = combinedError(USDC_ORACLE_ERROR, eUSD_ORACLE_ERROR) // 0.3% & 0.5% + await verifyContract( + chainId, + deployments.collateral.aeroUSDCeUSD, + [ + { + erc20: await aeroUSDCeUSDPoolCollateral.erc20(), + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: oracleError.toString(), + oracleTimeout: USDC_ORACLE_TIMEOUT, // max of oracleTimeouts + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + pool: AERO_USDC_eUSD_POOL, + poolType: AerodromePoolType.Stable, + feeds: [[USDC_USD_FEED], [eUSD_USD_FEED]], + oracleTimeouts: [[USDC_ORACLE_TIMEOUT], [eUSD_ORACLE_TIMEOUT]], + oracleErrors: [[USDC_ORACLE_ERROR], [eUSD_ORACLE_ERROR]], + }, + ], + 'contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol:AerodromeStableCollateral' + ) + } +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index 55023fd71..fa5cea47c 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -88,7 +88,8 @@ async function main() { 'collateral-plugins/verify_aave_v3_usdc.ts', 'collateral-plugins/verify_wsteth.ts', 'collateral-plugins/verify_cbeth.ts', - 'assets/verify_stg.ts' + 'assets/verify_stg.ts', + 'collateral-plugins/verify_aerodrome_usdc_eusd.ts' ) } else if (chainId == '42161' || chainId == '421614') { // Arbitrum One diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts new file mode 100644 index 000000000..99fe43794 --- /dev/null +++ b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts @@ -0,0 +1,435 @@ +import collateralTests from '../collateralTests' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + IAeroPool, + MockV3Aggregator, + MockV3Aggregator__factory, + AerodromeGaugeWrapper__factory, + TestICollateral, + AerodromeGaugeWrapper, + ERC20Mock, +} from '../../../../typechain' +import { networkConfig } from '../../../../common/configuration' +import { ZERO_ADDRESS } from '#/common/constants' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + AerodromePoolType, + USDC_USD_FEED, + USDC_HOLDER, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + PRICE_TIMEOUT, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + AERO_USDC_eUSD_POOL, + AERO_USDC_eUSD_GAUGE, + AERO_USDC_eUSD_HOLDER, + AERO, + USDC, + eUSD, + eUSD_HOLDER, + eUSD_USD_FEED, + eUSD_ORACLE_ERROR, + eUSD_ORACLE_TIMEOUT, + ORACLE_ERROR, +} from './constants' +import { expectPrice } from '../../../utils/oracles' +import { mintWrappedLpToken, resetFork, getFeeds, pushAllFeedsForward } from './helpers' +import BigNumber from 'bignumber.js' + +/* + Define interfaces +*/ + +interface AeroPoolTokenConfig { + token: string + feeds: string[] + oracleTimeouts: BigNumberish[] + oracleErrors: BigNumberish[] + holder: string +} + +interface AeroStablePoolEnumeration { + testName: string + pool: string + gauge: string + holder: string + toleranceDivisor: BigNumber + amountScaleDivisor: BigNumber + tokens: AeroPoolTokenConfig[] + oracleTimeout: BigNumberish + oracleError: BigNumberish +} + +interface AeroStableCollateralOpts extends CollateralOpts { + pool?: string + poolType?: AerodromePoolType + gauge?: string + feeds?: string[][] + oracleTimeouts?: BigNumberish[][] + oracleErrors?: BigNumberish[][] +} + +interface AerodromeCollateralFixtureContext extends CollateralFixtureContext { + feeds?: string[][] +} + +// ==== + +const config = networkConfig['8453'] // use Base fork + +// Test all Aerodrome Stable pools +const all = [ + { + testName: 'Aerodrome - USDC/eUSD Stable', + pool: AERO_USDC_eUSD_POOL, + gauge: AERO_USDC_eUSD_GAUGE, + holder: AERO_USDC_eUSD_HOLDER, + tokens: [ + { + token: USDC, + feeds: [USDC_USD_FEED], + oracleTimeouts: [USDC_ORACLE_TIMEOUT], + oracleErrors: [USDC_ORACLE_ERROR], + holder: USDC_HOLDER, + }, + { + token: eUSD, + feeds: [eUSD_USD_FEED], + oracleTimeouts: [eUSD_ORACLE_TIMEOUT], + oracleErrors: [eUSD_ORACLE_ERROR], + holder: eUSD_HOLDER, + }, + ], + oracleTimeout: PRICE_TIMEOUT, // max + oracleError: ORACLE_ERROR, // combined + amountScaleDivisor: bn('1e2'), + toleranceDivisor: bn('1e2'), + }, +] + +all.forEach((curr: AeroStablePoolEnumeration) => { + const defaultCollateralOpts: AeroStableCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: curr.tokens[0].feeds[0], // unused but cannot be zero + oracleTimeout: curr.oracleTimeout, // max of oracleTimeouts + oracleError: curr.oracleError, // combined oracle error + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + pool: curr.pool, + poolType: AerodromePoolType.Stable, + gauge: curr.gauge, + feeds: [curr.tokens[0].feeds, curr.tokens[1].feeds], + oracleTimeouts: [curr.tokens[0].oracleTimeouts, curr.tokens[1].oracleTimeouts], + oracleErrors: [curr.tokens[0].oracleErrors, curr.tokens[1].oracleErrors], + } + + const deployCollateral = async ( + opts: AeroStableCollateralOpts = {} + ): Promise => { + let pool: IAeroPool + let wrapper: AerodromeGaugeWrapper + + if (!opts.erc20) { + const AerodromGaugeWrapperFactory = ( + await ethers.getContractFactory('AerodromeGaugeWrapper') + ) + + // Create wrapper + pool = await ethers.getContractAt('IAeroPool', curr.pool) + + wrapper = await AerodromGaugeWrapperFactory.deploy( + pool.address, + 'w' + (await pool.name()), + 'w' + (await pool.symbol()), + AERO, + curr.gauge + ) + + opts.erc20 = wrapper.address + } + + opts = { ...defaultCollateralOpts, ...opts } + + const AeroStableCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'AerodromeStableCollateral' + ) + + const collateral = await AeroStableCollateralFactory.deploy( + { + erc20: opts.erc20, + targetName: opts.targetName, + priceTimeout: opts.priceTimeout, + chainlinkFeed: opts.chainlinkFeed, + oracleError: opts.oracleError, + oracleTimeout: opts.oracleTimeout, + maxTradeVolume: opts.maxTradeVolume, + defaultThreshold: opts.defaultThreshold, + delayUntilDefault: opts.delayUntilDefault, + }, + bn(0), + { + pool: opts.pool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + }, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + + // Push forward chainlink feeds + await pushAllFeedsForward(collateral) + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral + } + + type Fixture = () => Promise + + const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: AeroStableCollateralOpts = {} + ): Fixture => { + const collateralOpts = { ...defaultCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute both feeds + const token0Feed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateralOpts.chainlinkFeed = token0Feed.address + + const token1Feed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateralOpts.feeds = [[token0Feed.address], [token1Feed.address]] + + const pool = await ethers.getContractAt('IAeroPool', curr.pool) + + const AerodromeGaugeWrapperFactory = ( + await ethers.getContractFactory('AerodromeGaugeWrapper') + ) + + const wrapper = await AerodromeGaugeWrapperFactory.deploy( + pool.address, + 'w' + (await pool.name()), + 'w' + (await pool.symbol()), + AERO, + curr.gauge + ) + + collateralOpts.erc20 = wrapper.address + + const collateral = await deployCollateral(collateralOpts) + const erc20 = await ethers.getContractAt( + 'AerodromeGaugeWrapper', + (await collateral.erc20()) as string + ) + + const rewardToken = await ethers.getContractAt('ERC20Mock', AERO) + + return { + alice, + collateral, + chainlinkFeed: token0Feed, + tok: erc20, + rewardToken, + } + } + + return makeCollateralFixtureContext + } + + /* + Define helper functions +*/ + + const mintCollateralTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string + ) => { + const gauge = await ethers.getContractAt('IAeroGauge', curr.gauge) + const pool = await ethers.getContractAt('IAeroPool', curr.pool) + + await mintWrappedLpToken( + ctx.tok as AerodromeGaugeWrapper, + gauge, + pool, + amount, + curr.holder, + user, + recipient + ) + } + + const reduceTargetPerRef = async (ctx: CollateralFixtureContext, pctDecrease: BigNumberish) => { + const allFeeds = await getFeeds(ctx.collateral) + const initialPrices = await Promise.all(allFeeds.map((f) => f.latestRoundData())) + for (const [i, feed] of allFeeds.entries()) { + const nextAnswer = initialPrices[i].answer.sub( + initialPrices[i].answer.mul(pctDecrease).div(100) + ) + await feed.updateAnswer(nextAnswer) + } + } + + const increaseTargetPerRef = async (ctx: CollateralFixtureContext, pctIncrease: BigNumberish) => { + // Update values in Oracles increase by 10% + const allFeeds = await getFeeds(ctx.collateral) + const initialPrices = await Promise.all(allFeeds.map((f) => f.latestRoundData())) + for (const [i, feed] of allFeeds.entries()) { + const nextAnswer = initialPrices[i].answer.add( + initialPrices[i].answer.mul(pctIncrease).div(100) + ) + await feed.updateAnswer(nextAnswer) + } + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + const increaseRefPerTok = async (ctx: CollateralFixtureContext, pctIncrease: BigNumberish) => {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + const reduceRefPerTok = async (ctx: CollateralFixtureContext, pctDecrease: BigNumberish) => {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + const collateralSpecificConstructorTests = () => {} + + const collateralSpecificStatusTests = () => { + it('prices change as feed price changes', async () => { + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const feed0 = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const feed1 = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const coll = await deployCollateral({ + pool: curr.pool, + gauge: curr.gauge, + feeds: [[feed0.address], [feed1.address]], + }) + + const initialRefPerTok = await coll.refPerTok() + const [low, high] = await coll.price() + + // Update values in Oracles increase by 10% + const allFeeds = await getFeeds(coll) + const initialPrices = await Promise.all(allFeeds.map((f) => f.latestRoundData())) + for (const [i, feed] of allFeeds.entries()) { + await feed.updateAnswer(initialPrices[i].answer.mul(110).div(100)).then((e) => e.wait()) + } + + const [newLow, newHigh] = await coll.price() + + // with 18 decimals of price precision a 1e-9 tolerance seems fine for a 10% change + // and without this kind of tolerance the Volatile pool tests fail due to small movements + expect(newLow).to.be.closeTo(low.mul(110).div(100), fp('1e-9')) + expect(newHigh).to.be.closeTo(high.mul(110).div(100), fp('1e-9')) + + // Check refPerTok remains the same + const finalRefPerTok = await coll.refPerTok() + expect(finalRefPerTok).to.equal(initialRefPerTok) + }) + + it('prices change as targetPerRef changes', async () => { + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const feed0 = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const feed1 = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const coll = await deployCollateral({ + pool: curr.pool, + gauge: curr.gauge, + feeds: [[feed0.address], [feed1.address]], + }) + + const tok = await ethers.getContractAt('IERC20Metadata', await coll.erc20()) + const tempCtx = { collateral: coll, chainlinkFeed: feed0, tok } + + const oracleError = await coll.oracleError() + const expectedPrice = await getExpectedPrice(tempCtx) + await expectPrice(coll.address, expectedPrice, oracleError, true, curr.toleranceDivisor) + + // Get refPerTok initial values + const initialRefPerTok = await coll.refPerTok() + const [oldLow, oldHigh] = await coll.price() + + // Update values in Oracles increase by 10-20% + await increaseTargetPerRef(tempCtx, 20) + + // Check new prices -- increase expected + const newPrice = await getExpectedPrice(tempCtx) + await expectPrice(coll.address, newPrice, oracleError, true, curr.toleranceDivisor) + const [newLow, newHigh] = await coll.price() + expect(oldLow).to.be.lt(newLow) + expect(oldHigh).to.be.lt(newHigh) + + // Check refPerTok remains the same (because we have not refreshed) + const finalRefPerTok = await coll.refPerTok() + expect(finalRefPerTok).to.equal(initialRefPerTok) + }) + + // eslint-disable-next-line @typescript-eslint/no-empty-function + it.skip('prices change as refPerTok changes', async () => {}) + } + + const getExpectedPrice = async (ctx: CollateralFixtureContext) => { + // TODO: Improve use both feeds + const initRefPerTok = await ctx.collateral.refPerTok() + const decimals = await ctx.chainlinkFeed.decimals() + const initData = await ctx.chainlinkFeed.latestRoundData() + return initData.answer + .mul(bn(10).pow(18 - decimals)) + .mul(initRefPerTok) + .div(fp('1')) + } + + /* + Run the test suite + */ + + const emptyFn = () => { + return + } + + const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest: emptyFn, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it, + itChecksTargetPerRefDefault: it, + itChecksTargetPerRefDefaultUp: it, + itChecksRefPerTokDefault: it.skip, + itChecksPriceChanges: it.skip, + itChecksNonZeroDefaultThreshold: it, + itHasRevenueHiding: it.skip, + resetFork, + collateralName: curr.testName, + chainlinkDefaultAnswer: bn('1e8'), + itIsPricedByPeg: true, + toleranceDivisor: curr.toleranceDivisor, + amountScaleDivisor: curr.amountScaleDivisor, + targetNetwork: 'base', + } + + collateralTests(opts) +}) diff --git a/test/plugins/individual-collateral/aerodrome/constants.ts b/test/plugins/individual-collateral/aerodrome/constants.ts index 720b5f1fc..7c468868b 100644 --- a/test/plugins/individual-collateral/aerodrome/constants.ts +++ b/test/plugins/individual-collateral/aerodrome/constants.ts @@ -25,10 +25,11 @@ export const eUSD_ORACLE_TIMEOUT = bn('86400') export const eUSD_ORACLE_ERROR = fp('0.005') export const eUSD_HOLDER = '0xb5E331615FdbA7DF49e05CdEACEb14Acdd5091c3' -export const FORK_BLOCK = 19074000 +export const FORK_BLOCK = 19074500 // Common export const FIX_ONE = 1n * 10n ** 18n +export const ORACLE_ERROR = fp('0.005') export const PRICE_TIMEOUT = bn('604800') // 1 week export const DEFAULT_THRESHOLD = fp('0.02') // 2% export const DELAY_UNTIL_DEFAULT = bn('259200') // 72h diff --git a/test/plugins/individual-collateral/aerodrome/helpers.ts b/test/plugins/individual-collateral/aerodrome/helpers.ts index 266515a55..5a2de4312 100644 --- a/test/plugins/individual-collateral/aerodrome/helpers.ts +++ b/test/plugins/individual-collateral/aerodrome/helpers.ts @@ -4,6 +4,8 @@ import { IAeroGauge, AerodromeGaugeWrapper__factory, AerodromeGaugeWrapper, + TestICollateral, + MockV3Aggregator, } from '@typechain/index' import { ethers } from 'hardhat' import { @@ -15,7 +17,9 @@ import { FORK_BLOCK, } from './constants' import { getResetFork } from '../helpers' +import { pushOracleForward } from '../../../utils/oracles' import { whileImpersonating } from '#/test/utils/impersonation' +import { ZERO_ADDRESS } from '#/common/constants' interface WrappedAeroFixture { token0: ERC20Mock @@ -73,4 +77,27 @@ export const mintWrappedLpToken = async ( await wrapper.connect(user).deposit(amount, recipient) } +export const getFeeds = async (coll: TestICollateral): Promise => { + const aeroStableColl = await ethers.getContractAt('AerodromeStableCollateral', coll.address) + + const feedAddrs = (await aeroStableColl.tokenFeeds(0)).concat(await aeroStableColl.tokenFeeds(1)) + const feeds: MockV3Aggregator[] = [] + + for (const feedAddr of feedAddrs) { + if (feedAddr != ZERO_ADDRESS) { + const oracle = await ethers.getContractAt('MockV3Aggregator', feedAddr) + feeds.push(oracle) + } + } + + return feeds +} + +export const pushAllFeedsForward = async (coll: TestICollateral) => { + const feeds = await getFeeds(coll) + for (const oracle of feeds) { + await pushOracleForward(oracle.address) + } +} + export const resetFork = getResetFork(FORK_BLOCK) From 6b7be0e5edb50f93b393bad5b2bed2ccfa33ab9f Mon Sep 17 00:00:00 2001 From: Julian R Date: Thu, 12 Sep 2024 11:51:49 -0300 Subject: [PATCH 04/23] apply correct k formula for stable pools --- .../plugins/assets/aerodrome/AerodromePoolTokens.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol index 426855954..06a7d25c2 100644 --- a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol +++ b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol @@ -26,6 +26,7 @@ contract AerodromePoolTokens { // === State (Immutable) === IAeroPool public immutable pool; + AeroPoolType public immutable poolType; IERC20Metadata internal immutable token0; IERC20Metadata internal immutable token1; @@ -66,6 +67,7 @@ contract AerodromePoolTokens { require(address(config.pool) != address(0), "pool address is zero"); pool = config.pool; + poolType = config.poolType; // Solidity does not support immutable arrays. This is a hack to get the equivalent of // an immutable array so we do not have store the token feeds in the blockchain. This is @@ -181,8 +183,15 @@ contract AerodromePoolTokens { } function sqrtK() public view virtual returns (uint192) { + // x3y+y3x >= k for sAMM pools + // xy >= k for vAMM pools uint192 r0 = shiftl_toFix(tokenReserve(0), -int8(token0.decimals()), FLOOR); uint192 r1 = shiftl_toFix(tokenReserve(1), -int8(token1.decimals()), FLOOR); + if (poolType == AeroPoolType.Stable) { + uint192 _a = r0.mul(r1); + uint192 _b = (r0.mul(r0)).plus(r1.mul(r1)); + return _a.mul(_b).sqrt(); + } return r0.mul(r1).sqrt(); } From f63fb4ed7851cb4921cd709bde15a377ffe4c768 Mon Sep 17 00:00:00 2001 From: Julian R Date: Thu, 19 Sep 2024 10:27:35 -0300 Subject: [PATCH 05/23] temp testing changes --- .../individual-collateral/collateralTests.ts | 23 ++++++++++++++----- .../individual-collateral/pluginTestTypes.ts | 3 +++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 8dbce3ee9..394588283 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -93,6 +93,7 @@ export default function fn( resetFork, collateralName, chainlinkDefaultAnswer, + amountScaleDivisor, toleranceDivisor, targetNetwork, } = fixtures @@ -172,7 +173,8 @@ export default function fn( describe('functions', () => { it('returns the correct bal (18 decimals)', async () => { const decimals = await ctx.tok.decimals() - const amount = bn('20').mul(bn(10).pow(decimals)) + const scaleDivisor = amountScaleDivisor ?? bn(1) + const amount = bn('20').mul(bn(10).pow(decimals)).div(scaleDivisor) await mintCollateralTo(ctx, amount, alice, alice.address) const aliceBal = await collateral.bal(alice.address) @@ -195,7 +197,10 @@ export default function fn( }) itClaimsRewards('claims rewards (via collateral.claimRewards())', async () => { - const amount = bn('20').mul(bn(10).pow(await ctx.tok.decimals())) + const scaleDivisor = amountScaleDivisor ?? bn(1) + const amount = bn('20') + .mul(bn(10).pow(await ctx.tok.decimals())) + .div(scaleDivisor) await mintCollateralTo(ctx, amount, alice, ctx.collateral.address) await advanceBlocks(1000) await advanceToTimestamp((await getLatestBlockTimestamp()) + 12000) @@ -626,7 +631,7 @@ export default function fn( }) }) - describe('integration tests', () => { + describe.only('integration tests', () => { const onBase = useEnv('FORK_NETWORK').toLowerCase() == 'base' const onArbitrum = useEnv('FORK_NETWORK').toLowerCase() == 'arbitrum' @@ -662,6 +667,8 @@ export default function fn( let govParams: IGovParams let govRoles: IGovRoles + let scaleDivisor: BigNumber + const config = { dist: { rTokenDist: bn(0), // 0% RToken @@ -715,6 +722,7 @@ export default function fn( throw new Error(`Missing network configuration for ${hre.network.name}`) } ;[, owner, addr1] = await ethers.getSigners() + scaleDivisor = amountScaleDivisor ?? bn(1) }) beforeEach(async () => { @@ -735,7 +743,7 @@ export default function fn( collateralERC20 = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) await mintCollateralTo( ctx, - toBNDecimals(fp('1'), await collateralERC20.decimals()), + toBNDecimals(fp('1').div(scaleDivisor), await collateralERC20.decimals()), addr1, addr1.address ) @@ -823,7 +831,10 @@ export default function fn( it('redeems', async () => { await rToken.connect(addr1).redeem(supply) expect(await rToken.totalSupply()).to.equal(0) - const initialCollBal = toBNDecimals(fp('1'), await collateralERC20.decimals()) + const initialCollBal = toBNDecimals( + fp('1').div(scaleDivisor), + await collateralERC20.decimals() + ) expect(await collateralERC20.balanceOf(addr1.address)).to.be.closeTo( initialCollBal, initialCollBal.div(bn('1e5')) // 1-part-in-100k @@ -867,7 +878,7 @@ export default function fn( const router = await (await ethers.getContractFactory('DutchTradeRouter')).deploy() await rsr.connect(addr1).approve(router.address, MAX_UINT256) // Send excess collateral to the RToken trader via forwardRevenue() - let mintAmt = toBNDecimals(fp('1e-6'), await collateralERC20.decimals()) + let mintAmt = toBNDecimals(fp('1e-6'), await collateralERC20.decimals()).div(scaleDivisor) mintAmt = mintAmt.gt('100000') ? mintAmt : bn('100000') // fewest tokens distributor will transfer await mintCollateralTo(ctx, mintAmt, addr1, backingManager.address) await backingManager.forwardRevenue([collateralERC20.address]) diff --git a/test/plugins/individual-collateral/pluginTestTypes.ts b/test/plugins/individual-collateral/pluginTestTypes.ts index aa70a23c1..837e5a258 100644 --- a/test/plugins/individual-collateral/pluginTestTypes.ts +++ b/test/plugins/individual-collateral/pluginTestTypes.ts @@ -121,6 +121,9 @@ export interface CollateralTestSuiteFixtures // the default answer that will come from the chainlink feed after deployment chainlinkDefaultAnswer: BigNumberish + // the scale divisor that will be used for amounts in tests + amountScaleDivisor?: BigNumber + // the default tolerance divisor that will be used in expectPrice checks toleranceDivisor?: BigNumber From 4ca20a4ff757a3c3722365bb6c7194b632ed53fd Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 20 Sep 2024 13:34:16 -0300 Subject: [PATCH 06/23] implement new function and set debugging --- .../assets/aerodrome/AerodromePoolTokens.sol | 13 -- .../aerodrome/AerodromeStableCollateral.sol | 125 ++++++++++-------- .../AerodromeStableCollateral.test.ts | 27 ++-- .../individual-collateral/collateralTests.ts | 2 +- 4 files changed, 86 insertions(+), 81 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol index 06a7d25c2..001422fe3 100644 --- a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol +++ b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol @@ -182,19 +182,6 @@ contract AerodromePoolTokens { return pool.reserve1(); } - function sqrtK() public view virtual returns (uint192) { - // x3y+y3x >= k for sAMM pools - // xy >= k for vAMM pools - uint192 r0 = shiftl_toFix(tokenReserve(0), -int8(token0.decimals()), FLOOR); - uint192 r1 = shiftl_toFix(tokenReserve(1), -int8(token1.decimals()), FLOOR); - if (poolType == AeroPoolType.Stable) { - uint192 _a = r0.mul(r1); - uint192 _b = (r0.mul(r0)).plus(r1.mul(r1)); - return _a.mul(_b).sqrt(); - } - return r0.mul(r1).sqrt(); - } - /// @param index The index of the token: 0 or 1 /// @return [address of chainlink feeds] function tokenFeeds(uint8 index) public view virtual returns (AggregatorV3Interface[] memory) { diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index 6b0b0674c..d9c24d0e7 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -6,10 +6,12 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "contracts/interfaces/IAsset.sol"; import "contracts/libraries/Fixed.sol"; -import "contracts/plugins/assets/AppreciatingFiatCollateral.sol"; +import "contracts/plugins/assets/FiatCollateral.sol"; import "../../../interfaces/IRewardable.sol"; import "./AerodromePoolTokens.sol"; +import "hardhat/console.sol"; + // This plugin only works on Base IERC20 constant AERO = IERC20(0x940181a94A35A4569E4529A3CDfB74e38FD98631); @@ -19,12 +21,12 @@ IERC20 constant AERO = IERC20(0x940181a94A35A4569E4529A3CDfB74e38FD98631); * Each token in the pool can have between 1 and 2 oracles per each token. * * tok = AerodromeStakingWrapper(stablePool) - * ref = 1e18 (fixed) + * ref = toFix(2) * tar = USD * UoA = USD * */ -contract AerodromeStableCollateral is AppreciatingFiatCollateral, AerodromePoolTokens { +contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { using OracleLib for AggregatorV3Interface; using FixLib for uint192; @@ -33,9 +35,8 @@ contract AerodromeStableCollateral is AppreciatingFiatCollateral, AerodromePoolT /// @dev config.erc20 should be an AerodromeStakingWrapper constructor( CollateralConfig memory config, - uint192 revenueHiding, APTConfiguration memory aptConfig - ) AppreciatingFiatCollateral(config, revenueHiding) AerodromePoolTokens(aptConfig) { + ) FiatCollateral(config) AerodromePoolTokens(aptConfig) { require(config.defaultThreshold != 0, "defaultThreshold zero"); maxOracleTimeout = uint48(Math.max(maxOracleTimeout, maxPoolOracleTimeout())); } @@ -52,21 +53,50 @@ contract AerodromeStableCollateral is AppreciatingFiatCollateral, AerodromePoolT view virtual override - returns ( - uint192 low, - uint192 high, - uint192 pegPrice - ) + returns (uint192 low, uint192 high, uint192 pegPrice) { + uint256 r0 = tokenReserve(0); + uint256 r1 = tokenReserve(1); + + console.log("reserves0: %s", r0); + console.log("reserves1: %s", r1); + + // x3y+y3x >= k for sAMM pools + uint256 sqrtReserve = sqrt256(sqrt256(r0 * r1) * sqrt256(r0 * r0 + r1 * r1)); + + console.log("sqrtReserve: %s", sqrtReserve); + // get token prices (uint192 p0_low, uint192 p0_high) = tokenPrice(0); (uint192 p1_low, uint192 p1_high) = tokenPrice(1); + console.log("p0_low: %s", p0_low); + console.log("p0_high: %s", p0_high); + console.log("p1_low: %s", p1_low); + console.log("p1_high: %s", p1_high); + uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); - // {UoA/tok} - low = sqrtK().mulu(2).mulDiv(p0_low.mul(p1_low).sqrt(), totalSupply); - high = sqrtK().mulu(2).mulDiv(p0_high.mul(p1_high).sqrt(), totalSupply); + console.log("total supply raw: %s", pool.totalSupply()); + console.log("total supply: %s", totalSupply); + + // low + uint256 ratioLow = ((1e18) * p0_low) / p1_low; + uint256 sqrtPriceLow = sqrt256( + sqrt256((1e18) * ratioLow) * sqrt256(1e36 + ratioLow * ratioLow) + ); + low = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceLow) * p0_low * 2) / totalSupply); + + console.log("low: %s", low); + + // high + uint256 ratioHigh = ((1e18) * p0_high) / p1_high; + uint256 sqrtPriceHigh = sqrt256( + sqrt256((1e18) * ratioHigh) * sqrt256(1e36 + ratioHigh * ratioHigh) + ); + high = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceHigh) * p0_high * 2) / totalSupply); + + console.log("high: %s", high); assert(low <= high); //obviously true just by inspection pegPrice = FIX_ONE; @@ -78,53 +108,35 @@ contract AerodromeStableCollateral is AppreciatingFiatCollateral, AerodromePoolT function refresh() public virtual override { CollateralStatus oldStatus = status(); - // Check for hard default - try this.underlyingRefPerTok() returns (uint192 underlyingRefPerTok_) { - // {ref/tok} = {ref/tok} * {1} - uint192 hiddenReferencePrice = underlyingRefPerTok_.mul(revenueShowing); - - // uint192(<) is equivalent to Fix.lt - if (underlyingRefPerTok_ < exposedReferencePrice) { - exposedReferencePrice = underlyingRefPerTok_; - markStatus(CollateralStatus.DISABLED); - } else if (hiddenReferencePrice > exposedReferencePrice) { - exposedReferencePrice = hiddenReferencePrice; + // Check for soft default + save prices + try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { + // {UoA/tok}, {UoA/tok}, {UoA/tok} + // (0, 0) is a valid price; (0, FIX_MAX) is unpriced + + // Save prices if priced + if (high != FIX_MAX) { + savedLowPrice = low; + savedHighPrice = high; + savedPegPrice = pegPrice; + lastSave = uint48(block.timestamp); + } else { + // must be unpriced + // untested: + // validated in other plugins, cost to test here is high + assert(low == 0); } - // Check for soft default + save prices - try this.tryPrice() returns (uint192 low, uint192 high, uint192 pegPrice) { - // {UoA/tok}, {UoA/tok}, {UoA/tok} - // (0, 0) is a valid price; (0, FIX_MAX) is unpriced - - // Save prices if priced - if (high != FIX_MAX) { - savedLowPrice = low; - savedHighPrice = high; - savedPegPrice = pegPrice; - lastSave = uint48(block.timestamp); - } else { - // must be unpriced - // untested: - // validated in other plugins, cost to test here is high - assert(low == 0); - } - - // If the price is below the default-threshold price, default eventually - // uint192(+/-) is the same as Fix.plus/minus - if (low == 0 || _anyDepeggedInPool()) { - markStatus(CollateralStatus.IFFY); - } else { - markStatus(CollateralStatus.SOUND); - } - } catch (bytes memory errData) { - // see: docs/solidity-style.md#Catching-Empty-Data - if (errData.length == 0) revert(); // solhint-disable-line reason-string + // If the price is below the default-threshold price, default eventually + // uint192(+/-) is the same as Fix.plus/minus + if (low == 0 || _anyDepeggedInPool()) { markStatus(CollateralStatus.IFFY); + } else { + markStatus(CollateralStatus.SOUND); } } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data if (errData.length == 0) revert(); // solhint-disable-line reason-string - markStatus(CollateralStatus.DISABLED); + markStatus(CollateralStatus.IFFY); } CollateralStatus newStatus = status(); @@ -142,9 +154,10 @@ contract AerodromeStableCollateral is AppreciatingFiatCollateral, AerodromePoolT } /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens - function underlyingRefPerTok() public view virtual override returns (uint192) { - uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); - return sqrtK().mulu(2).mulDiv(FIX_ONE, totalSupply); + function refPerTok() public view virtual override returns (uint192) { + // TODO: Review case of negative offset + uint8 decimalOffset = token0.decimals() + token1.decimals() - 18; + return toFix(2).mulu(10 ** decimalOffset); } // === Internal === diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts index 99fe43794..5032b189c 100644 --- a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts +++ b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts @@ -175,7 +175,6 @@ all.forEach((curr: AeroStablePoolEnumeration) => { defaultThreshold: opts.defaultThreshold, delayUntilDefault: opts.delayUntilDefault, }, - bn(0), { pool: opts.pool, poolType: opts.poolType, @@ -334,7 +333,6 @@ all.forEach((curr: AeroStablePoolEnumeration) => { const [newLow, newHigh] = await coll.price() // with 18 decimals of price precision a 1e-9 tolerance seems fine for a 10% change - // and without this kind of tolerance the Volatile pool tests fail due to small movements expect(newLow).to.be.closeTo(low.mul(110).div(100), fp('1e-9')) expect(newHigh).to.be.closeTo(high.mul(110).div(100), fp('1e-9')) @@ -375,22 +373,29 @@ all.forEach((curr: AeroStablePoolEnumeration) => { expect(oldLow).to.be.lt(newLow) expect(oldHigh).to.be.lt(newHigh) - // Check refPerTok remains the same (because we have not refreshed) + // Check refPerTok remains the same const finalRefPerTok = await coll.refPerTok() expect(finalRefPerTok).to.equal(initialRefPerTok) }) - - // eslint-disable-next-line @typescript-eslint/no-empty-function - it.skip('prices change as refPerTok changes', async () => {}) } const getExpectedPrice = async (ctx: CollateralFixtureContext) => { - // TODO: Improve use both feeds const initRefPerTok = await ctx.collateral.refPerTok() - const decimals = await ctx.chainlinkFeed.decimals() - const initData = await ctx.chainlinkFeed.latestRoundData() - return initData.answer - .mul(bn(10).pow(18 - decimals)) + const coll = await ethers.getContractAt('AerodromeStableCollateral', ctx.collateral.address) + + const feed0 = await ethers.getContractAt('MockV3Aggregator', (await coll.tokenFeeds(0))[0]) + const decimals0 = await feed0.decimals() + const initData0 = await feed0.latestRoundData() + + const feed1 = await ethers.getContractAt('MockV3Aggregator', (await coll.tokenFeeds(1))[0]) + const decimals1 = await feed1.decimals() + const initData1 = await feed1.latestRoundData() + + return initData0.answer + .mul(bn(10).pow(18 - decimals0)) + .mul(initData1.answer) + .mul(bn(10).pow(18 - decimals1)) + .div(fp('1')) .mul(initRefPerTok) .div(fp('1')) } diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index 394588283..fa7ae3a1e 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -631,7 +631,7 @@ export default function fn( }) }) - describe.only('integration tests', () => { + describe('integration tests', () => { const onBase = useEnv('FORK_NETWORK').toLowerCase() == 'base' const onArbitrum = useEnv('FORK_NETWORK').toLowerCase() == 'arbitrum' From 00664266dbb5d3de37df47603e05a79fd7d4a919 Mon Sep 17 00:00:00 2001 From: Akshat Mittal Date: Fri, 20 Sep 2024 23:37:56 +0530 Subject: [PATCH 07/23] Tiny change --- .../assets/aerodrome/AerodromePoolTokens.sol | 8 +++++-- .../aerodrome/AerodromeStableCollateral.sol | 24 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol index 001422fe3..15bb4f15b 100644 --- a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol +++ b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol @@ -178,8 +178,12 @@ contract AerodromePoolTokens { /// @return [{ref_index}] function tokenReserve(uint8 index) public view virtual returns (uint256) { if (index >= nTokens) revert WrongIndex(nTokens - 1); - if (index == 0) return pool.reserve0(); - return pool.reserve1(); + // Maybe also cache token decimals as immutable? + IERC20Metadata tokenInterface = getToken(index); + if (index == 0) { + return shiftl_toFix(pool.reserve0(), -int8(tokenInterface.decimals()), FLOOR); + } + return shiftl_toFix(pool.reserve1(), -int8(tokenInterface.decimals()), FLOOR); } /// @param index The index of the token: 0 or 1 diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index d9c24d0e7..047f3fff1 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -33,10 +33,10 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { /// @dev config Unused members: chainlinkFeed, oracleError, oracleTimeout /// @dev No revenue hiding (refPerTok() == FIX_ONE) /// @dev config.erc20 should be an AerodromeStakingWrapper - constructor( - CollateralConfig memory config, - APTConfiguration memory aptConfig - ) FiatCollateral(config) AerodromePoolTokens(aptConfig) { + constructor(CollateralConfig memory config, APTConfiguration memory aptConfig) + FiatCollateral(config) + AerodromePoolTokens(aptConfig) + { require(config.defaultThreshold != 0, "defaultThreshold zero"); maxOracleTimeout = uint48(Math.max(maxOracleTimeout, maxPoolOracleTimeout())); } @@ -53,7 +53,11 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { view virtual override - returns (uint192 low, uint192 high, uint192 pegPrice) + returns ( + uint192 low, + uint192 high, + uint192 pegPrice + ) { uint256 r0 = tokenReserve(0); uint256 r1 = tokenReserve(1); @@ -80,12 +84,12 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { console.log("total supply raw: %s", pool.totalSupply()); console.log("total supply: %s", totalSupply); - // low - uint256 ratioLow = ((1e18) * p0_low) / p1_low; - uint256 sqrtPriceLow = sqrt256( + // low + uint256 ratioLow = ((1e18) * p0_low) / p1_low; + uint256 sqrtPriceLow = sqrt256( sqrt256((1e18) * ratioLow) * sqrt256(1e36 + ratioLow * ratioLow) ); - low = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceLow) * p0_low * 2) / totalSupply); + low = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceLow) * p0_low * 2) / totalSupply); console.log("low: %s", low); @@ -157,7 +161,7 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { function refPerTok() public view virtual override returns (uint192) { // TODO: Review case of negative offset uint8 decimalOffset = token0.decimals() + token1.decimals() - 18; - return toFix(2).mulu(10 ** decimalOffset); + return toFix(2).mulu(10**decimalOffset); } // === Internal === From 5870acdcc22dc4e7128db1bf5925540a92a8487c Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Fri, 20 Sep 2024 21:59:09 -0400 Subject: [PATCH 08/23] refPerTok() --- .../assets/aerodrome/AerodromeStableCollateral.sol | 12 +++++++----- .../aerodrome/AerodromeStableCollateral.test.ts | 9 ++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index 047f3fff1..bd812c8d5 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -21,7 +21,7 @@ IERC20 constant AERO = IERC20(0x940181a94A35A4569E4529A3CDfB74e38FD98631); * Each token in the pool can have between 1 and 2 oracles per each token. * * tok = AerodromeStakingWrapper(stablePool) - * ref = toFix(2) + * ref = LP token /w shift * tar = USD * UoA = USD * @@ -38,6 +38,7 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { AerodromePoolTokens(aptConfig) { require(config.defaultThreshold != 0, "defaultThreshold zero"); + assert((token0.decimals() + token1.decimals()) % 2 == 0); maxOracleTimeout = uint48(Math.max(maxOracleTimeout, maxPoolOracleTimeout())); } @@ -65,7 +66,7 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { console.log("reserves0: %s", r0); console.log("reserves1: %s", r1); - // x3y+y3x >= k for sAMM pools + // xy^3 + yx^3 >= k for sAMM pools uint256 sqrtReserve = sqrt256(sqrt256(r0 * r1) * sqrt256(r0 * r0 + r1 * r1)); console.log("sqrtReserve: %s", sqrtReserve); @@ -104,6 +105,8 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { assert(low <= high); //obviously true just by inspection pegPrice = FIX_ONE; + + console.log("refPerTok: %s", refPerTok()); } /// Should not revert @@ -159,9 +162,8 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens function refPerTok() public view virtual override returns (uint192) { - // TODO: Review case of negative offset - uint8 decimalOffset = token0.decimals() + token1.decimals() - 18; - return toFix(2).mulu(10**decimalOffset); + int8 shift = 18 - int8((token0.decimals() + token1.decimals()) / 2); + return shiftl_toFix(2, shift, FLOOR); } // === Internal === diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts index 5032b189c..d8e01e80a 100644 --- a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts +++ b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts @@ -1,7 +1,7 @@ import collateralTests from '../collateralTests' import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' import { ethers } from 'hardhat' -import { ContractFactory, BigNumberish } from 'ethers' +import { ContractFactory, BigNumberish, BigNumber } from 'ethers' import { IAeroPool, MockV3Aggregator, @@ -40,7 +40,6 @@ import { } from './constants' import { expectPrice } from '../../../utils/oracles' import { mintWrappedLpToken, resetFork, getFeeds, pushAllFeedsForward } from './helpers' -import BigNumber from 'bignumber.js' /* Define interfaces @@ -84,7 +83,7 @@ interface AerodromeCollateralFixtureContext extends CollateralFixtureContext { const config = networkConfig['8453'] // use Base fork // Test all Aerodrome Stable pools -const all = [ +const all: AeroStablePoolEnumeration[] = [ { testName: 'Aerodrome - USDC/eUSD Stable', pool: AERO_USDC_eUSD_POOL, @@ -383,11 +382,11 @@ all.forEach((curr: AeroStablePoolEnumeration) => { const initRefPerTok = await ctx.collateral.refPerTok() const coll = await ethers.getContractAt('AerodromeStableCollateral', ctx.collateral.address) - const feed0 = await ethers.getContractAt('MockV3Aggregator', (await coll.tokenFeeds(0))[0]) + const feed0 = await ethers.getContractAt('MockV3Aggregator', (await coll.tokenFeeds(0))[0]) const decimals0 = await feed0.decimals() const initData0 = await feed0.latestRoundData() - const feed1 = await ethers.getContractAt('MockV3Aggregator', (await coll.tokenFeeds(1))[0]) + const feed1 = await ethers.getContractAt('MockV3Aggregator', (await coll.tokenFeeds(1))[0]) const decimals1 = await feed1.decimals() const initData1 = await feed1.latestRoundData() From 3dd2b9d66d5a595747564af3afd222b1d8136d04 Mon Sep 17 00:00:00 2001 From: Julian R Date: Mon, 23 Sep 2024 09:25:37 -0300 Subject: [PATCH 09/23] final test and script adjustments --- .../aerodrome/AerodromeStableCollateral.sol | 21 ------------------- .../collaterals/deploy_aerodrome_usdc_eusd.ts | 3 +-- .../verify_aerodrome_usdc_eusd.ts | 3 +-- .../AerodromeStableCollateral.test.ts | 11 +++++----- 4 files changed, 8 insertions(+), 30 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index bd812c8d5..bf7a03b45 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -10,8 +10,6 @@ import "contracts/plugins/assets/FiatCollateral.sol"; import "../../../interfaces/IRewardable.sol"; import "./AerodromePoolTokens.sol"; -import "hardhat/console.sol"; - // This plugin only works on Base IERC20 constant AERO = IERC20(0x940181a94A35A4569E4529A3CDfB74e38FD98631); @@ -63,28 +61,15 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { uint256 r0 = tokenReserve(0); uint256 r1 = tokenReserve(1); - console.log("reserves0: %s", r0); - console.log("reserves1: %s", r1); - // xy^3 + yx^3 >= k for sAMM pools uint256 sqrtReserve = sqrt256(sqrt256(r0 * r1) * sqrt256(r0 * r0 + r1 * r1)); - console.log("sqrtReserve: %s", sqrtReserve); - // get token prices (uint192 p0_low, uint192 p0_high) = tokenPrice(0); (uint192 p1_low, uint192 p1_high) = tokenPrice(1); - console.log("p0_low: %s", p0_low); - console.log("p0_high: %s", p0_high); - console.log("p1_low: %s", p1_low); - console.log("p1_high: %s", p1_high); - uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); - console.log("total supply raw: %s", pool.totalSupply()); - console.log("total supply: %s", totalSupply); - // low uint256 ratioLow = ((1e18) * p0_low) / p1_low; uint256 sqrtPriceLow = sqrt256( @@ -92,8 +77,6 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { ); low = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceLow) * p0_low * 2) / totalSupply); - console.log("low: %s", low); - // high uint256 ratioHigh = ((1e18) * p0_high) / p1_high; uint256 sqrtPriceHigh = sqrt256( @@ -101,12 +84,8 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { ); high = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceHigh) * p0_high * 2) / totalSupply); - console.log("high: %s", high); - assert(low <= high); //obviously true just by inspection pegPrice = FIX_ONE; - - console.log("refPerTok: %s", refPerTok()); } /// Should not revert diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts b/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts index a0b866a57..31b39c810 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_aerodrome_usdc_eusd.ts @@ -12,7 +12,7 @@ import { fileExists, } from '../../common' import { AerodromeStableCollateral, AerodromeGaugeWrapper, IAeroPool } from '../../../../typechain' -import { combinedError, revenueHiding } from '../../utils' +import { combinedError } from '../../utils' import { AerodromePoolType, DEFAULT_THRESHOLD, @@ -101,7 +101,6 @@ async function main() { defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, - revenueHiding.toString(), { pool: AERO_USDC_eUSD_POOL, poolType: AerodromePoolType.Stable, diff --git a/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts b/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts index 26a0f81d2..0c0f43485 100644 --- a/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts +++ b/scripts/verification/collateral-plugins/verify_aerodrome_usdc_eusd.ts @@ -8,7 +8,7 @@ import { IAssetCollDeployments, } from '../../deployment/common' import { verifyContract } from '../../deployment/utils' -import { combinedError, revenueHiding } from '../../deployment/utils' +import { combinedError } from '../../deployment/utils' import { IAeroPool } from '@typechain/IAeroPool' import { AerodromePoolType, @@ -83,7 +83,6 @@ async function main() { defaultThreshold: DEFAULT_THRESHOLD, delayUntilDefault: DELAY_UNTIL_DEFAULT, }, - revenueHiding.toString(), { pool: AERO_USDC_eUSD_POOL, poolType: AerodromePoolType.Stable, diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts index d8e01e80a..2727abd94 100644 --- a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts +++ b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts @@ -390,11 +390,12 @@ all.forEach((curr: AeroStablePoolEnumeration) => { const decimals1 = await feed1.decimals() const initData1 = await feed1.latestRoundData() - return initData0.answer - .mul(bn(10).pow(18 - decimals0)) - .mul(initData1.answer) - .mul(bn(10).pow(18 - decimals1)) - .div(fp('1')) + const avgPrice = (initData0.answer + .mul(bn(10).pow(18 - decimals0))) + .add(initData1.answer.mul(bn(10).pow(18 - decimals1))) + .div(2) + + return avgPrice .mul(initRefPerTok) .div(fp('1')) } From 3cdb40b0cc3082e8e3e078b1ae0e3d596612ba03 Mon Sep 17 00:00:00 2001 From: Julian R Date: Mon, 23 Sep 2024 09:27:32 -0300 Subject: [PATCH 10/23] fix lint --- .../aerodrome/AerodromeStableCollateral.test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts index 2727abd94..109191cdb 100644 --- a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts +++ b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts @@ -390,14 +390,12 @@ all.forEach((curr: AeroStablePoolEnumeration) => { const decimals1 = await feed1.decimals() const initData1 = await feed1.latestRoundData() - const avgPrice = (initData0.answer - .mul(bn(10).pow(18 - decimals0))) - .add(initData1.answer.mul(bn(10).pow(18 - decimals1))) - .div(2) - - return avgPrice - .mul(initRefPerTok) - .div(fp('1')) + const avgPrice = initData0.answer + .mul(bn(10).pow(18 - decimals0)) + .add(initData1.answer.mul(bn(10).pow(18 - decimals1))) + .div(2) + + return avgPrice.mul(initRefPerTok).div(fp('1')) } /* From ed650aaae772c217204c807332d012bd43f20975 Mon Sep 17 00:00:00 2001 From: Julian R Date: Mon, 23 Sep 2024 09:48:38 -0300 Subject: [PATCH 11/23] add readme --- contracts/plugins/assets/aerodrome/README.md | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 contracts/plugins/assets/aerodrome/README.md diff --git a/contracts/plugins/assets/aerodrome/README.md b/contracts/plugins/assets/aerodrome/README.md new file mode 100644 index 000000000..82cf963aa --- /dev/null +++ b/contracts/plugins/assets/aerodrome/README.md @@ -0,0 +1,45 @@ +# Aerodrome Stable Collateral Plugin + +[Aerodrome Finance](https://aerodrome.finance) is an AMM designed to serve as Base's central liquidity hub. This plugin enables the use of any Aerodrome Stable LP token as collateral within the Reserve Protocol. + +Aerodrome Finance offers two different liquidity pool types based on token pair needs, `Stable Pools` and `Volatile Pools`. + +Only `Stable Pools` are currently supported. These pools are designed for tokens which have little to no volatility, and use the current formula for pricing tokens: + +`x³y + y³x ≥ k` + +## Usage + +### Number of Tokens in The Pool + +All Aerodrome Pools are designed to support `2 (two)` tokens. So this field is harcoded and not provided as a configuration deployment parameter. + +### Multiple Price Feeds + +Some tokens require multiple price feeds since they do not have a direct price feed to USD. One example of this is WBTC. To support this, the plugin accepts a `tokensPriceFeeds` field in the configuration deployment parameter. This data structure is a `address[][]` and should have the same length as the number of coins in the Pool. The indices of these price feeds should also match the indices of the tokens in the pool. For example, if I am deploying a collateral plugin for the USDC/EUSD, I would need to pass something like `[[USDC_USD_FEED_ADDR], [EUSD_USD_FEED_ADDR]]` as `tokensPriceFeeds`. Since USDC has an index of 0 in the Aerodrome USDC/eUSD pool, the USDC price feed should be in index 0 in `tokensPriceFeeds`. + +### Wrapped Stake Token + +Since the Aerodrome LP Token needs to be staked in the Gauge to get rewards in AERO, we need to wrap it in another ERC20-token. This repo includes an `AerodromeGaugeStakingWrapper` contract that needs to be deployed and its address passed as the `erc20` configuration parameter. + +### Rewards + +Rewards come in the form of AERO tokens, which will be distributed once `claimRewards()` is called. + +AERO token: `https://basescan.org/token/0x940181a94a35a4569e4529a3cdfb74e38fd98631` + +## Implementation Notes + +### Immutable Arrays for Price Feeds + +Internally, all `tokensPriceFeeds` are stored as multiple separate immutable variables instead of just one array-type state variable for each. This is a gas-optimization done to avoid using SSTORE/SLOAD opcodes which are necessary but expensive operations when using state variables. Immutable variables, on the other hand, are embedded in the bytecode and are much cheaper to use which leads to more gas-efficient `price`, `strictPrice` and `refresh` functions. This work-around is necessary since Solidity does not yet support immutable arrays. + +### refPerTok + +Aerodrome Stable Pools do not appreciate in value over time, so `refPerTok()` will be constant for these plugins and will not change. This also means there are no hard default checks in place. + +## Implementation + +| `tok` | `ref` | `target` | `UoA` | +| :------------------: | :---------------: | :------: | :---: | +| Aero Staking Wrapper | LP token /w shift | USD | USD | From 773ed0d3f1d24dfe70255ee1643d2f5fd0db875f Mon Sep 17 00:00:00 2001 From: Julian R Date: Mon, 23 Sep 2024 10:01:53 -0300 Subject: [PATCH 12/23] add aerodrome test in ci --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9234f2717..b29512609 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -159,7 +159,7 @@ jobs: restore-keys: | hardhat-network-fork-${{ runner.os }}- hardhat-network-fork- - - run: yarn hardhat test ./test/plugins/individual-collateral/{cbeth,aave-v3,compoundv3,stargate,lido}/*.test.ts + - run: yarn hardhat test ./test/plugins/individual-collateral/{cbeth,aave-v3,aerodrome,compoundv3,stargate,lido}/*.test.ts env: NODE_OPTIONS: '--max-old-space-size=32768' TS_NODE_SKIP_IGNORE: true From 4802b4d8d6c6e787c013e142fea4c824eb18a426 Mon Sep 17 00:00:00 2001 From: Julian R Date: Mon, 23 Sep 2024 11:31:01 -0300 Subject: [PATCH 13/23] avoid gauge test on mainnet --- .../aerodrome/AerodromeGaugeWrapper.test.ts | 3 ++- test/plugins/individual-collateral/aerodrome/constants.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts index 954656fd8..652aa034f 100644 --- a/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts +++ b/test/plugins/individual-collateral/aerodrome/AerodromeGaugeWrapper.test.ts @@ -14,6 +14,7 @@ import { import { expect } from 'chai' import { ZERO_ADDRESS } from '#/common/constants' import { + forkNetwork, AERO, eUSD, AERO_USDC_eUSD_GAUGE, @@ -24,7 +25,7 @@ import { bn, fp } from '#/common/numbers' import { getChainId } from '#/common/blockchain-utils' import { advanceTime } from '#/test/utils/time' -const describeFork = useEnv('FORK') ? describe : describe.skip +const describeFork = useEnv('FORK') && forkNetwork == 'base' ? describe : describe.skip const point1Pct = (value: BigNumber): BigNumber => { return value.div(1000) diff --git a/test/plugins/individual-collateral/aerodrome/constants.ts b/test/plugins/individual-collateral/aerodrome/constants.ts index 7c468868b..d8a6e7dad 100644 --- a/test/plugins/individual-collateral/aerodrome/constants.ts +++ b/test/plugins/individual-collateral/aerodrome/constants.ts @@ -1,5 +1,8 @@ import { bn, fp } from '../../../../common/numbers' import { networkConfig } from '../../../../common/configuration' +import { useEnv } from '#/utils/env' + +export const forkNetwork = useEnv('FORK_NETWORK') ?? 'base' // Base Addresses export const AERO_USDC_eUSD_GAUGE = '0x793F22aB88dC91793E5Ce6ADbd7E733B0BD4733e' From 4475c31becceaa4c18935ca1fb0ea84dd1dcef60 Mon Sep 17 00:00:00 2001 From: Julian R Date: Tue, 1 Oct 2024 10:37:20 -0300 Subject: [PATCH 14/23] add aero to config --- common/configuration.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/configuration.ts b/common/configuration.ts index 58be27aed..c1140aeaa 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -116,6 +116,9 @@ export interface ITokens { // Mountain USDM?: string wUSDM?: string + + // Aerodrome + AERO?: string } export type ITokensKeys = Array From f974bf33de4ba497884848b2464a48b092e98944 Mon Sep 17 00:00:00 2001 From: Julian R Date: Tue, 1 Oct 2024 10:37:46 -0300 Subject: [PATCH 15/23] add check for stable pools --- contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol index 15bb4f15b..882b32ee3 100644 --- a/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol +++ b/contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol @@ -76,7 +76,7 @@ contract AerodromePoolTokens { // === Tokens === - if (config.poolType != AeroPoolType.Stable) { + if (config.poolType != AeroPoolType.Stable || !config.pool.stable()) { revert("invalid poolType"); } From 49b0216d5f3352b3bdc756b3d0e2fab94ccfd326 Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 4 Oct 2024 11:24:19 -0300 Subject: [PATCH 16/23] change pegPrice --- .../aerodrome/AerodromeStableCollateral.sol | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index bf7a03b45..57f5303bc 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -71,21 +71,29 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { uint192 totalSupply = shiftl_toFix(pool.totalSupply(), -int8(pool.decimals()), FLOOR); // low - uint256 ratioLow = ((1e18) * p0_low) / p1_low; - uint256 sqrtPriceLow = sqrt256( - sqrt256((1e18) * ratioLow) * sqrt256(1e36 + ratioLow * ratioLow) - ); - low = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceLow) * p0_low * 2) / totalSupply); - + { + uint256 ratioLow = ((1e18) * p0_low) / p1_low; + uint256 sqrtPriceLow = sqrt256( + sqrt256((1e18) * ratioLow) * sqrt256(1e36 + ratioLow * ratioLow) + ); + low = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceLow) * p0_low * 2) / totalSupply); + } // high - uint256 ratioHigh = ((1e18) * p0_high) / p1_high; - uint256 sqrtPriceHigh = sqrt256( - sqrt256((1e18) * ratioHigh) * sqrt256(1e36 + ratioHigh * ratioHigh) - ); - high = _safeWrap(((((1e18) * sqrtReserve) / sqrtPriceHigh) * p0_high * 2) / totalSupply); - + { + uint256 ratioHigh = ((1e18) * p0_high) / p1_high; + uint256 sqrtPriceHigh = sqrt256( + sqrt256((1e18) * ratioHigh) * sqrt256(1e36 + ratioHigh * ratioHigh) + ); + + high = _safeWrap( + ((((1e18) * sqrtReserve) / sqrtPriceHigh) * p0_high * 2) / totalSupply + ); + } assert(low <= high); //obviously true just by inspection - pegPrice = FIX_ONE; + + // {target/ref} = {UoA/ref} = {UoA/tok} / ({ref/tok} + // {target/ref} and {UoA/ref} are the same since target == UoA + pegPrice = ((low + high) / 2).div(refPerTok()); } /// Should not revert From 434145d198bc6a80d0f8ed9d161ccf184d21d1ae Mon Sep 17 00:00:00 2001 From: Julian R Date: Mon, 7 Oct 2024 08:27:05 -0300 Subject: [PATCH 17/23] low high fix --- .../plugins/assets/aerodrome/AerodromeStableCollateral.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol index 57f5303bc..aab8b8f75 100644 --- a/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol +++ b/contracts/plugins/assets/aerodrome/AerodromeStableCollateral.sol @@ -72,7 +72,7 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { // low { - uint256 ratioLow = ((1e18) * p0_low) / p1_low; + uint256 ratioLow = ((1e18) * p0_high) / p1_low; uint256 sqrtPriceLow = sqrt256( sqrt256((1e18) * ratioLow) * sqrt256(1e36 + ratioLow * ratioLow) ); @@ -80,7 +80,7 @@ contract AerodromeStableCollateral is FiatCollateral, AerodromePoolTokens { } // high { - uint256 ratioHigh = ((1e18) * p0_high) / p1_high; + uint256 ratioHigh = ((1e18) * p0_low) / p1_high; uint256 sqrtPriceHigh = sqrt256( sqrt256((1e18) * ratioHigh) * sqrt256(1e36 + ratioHigh * ratioHigh) ); From b42b0c94a7ec5c817f97f7be4bd8347894431238 Mon Sep 17 00:00:00 2001 From: Patrick McKelvy Date: Wed, 16 Oct 2024 14:39:45 -0400 Subject: [PATCH 18/23] deployed. --- scripts/addresses/8453-tmp-assets-collateral.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/addresses/8453-tmp-assets-collateral.json b/scripts/addresses/8453-tmp-assets-collateral.json index 4602fbd2d..302dd7a47 100644 --- a/scripts/addresses/8453-tmp-assets-collateral.json +++ b/scripts/addresses/8453-tmp-assets-collateral.json @@ -11,7 +11,8 @@ "cbETH": "0x851B461a9744f4c9E996C03072cAB6f44Fa04d0D", "saBasUSDC": "0xC19f5d60e2Aca1174f3D5Fe189f0A69afaB76f50", "cUSDCv3": "0xf7a9D27c3B60c78c6F6e2c2d6ED6E8B94b352461", - "wstETH": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73" + "wstETH": "0x8b4374005291B8FCD14C4E947604b2FB3C660A73", + "aeroUSDCeUSD": "0x9216CD5cA133aBBd23cc6F873bB4a95A78032db0" }, "erc20s": { "COMP": "0x9e1028F5F1D5eDE59748FFceE5532509976840E0", @@ -23,6 +24,7 @@ "saBasUSDC": "0x6F6f81e5E66f503184f2202D83a79650c3285759", "STG": "0xE3B53AF74a4BF62Ae5511055290838050bf764Df", "cUSDCv3": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA", - "wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452" + "wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + "aeroUSDCeUSD": "0xDB5b8cead52f77De0f6B5255f73F348AAf2CBb8D" } -} +} \ No newline at end of file From c3a0189a0cbb65e877182fc0c978ed34a31cc733 Mon Sep 17 00:00:00 2001 From: Julian R Date: Tue, 29 Oct 2024 13:01:52 -0300 Subject: [PATCH 19/23] deploy aero --- common/configuration.ts | 1 + .../addresses/8453-tmp-assets-collateral.json | 8 ++- .../8453-tmp-assets-collateral.json | 12 ++++ .../phase2-assets/assets/deploy_aero.ts | 71 +++++++++++++++++++ scripts/verification/assets/verify_aero.ts | 49 +++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 scripts/addresses/base-4.0.0/8453-tmp-assets-collateral.json create mode 100644 scripts/deployment/phase2-assets/assets/deploy_aero.ts create mode 100644 scripts/verification/assets/verify_aero.ts diff --git a/common/configuration.ts b/common/configuration.ts index c1140aeaa..300f60650 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -538,6 +538,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h + AERO: '0x4EC5970fC728C5f65ba413992CD5fF6FD70fcfF0', // 0.5%, 24h eUSD: '0x9b2C948dbA5952A1f5Ab6fA16101c1392b8da1ab', }, GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock diff --git a/scripts/addresses/8453-tmp-assets-collateral.json b/scripts/addresses/8453-tmp-assets-collateral.json index 302dd7a47..5d8c2b9cb 100644 --- a/scripts/addresses/8453-tmp-assets-collateral.json +++ b/scripts/addresses/8453-tmp-assets-collateral.json @@ -1,7 +1,8 @@ { "assets": { "COMP": "0xB8794Fb1CCd62bFe631293163F4A3fC2d22e37e0", - "STG": "0xEE527CC63122732532d0f1ad33Ec035D30f3050f" + "STG": "0xEE527CC63122732532d0f1ad33Ec035D30f3050f", + "AERO": "0x5D09F98B6fA59456E608bD20Ca806140884C3790" }, "collateral": { "DAI": "0x3E40840d0282C9F9cC7d17094b5239f87fcf18e5", @@ -25,6 +26,7 @@ "STG": "0xE3B53AF74a4BF62Ae5511055290838050bf764Df", "cUSDCv3": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA", "wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", - "aeroUSDCeUSD": "0xDB5b8cead52f77De0f6B5255f73F348AAf2CBb8D" + "aeroUSDCeUSD": "0xDB5b8cead52f77De0f6B5255f73F348AAf2CBb8D", + "AERO": "0x940181a94A35A4569E4529A3CDfB74e38FD98631" } -} \ No newline at end of file +} diff --git a/scripts/addresses/base-4.0.0/8453-tmp-assets-collateral.json b/scripts/addresses/base-4.0.0/8453-tmp-assets-collateral.json new file mode 100644 index 000000000..fefc3e876 --- /dev/null +++ b/scripts/addresses/base-4.0.0/8453-tmp-assets-collateral.json @@ -0,0 +1,12 @@ +{ + "assets": { + "AERO": "0x5D09F98B6fA59456E608bD20Ca806140884C3790" + }, + "collateral": { + "aeroUSDCeUSD": "0x9216CD5cA133aBBd23cc6F873bB4a95A78032db0" + }, + "erc20s": { + "aeroUSDCeUSD": "0xDB5b8cead52f77De0f6B5255f73F348AAf2CBb8D", + "AERO": "0x940181a94A35A4569E4529A3CDfB74e38FD98631" + } +} diff --git a/scripts/deployment/phase2-assets/assets/deploy_aero.ts b/scripts/deployment/phase2-assets/assets/deploy_aero.ts new file mode 100644 index 000000000..d2e467254 --- /dev/null +++ b/scripts/deployment/phase2-assets/assets/deploy_aero.ts @@ -0,0 +1,71 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { baseL2Chains, networkConfig } from '../../../../common/configuration' +import { fp } from '../../../../common/numbers' +import { + getDeploymentFile, + getDeploymentFilename, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + fileExists, +} from '../../common' +import { priceTimeout } from '../../utils' +import { Asset } from '../../../../typechain' + +async function main() { + // ==== Read Configuration ==== + const [burner] = await hre.ethers.getSigners() + const chainId = await getChainId(hre) + + console.log(`Deploying AERO asset to network ${hre.network.name} (${chainId}) + with burner account: ${burner.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedAssets: string[] = [] + + // Only for Base + if (baseL2Chains.includes(hre.network.name)) { + /******** Deploy AERO asset **************************/ + const { asset: aeroAsset } = await hre.run('deploy-asset', { + priceTimeout: priceTimeout.toString(), + priceFeed: networkConfig[chainId].chainlinkFeeds.AERO, + oracleError: fp('0.005').toString(), // 0.5% + tokenAddress: networkConfig[chainId].tokens.AERO, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: '86400', // 24 hr + }) + await (await ethers.getContractAt('Asset', aeroAsset)).refresh() + + assetCollDeployments.assets.AERO = aeroAsset + assetCollDeployments.erc20s.AERO = networkConfig[chainId].tokens.AERO + deployedAssets.push(aeroAsset.toString()) + } else { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + /**************************************************************/ + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed AERO asset to ${hre.network.name} (${chainId}): + New deployments: ${deployedAssets} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/assets/verify_aero.ts b/scripts/verification/assets/verify_aero.ts new file mode 100644 index 000000000..fa41a2bb4 --- /dev/null +++ b/scripts/verification/assets/verify_aero.ts @@ -0,0 +1,49 @@ +import hre from 'hardhat' + +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { + getAssetCollDeploymentFilename, + getDeploymentFile, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { fp } from '../../../common/numbers' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + deployments = getDeploymentFile(getAssetCollDeploymentFilename(chainId)) + + const asset = await hre.ethers.getContractAt('Asset', deployments.assets.AERO!) + + /** ******************** Verify AERO Asset ****************************************/ + await verifyContract( + chainId, + deployments.assets.AERO, + [ + (await asset.priceTimeout()).toString(), + await asset.chainlinkFeed(), + fp('0.005').toString(), + await asset.erc20(), + (await asset.maxTradeVolume()).toString(), + (await asset.oracleTimeout()).toString(), + ], + 'contracts/plugins/assets/Asset.sol:Asset' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) From 26ff39ae1b3fd4a503584a05370faf81933d1a9e Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 1 Nov 2024 09:58:00 -0300 Subject: [PATCH 20/23] adapt feed revert test --- .../AerodromeStableCollateral.test.ts | 29 +++++++++++- .../individual-collateral/collateralTests.ts | 44 ++++++++++--------- .../individual-collateral/pluginTestTypes.ts | 3 ++ 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts index 109191cdb..bd6c9e518 100644 --- a/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts +++ b/test/plugins/individual-collateral/aerodrome/AerodromeStableCollateral.test.ts @@ -6,13 +6,14 @@ import { IAeroPool, MockV3Aggregator, MockV3Aggregator__factory, + InvalidMockV3Aggregator, AerodromeGaugeWrapper__factory, TestICollateral, AerodromeGaugeWrapper, ERC20Mock, } from '../../../../typechain' import { networkConfig } from '../../../../common/configuration' -import { ZERO_ADDRESS } from '#/common/constants' +import { CollateralStatus, ZERO_ADDRESS } from '#/common/constants' import { bn, fp } from '../../../../common/numbers' import { expect } from 'chai' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' @@ -376,6 +377,31 @@ all.forEach((curr: AeroStablePoolEnumeration) => { const finalRefPerTok = await coll.refPerTok() expect(finalRefPerTok).to.equal(initialRefPerTok) }) + + it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { + const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( + 'InvalidMockV3Aggregator' + ) + const invalidChainlinkFeed = ( + await InvalidMockV3AggregatorFactory.deploy(6, bn('1e6')) + ) + + const invalidCollateral = await deployCollateral({ + pool: curr.pool, + gauge: curr.gauge, + feeds: [[invalidChainlinkFeed.address], [invalidChainlinkFeed.address]], + }) + + // Reverting with no reason + await invalidChainlinkFeed.setSimplyRevert(true) + await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() + expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) + + // Runnning out of gas (same error) + await invalidChainlinkFeed.setSimplyRevert(false) + await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() + expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) + }) } const getExpectedPrice = async (ctx: CollateralFixtureContext) => { @@ -424,6 +450,7 @@ all.forEach((curr: AeroStablePoolEnumeration) => { itChecksRefPerTokDefault: it.skip, itChecksPriceChanges: it.skip, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it.skip, itHasRevenueHiding: it.skip, resetFork, collateralName: curr.testName, diff --git a/test/plugins/individual-collateral/collateralTests.ts b/test/plugins/individual-collateral/collateralTests.ts index fa7ae3a1e..8ce9c5023 100644 --- a/test/plugins/individual-collateral/collateralTests.ts +++ b/test/plugins/individual-collateral/collateralTests.ts @@ -87,6 +87,7 @@ export default function fn( itChecksRefPerTokDefault, itChecksPriceChanges, itChecksNonZeroDefaultThreshold, + itChecksMainChainlinkOracleRevert, itHasRevenueHiding, itIsPricedByPeg, itHasOracleRefPerTok, @@ -394,29 +395,32 @@ export default function fn( ) // within 1-part-in-1-thousand }) - it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { - const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( - 'InvalidMockV3Aggregator' - ) - const invalidChainlinkFeed = ( - await InvalidMockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) - ) + itChecksMainChainlinkOracleRevert( + 'reverts if Chainlink feed reverts or runs out of gas, maintains status', + async () => { + const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( + 'InvalidMockV3Aggregator' + ) + const invalidChainlinkFeed = ( + await InvalidMockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) - const invalidCollateral = await deployCollateral({ - erc20: ctx.tok.address, - chainlinkFeed: invalidChainlinkFeed.address, - }) + const invalidCollateral = await deployCollateral({ + erc20: ctx.tok.address, + chainlinkFeed: invalidChainlinkFeed.address, + }) - // Reverting with no reason - await invalidChainlinkFeed.setSimplyRevert(true) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) + // Reverting with no reason + await invalidChainlinkFeed.setSimplyRevert(true) + await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() + expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - // Runnning out of gas (same error) - await invalidChainlinkFeed.setSimplyRevert(false) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - }) + // Runnning out of gas (same error) + await invalidChainlinkFeed.setSimplyRevert(false) + await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() + expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) + } + ) it('decays price over priceTimeout period', async () => { await collateral.refresh() diff --git a/test/plugins/individual-collateral/pluginTestTypes.ts b/test/plugins/individual-collateral/pluginTestTypes.ts index 837e5a258..d65722bf8 100644 --- a/test/plugins/individual-collateral/pluginTestTypes.ts +++ b/test/plugins/individual-collateral/pluginTestTypes.ts @@ -106,6 +106,9 @@ export interface CollateralTestSuiteFixtures // toggle on or off: tests that check that defaultThreshold is not zero itChecksNonZeroDefaultThreshold: Mocha.TestFunction | Mocha.PendingTestFunction + // toggle on or off: tests that check when the main chainlink feed reverts (not always used) + itChecksMainChainlinkOracleRevert: Mocha.TestFunction | Mocha.PendingTestFunction + // does the peg price matter for the results of tryPrice()? itIsPricedByPeg?: boolean From fa9070e68236b7f9247b5a83a63ece8ac7413ab9 Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 1 Nov 2024 10:15:36 -0300 Subject: [PATCH 21/23] add new parameter --- .../ankr/AnkrEthCollateralTestSuite.test.ts | 1 + test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts | 1 + .../individual-collateral/cbeth/CBETHCollateralL2.test.ts | 1 + .../individual-collateral/compoundv3/CometTestSuite.test.ts | 1 + .../individual-collateral/dsr/SDaiCollateralTestSuite.test.ts | 1 + .../individual-collateral/ethena/USDeFiatCollateral.test.ts | 1 + test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts | 1 + .../flux-finance/FTokenFiatCollateral.test.ts | 1 + .../individual-collateral/frax-eth/SFrxEthTestSuite.test.ts | 1 + .../individual-collateral/frax/SFraxCollateralTestSuite.test.ts | 1 + .../individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts | 1 + .../individual-collateral/lido/LidoStakedEthTestSuite.test.ts | 1 + .../meta-morpho/MetaMorphoFiatCollateral.test.ts | 1 + .../meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts | 1 + .../morpho-aave/MorphoAAVEFiatCollateral.test.ts | 1 + .../morpho-aave/MorphoAAVENonFiatCollateral.test.ts | 1 + .../morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts | 1 + .../individual-collateral/mountain/USDMCollateral.test.ts | 1 + .../individual-collateral/pirex-eth/ApxEthCollateral.test.ts | 1 + .../rocket-eth/RethCollateralTestSuite.test.ts | 1 + .../stargate/StargateUSDCTestSuite.test.ts_DEPRECATED | 1 + 21 files changed, 21 insertions(+) diff --git a/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts b/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts index 8a3a07a83..ed9bae870 100644 --- a/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/ankr/AnkrEthCollateralTestSuite.test.ts @@ -291,6 +291,7 @@ const opts = { itChecksPriceChanges: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, resetFork, collateralName: 'AnkrStakedETH', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts index cd35ee5c0..6dea339bc 100644 --- a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts @@ -248,6 +248,7 @@ const opts = { itChecksPriceChanges: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, resetFork, collateralName: 'CBEthCollateral', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts index a4a9c3242..7e1a28744 100644 --- a/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateralL2.test.ts @@ -279,6 +279,7 @@ const opts = { itChecksPriceChanges: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, resetFork, collateralName: 'CBEthCollateralL2', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts index c0062e93d..5b5c7ba6f 100644 --- a/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CometTestSuite.test.ts @@ -384,6 +384,7 @@ allTests.forEach((curr: CTokenV3Enumeration) => { itChecksRefPerTokDefault: it.skip, // implemented in this file itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it.skip, // implemented in this file itIsPricedByPeg: true, resetFork: getResetFork(getForkBlock(curr.tokenName)), diff --git a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts index 2919f0ebc..52549b704 100644 --- a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts @@ -215,6 +215,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it.skip, resetFork, collateralName: 'SDaiCollateral', diff --git a/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts b/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts index 9624aaa70..6a6528d9f 100644 --- a/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/ethena/USDeFiatCollateral.test.ts @@ -211,6 +211,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, collateralName: 'USDe Fiat Collateral', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts b/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts index 10b4da87c..b3c380480 100644 --- a/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts +++ b/test/plugins/individual-collateral/ethx/ETHxCollateral.test.ts @@ -282,6 +282,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork, collateralName: 'Stader ETHx', diff --git a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts index 180a88935..eabfa1269 100644 --- a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts @@ -258,6 +258,7 @@ all.forEach((curr: FTokenEnumeration) => { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork, collateralName: curr.testName, diff --git a/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts b/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts index b15e1df41..b25608382 100644 --- a/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/frax-eth/SFrxEthTestSuite.test.ts @@ -311,6 +311,7 @@ const opts = { itChecksPriceChanges: it, itHasRevenueHiding: it.skip, // implemented in this file itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, resetFork, collateralName: 'SFraxEthCollateral', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts b/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts index 2ed9bc5e4..e9ec2b3e2 100644 --- a/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/frax/SFraxCollateralTestSuite.test.ts @@ -195,6 +195,7 @@ const opts = { itChecksTargetPerRefDefault: it, itChecksTargetPerRefDefaultUp: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itHasRevenueHiding: it.skip, diff --git a/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts b/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts index 184b1090a..7ac7956e2 100644 --- a/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/lido/L2LidoStakedEthTestSuite.test.ts @@ -278,6 +278,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK_BASE), collateralName: 'L2LidoStakedETH', diff --git a/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts b/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts index 366c8c81c..09bb47a51 100644 --- a/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts +++ b/test/plugins/individual-collateral/lido/LidoStakedEthTestSuite.test.ts @@ -269,6 +269,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork, collateralName: 'LidoStakedETH', diff --git a/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts b/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts index f8138befb..05429c153 100644 --- a/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/meta-morpho/MetaMorphoFiatCollateral.test.ts @@ -175,6 +175,7 @@ const makeFiatCollateralTestSuite = ( itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), targetNetwork: defaultCollateralOpts.forkNetwork, diff --git a/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts b/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts index d243ec85a..44b009bd6 100644 --- a/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts +++ b/test/plugins/individual-collateral/meta-morpho/MetaMorphoSelfReferentialCollateral.test.ts @@ -171,6 +171,7 @@ const makeFiatCollateralTestSuite = ( itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it.skip, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), targetNetwork: defaultCollateralOpts.forkNetwork, diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts index 9a9b94fad..27f971c04 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts @@ -368,6 +368,7 @@ const makeAaveFiatCollateralTestSuite = ( itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), collateralName, diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts index 937ec99e7..e846ec7e8 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts @@ -229,6 +229,7 @@ const makeAaveNonFiatCollateralTestSuite = ( itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, itIsPricedByPeg: true, resetFork: getResetFork(FORK_BLOCK), diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts index 81404fe20..71100d2a3 100644 --- a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts @@ -229,6 +229,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it.skip, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork: getResetFork(FORK_BLOCK), collateralName: 'MorphoAAVEV2SelfReferentialCollateral - WETH', diff --git a/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts b/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts index 570c60345..3605c491a 100644 --- a/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts +++ b/test/plugins/individual-collateral/mountain/USDMCollateral.test.ts @@ -311,6 +311,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it.skip, // implemented in this file collateralName: 'USDM Collateral', chainlinkDefaultAnswer, diff --git a/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts b/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts index db143ad60..e49008c64 100644 --- a/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts +++ b/test/plugins/individual-collateral/pirex-eth/ApxEthCollateral.test.ts @@ -378,6 +378,7 @@ const opts = { itChecksRefPerTokDefault: it.skip, // implemented in this file itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it.skip, // implemented in this file resetFork, collateralName: 'ApxETH', diff --git a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts index f766a3bc0..48017d264 100644 --- a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts @@ -276,6 +276,7 @@ const opts = { itChecksRefPerTokDefault: it, itChecksPriceChanges: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, resetFork, collateralName: 'RocketPoolETH', diff --git a/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED b/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED index 009ffdf1a..f3e64455d 100644 --- a/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED +++ b/test/plugins/individual-collateral/stargate/StargateUSDCTestSuite.test.ts_DEPRECATED @@ -316,6 +316,7 @@ export const stableOpts = { itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, itIsPricedByPeg: true, chainlinkDefaultAnswer: 1e8, From 6b183a74e4102a3a7a67906ed5657785d564590f Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 1 Nov 2024 10:33:20 -0300 Subject: [PATCH 22/23] add new param --- .../yearnv2/YearnV2CurveFiatCollateral.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/plugins/individual-collateral/yearnv2/YearnV2CurveFiatCollateral.test.ts b/test/plugins/individual-collateral/yearnv2/YearnV2CurveFiatCollateral.test.ts index 22017205a..9b34573b7 100644 --- a/test/plugins/individual-collateral/yearnv2/YearnV2CurveFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/yearnv2/YearnV2CurveFiatCollateral.test.ts @@ -260,6 +260,7 @@ tests.forEach((test: CurveFiatTest) => { itChecksTargetPerRefDefault: it, itChecksTargetPerRefDefaultUp: it, itChecksRefPerTokDefault: it, + itChecksMainChainlinkOracleRevert: it, itHasRevenueHiding: it, itClaimsRewards: it.skip, isMetapool: false, From dc65ee37c19dd81476b8d9d215be1df0cab442da Mon Sep 17 00:00:00 2001 From: Julian R Date: Fri, 1 Nov 2024 10:45:57 -0300 Subject: [PATCH 23/23] add new param to aave test --- test/plugins/individual-collateral/aave-v3/common.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/plugins/individual-collateral/aave-v3/common.ts b/test/plugins/individual-collateral/aave-v3/common.ts index 77e11f1be..f9f22f6a2 100644 --- a/test/plugins/individual-collateral/aave-v3/common.ts +++ b/test/plugins/individual-collateral/aave-v3/common.ts @@ -210,6 +210,7 @@ export const makeTests = (defaultCollateralOpts: CollateralParams, altParams: Al itChecksRefPerTokDefault: it, itHasRevenueHiding: it, itChecksNonZeroDefaultThreshold: it, + itChecksMainChainlinkOracleRevert: it, itIsPricedByPeg: true, chainlinkDefaultAnswer: 1e8, itChecksPriceChanges: it,