diff --git a/common/configuration.ts b/common/configuration.ts index 74b4b33eaa..af98ceba2f 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -54,6 +54,8 @@ export interface ITokens { sDAI?: string cbETH?: string MORPHO?: string + wBTCBTC?: string + astETH?: string } export interface IFeeds { @@ -144,7 +146,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { ONDO: '0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3', sDAI: '0x83f20f44975d03b1b09e64809b757c47f942beea', cbETH: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', - MORPHO: '0x9994E35Db50125E0DF82e4c2dde62496CE330999' + MORPHO: '0x9994E35Db50125E0DF82e4c2dde62496CE330999', + astETH: "0x1982b2F5814301d4e9a8b0201555376e62F82428" }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', @@ -170,6 +173,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH + wBTCBTC: "0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23", // "WBTC/BTC" + }, AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9', AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5', @@ -224,6 +229,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stkAAVE: '0x4da27a545c0c5B758a6BA100e3a049001de870f5', COMP: '0xc00e94Cb662C3520282E6f5717214004A7f26888', WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + aWBTC: '0x9ff58f4ffb29fa2266ab25e75e2a8b3503311656', WBTC: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', EURT: '0xC581b735A1688071A1746c968e0798D642EDE491', RSR: '0x320623b8e4ff03373931769a31fc52a4e78b5d70', @@ -239,6 +245,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { ONDO: '0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3', sDAI: '0x83f20f44975d03b1b09e64809b757c47f942beea', cbETH: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', + astETH: "0x1982b2F5814301d4e9a8b0201555376e62F82428" }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', @@ -264,12 +271,15 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH + wBTCBTC: "0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23" // "WBTC/BTC" }, AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9', AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c', FLUX_FINANCE_COMPTROLLER: '0x95Af143a021DF745bc78e845b54591C53a8B3A51', COMPTROLLER: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', - GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101' + GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101', + MORPHO_AAVE_LENS: '0x507fA343d0A90786d86C7cd885f5C49263A91FF4', + MORPHO_AAVE_CONTROLLER: '0x777777c9898D384F785Ee44Acfe945efDFf5f3E0' }, '5': { name: 'goerli', diff --git a/contracts/plugins/assets/morpho-aave/IMorpho.sol b/contracts/plugins/assets/morpho-aave/IMorpho.sol index b98bf349af..2427768b69 100644 --- a/contracts/plugins/assets/morpho-aave/IMorpho.sol +++ b/contracts/plugins/assets/morpho-aave/IMorpho.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface IMorpho { function supply(address _poolToken, uint256 _amount) external; + function withdraw(address _poolToken, uint256 _amount) external; } diff --git a/contracts/plugins/assets/morpho-aave/MorphoAAVEFiatCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoAAVEFiatCollateral.sol index 5927e25b2b..8531a7162c 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoAAVEFiatCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoAAVEFiatCollateral.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; - -import "../AppreciatingFiatCollateral.sol"; -import "../../../libraries/Fixed.sol"; -import "./MorphoAAVEPositionWrapper.sol"; +pragma solidity 0.8.19; +// solhint-disable-next-line max-line-length +import { IRewardable, Asset, AppreciatingFiatCollateral, CollateralConfig } from "../AppreciatingFiatCollateral.sol"; +import { MorphoAAVEPositionWrapper } from "./MorphoAAVEPositionWrapper.sol"; +import { FixLib } from "../../../libraries/Fixed.sol"; +import { OracleLib } from "../OracleLib.sol"; +// solhint-disable-next-line max-line-length +import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; /** * @title MorphoAAVEFiatCollateral @@ -15,14 +18,14 @@ contract MorphoAAVEFiatCollateral is AppreciatingFiatCollateral { using OracleLib for AggregatorV3Interface; using FixLib for uint192; - MorphoAAVEPositionWrapper wrapper; + MorphoAAVEPositionWrapper public immutable wrapper; - /// @param config Configuration of this collateral. config.erc20 must be a MorphoAAVEPositionWrapper - /// @param revenue_hiding {1} A value like 1e-6 that represents the maximum refPerTok to hide - constructor( - CollateralConfig memory config, - uint192 revenue_hiding - ) AppreciatingFiatCollateral(config, revenue_hiding) { + /// @param config Configuration of this collateral. + /// config.erc20 must be a MorphoAAVEPositionWrapper + /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide + constructor(CollateralConfig memory config, uint192 revenueHiding) + AppreciatingFiatCollateral(config, revenueHiding) + { require(address(config.erc20) != address(0), "missing erc20"); wrapper = MorphoAAVEPositionWrapper(address(config.erc20)); } @@ -31,25 +34,12 @@ contract MorphoAAVEFiatCollateral is AppreciatingFiatCollateral { /// @custom:interaction RCEI function refresh() public virtual override { // Update wrapper exchange rate for underlying token - wrapper.refresh_exchange_rate(); + wrapper.refreshExchangeRate(); super.refresh(); } /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens function _underlyingRefPerTok() internal view override returns (uint192) { - return wrapper.get_exchange_rate(); - } - - /// Claim rewards earned by holding a balance of the ERC20 token - /// @dev delegatecall - function claimRewards() external virtual override(Asset, IRewardable) { - // unfortunately Morpho uses a rewards scheme that requires the results - // of off-chain computation to be piped into an on-chain function, - // which is not possible to do with Reserve's collateral plugin interface. - - // https://integration.morpho.xyz/track-and-manage-position/manage-positions-on-morpho/claim-morpho-rewards - - // claiming rewards for this wrapper can be done by any account, and must be done on Morpho's rewards distributor contract - // https://etherscan.io/address/0x3b14e5c73e0a56d607a8688098326fd4b4292135 + return wrapper.getExchangeRate(); } } diff --git a/contracts/plugins/assets/morpho-aave/MorphoAAVENonFiatCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoAAVENonFiatCollateral.sol index 7efeae3600..3c0c0f1ff7 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoAAVENonFiatCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoAAVENonFiatCollateral.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; - -import "./MorphoAAVEFiatCollateral.sol"; -import "../../../libraries/Fixed.sol"; -import "./MorphoAAVEPositionWrapper.sol"; +pragma solidity 0.8.19; +import { CollateralConfig, MorphoAAVEFiatCollateral } from "./MorphoAAVEFiatCollateral.sol"; +import { FixLib, CEIL } from "../../../libraries/Fixed.sol"; +import { OracleLib } from "../OracleLib.sol"; +// solhint-disable-next-line max-line-length +import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; /** * @title MorphoAAVENonFiatCollateral @@ -18,16 +19,17 @@ contract MorphoAAVENonFiatCollateral is MorphoAAVEFiatCollateral { AggregatorV3Interface public immutable targetUnitChainlinkFeed; // {UoA/target} uint48 public immutable targetUnitOracleTimeout; // {s} - /// @param config Configuration of this collateral. config.erc20 must be a MorphoAAVEPositionWrapper - /// @param revenue_hiding {1} A value like 1e-6 that represents the maximum refPerTok to hide + /// @param config Configuration of this collateral. + /// config.erc20 must be a MorphoAAVEPositionWrapper + /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide /// @param targetUnitChainlinkFeed_ Feed units: {UoA/target} /// @param targetUnitOracleTimeout_ {s} oracle timeout to use for targetUnitChainlinkFeed constructor( CollateralConfig memory config, - uint192 revenue_hiding, + uint192 revenueHiding, AggregatorV3Interface targetUnitChainlinkFeed_, uint48 targetUnitOracleTimeout_ - ) MorphoAAVEFiatCollateral(config, revenue_hiding) { + ) MorphoAAVEFiatCollateral(config, revenueHiding) { targetUnitChainlinkFeed = targetUnitChainlinkFeed_; targetUnitOracleTimeout = targetUnitOracleTimeout_; } @@ -35,7 +37,7 @@ contract MorphoAAVENonFiatCollateral is MorphoAAVEFiatCollateral { /// Can revert, used by other contract functions in order to catch errors /// @return low {UoA/tok} The low price estimate /// @return high {UoA/tok} The high price estimate - /// @return pegPrice {target/ref} + /// @return pegPrice {target/ref} The actual price observed in the peg function tryPrice() external view @@ -46,17 +48,15 @@ contract MorphoAAVENonFiatCollateral is MorphoAAVEFiatCollateral { uint192 pegPrice ) { - pegPrice = chainlinkFeed.price(oracleTimeout); // {target/ref} + // {target/ref} Get current market peg ({btc/wbtc}) + pegPrice = targetUnitChainlinkFeed.price(targetUnitOracleTimeout); - // {UoA/tok} = {UoA/target} * {target/ref} * {ref/tok} - uint192 p = targetUnitChainlinkFeed.price(targetUnitOracleTimeout).mul(pegPrice).mul( - _underlyingRefPerTok() - ); + // {UoA/tok} = {UoA/ref} * {ref/tok} + uint192 p = chainlinkFeed.price(oracleTimeout).mul(_underlyingRefPerTok()); uint192 err = p.mul(oracleError, CEIL); - low = p - err; high = p + err; + low = p - err; // assert(low <= high); obviously true just by inspection } - } diff --git a/contracts/plugins/assets/morpho-aave/MorphoAAVEPositionWrapper.sol b/contracts/plugins/assets/morpho-aave/MorphoAAVEPositionWrapper.sol index b3ffc3cf5f..ea76ea8028 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoAAVEPositionWrapper.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoAAVEPositionWrapper.sol @@ -1,24 +1,27 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; -import "../../../libraries/Fixed.sol"; -import "./IMorpho.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; +import { FixLib, shiftl_toFix, FIX_ONE } from "../../../libraries/Fixed.sol"; + +import { IMorpho, UsersLens } from "./IMorpho.sol"; struct MorphoAAVEWrapperConfig { - IMorpho morpho_controller; - UsersLens morpho_lens; - IERC20Metadata underlying_erc20; - address pool_token; - string underlying_symbol; + IMorpho morphoController; + UsersLens morphoLens; + IERC20Metadata underlyingERC20; + address poolToken; + string underlyingSymbol; } /** * @title MorphoAAVEPositionWrapper - * @notice ERC20 that wraps a Morpho AAVE position, requiring underlying tokens on mint and redeeming underlying tokens on burn. + * @notice ERC20 that wraps a Morpho AAVE position, + * requiring underlying tokens on mint and redeeming underlying tokens on burn. + * * Designed to mimic a Compound cToken. */ contract MorphoAAVEPositionWrapper is ERC20, ERC20Burnable { @@ -26,99 +29,106 @@ contract MorphoAAVEPositionWrapper is ERC20, ERC20Burnable { using FixLib for uint192; uint8 private immutable _decimals; - IERC20Metadata private immutable underlying_erc20; + IERC20Metadata public immutable underlyingERC20; - address pool_token; - IMorpho morpho_controller; - UsersLens morpho_lens; + address public poolToken; + IMorpho public morphoController; + UsersLens public morphoLens; - uint256 tokens_supplied; + uint256 internal tokensSupplied; //Amount of the underlying that can be exchanged for 1 of this ERC20 - uint192 private exchange_rate; - - /// @param config Configuration of this wrapper. config.pool_token must be the respective AAVE pool token (e.g. aUSDC) - constructor(MorphoAAVEWrapperConfig memory config) ERC20( - string.concat("RMorphoAAVE", config.underlying_symbol), - string.concat("rma", config.underlying_symbol) - ) { - underlying_erc20 = config.underlying_erc20; - - _decimals = underlying_erc20.decimals(); - morpho_controller = config.morpho_controller; - morpho_lens = config.morpho_lens; - pool_token = config.pool_token; - exchange_rate = FIX_ONE; + uint192 internal exchangeRate; + + /// @param config Configuration of this wrapper. + /// config.poolToken must be the respective AAVE pool token (e.g. aUSDC) + constructor(MorphoAAVEWrapperConfig memory config) + ERC20( + string.concat("RMorphoAAVE", config.underlyingSymbol), + string.concat("rma", config.underlyingSymbol) + ) + { + underlyingERC20 = config.underlyingERC20; + + _decimals = underlyingERC20.decimals(); + morphoController = config.morphoController; + morphoLens = config.morphoLens; + poolToken = config.poolToken; + exchangeRate = FIX_ONE; + + underlyingERC20.safeApprove(address(morphoController), type(uint256).max); } - // Takes an uint256 amount of the wrapper token represented by this contract and returns - // a fixed point representation of that value. - function wrapper_to_fix(uint256 x) internal pure returns (uint192) { - return shiftl_toFix(x, -18); - } - - // Takes an uint256 amount of the underlying token to be deposited into Morpho and returns - // a fixed point representation of that value. - function underlying_to_fix(uint256 x) internal view returns (uint192) { - return shiftl_toFix(x, -int8(_decimals)); - } - - // Takes a fixed point representation of the underlying token to be - // deposited into Morpho and returns its uint256 amount. - function fix_to_underlying(uint192 x) internal view returns (uint256) { - return x.shiftl_toUint(int8(_decimals)); - } - - /* Check the current morpho pool's balance, and update the exchange_rate accordingly, + /* Check the current morpho pool's balance, and update the exchangeRate accordingly, * taking into account the number of tokens expected to be in the pool. * This is called on mint and burn, and can be called manually to update the exchange rate. */ - function adjust_exchange_rate() internal { - if (tokens_supplied == 0) { + function adjustExchangeRate() internal { + if (tokensSupplied == 0) { return; } - (, , uint256 grown_balance) = morpho_lens.getCurrentSupplyBalanceInOf(pool_token, address(this)); - exchange_rate = exchange_rate.mul( - underlying_to_fix(grown_balance).div(underlying_to_fix(tokens_supplied)) + (, , uint256 grownBalance) = morphoLens.getCurrentSupplyBalanceInOf( + poolToken, + address(this) + ); + exchangeRate = exchangeRate.mul( + underlyingToFix(grownBalance).div(underlyingToFix(tokensSupplied)) ); - tokens_supplied = grown_balance; + tokensSupplied = grownBalance; } - function refresh_exchange_rate() external { - adjust_exchange_rate(); + function refreshExchangeRate() external { + adjustExchangeRate(); } - function get_exchange_rate() external virtual view returns (uint192) { - return exchange_rate; + function getExchangeRate() external view virtual returns (uint192) { + return exchangeRate; } - /* On mint, we transfer the underlying tokens from the user to this contract, + /* On mint, we transfer the underlying tokens from the user to this contract, * and then supply them to the morpho pool. We then mint an appropriate amount of this ERC20. */ function mint(address to, uint256 amount) public { - adjust_exchange_rate(); + adjustExchangeRate(); - uint256 to_transfer_of_underlying = fix_to_underlying(wrapper_to_fix(amount).mul(exchange_rate)); - - underlying_erc20.safeTransferFrom(msg.sender, address(this), to_transfer_of_underlying); - underlying_erc20.safeIncreaseAllowance(address(morpho_controller), to_transfer_of_underlying); - morpho_controller.supply(pool_token, to_transfer_of_underlying); - tokens_supplied += to_transfer_of_underlying; + uint256 toTransferOfUnderlying = fixToUnderlying(wrapperToFix(amount).mul(exchangeRate)); + + underlyingERC20.safeTransferFrom(msg.sender, address(this), toTransferOfUnderlying); + morphoController.supply(poolToken, toTransferOfUnderlying); + tokensSupplied += toTransferOfUnderlying; _mint(to, amount); } - /* On burn, we transfer the underlying tokens from the morpho pool to this contract, + /* On burn, we transfer the underlying tokens from the morpho pool to this contract, * and then transfer them to the user. We then burn an appropriate amount of this ERC20. */ function burn(uint256 amount) public override { - adjust_exchange_rate(); + adjustExchangeRate(); _burn(_msgSender(), amount); - uint256 to_transfer_of_underlying = fix_to_underlying(wrapper_to_fix(amount).mul(exchange_rate)); - morpho_controller.withdraw(pool_token, to_transfer_of_underlying); - tokens_supplied -= to_transfer_of_underlying; + uint256 toTransferOfUnderlying = fixToUnderlying(wrapperToFix(amount).mul(exchangeRate)); + morphoController.withdraw(poolToken, toTransferOfUnderlying); + tokensSupplied -= toTransferOfUnderlying; + + underlyingERC20.transferFrom(address(this), msg.sender, toTransferOfUnderlying); + } + + // Takes an uint256 amount of the wrapper token represented by this contract and returns + // a fixed point representation of that value. + function wrapperToFix(uint256 x) internal pure returns (uint192) { + return shiftl_toFix(x, -18); + } + + // Takes an uint256 amount of the underlying token to be deposited into Morpho and returns + // a fixed point representation of that value. + function underlyingToFix(uint256 x) internal view returns (uint192) { + return shiftl_toFix(x, -int8(_decimals)); + } - underlying_erc20.transferFrom(address(this), msg.sender, to_transfer_of_underlying); + // Takes a fixed point representation of the underlying token to be + // deposited into Morpho and returns its uint256 amount. + function fixToUnderlying(uint192 x) internal view returns (uint256) { + return x.shiftl_toUint(int8(_decimals)); } } diff --git a/contracts/plugins/assets/morpho-aave/MorphoAAVESelfReferentialCollateral.sol b/contracts/plugins/assets/morpho-aave/MorphoAAVESelfReferentialCollateral.sol index 3386e36f6b..b544c50c8d 100644 --- a/contracts/plugins/assets/morpho-aave/MorphoAAVESelfReferentialCollateral.sol +++ b/contracts/plugins/assets/morpho-aave/MorphoAAVESelfReferentialCollateral.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; - -import "../AppreciatingFiatCollateral.sol"; -import "../../../libraries/Fixed.sol"; -import "./MorphoAAVEPositionWrapper.sol"; - +pragma solidity 0.8.19; +// solhint-disable-next-line max-line-length +import { IRewardable, Asset, AppreciatingFiatCollateral, CollateralConfig } from "../AppreciatingFiatCollateral.sol"; +import { MorphoAAVEPositionWrapper } from "./MorphoAAVEPositionWrapper.sol"; +import { FixLib, CEIL } from "../../../libraries/Fixed.sol"; +import { OracleLib } from "../OracleLib.sol"; +// solhint-disable-next-line max-line-length +import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; /** * @title MorphoAAVESelfReferentialCollateral @@ -15,14 +17,14 @@ contract MorphoAAVESelfReferentialCollateral is AppreciatingFiatCollateral { using OracleLib for AggregatorV3Interface; using FixLib for uint192; - MorphoAAVEPositionWrapper wrapper; + MorphoAAVEPositionWrapper public immutable wrapper; - /// @param config Configuration of this collateral. config.erc20 must be a MorphoAAVEPositionWrapper - /// @param revenue_hiding {1} A value like 1e-6 that represents the maximum refPerTok to hide - constructor( - CollateralConfig memory config, - uint192 revenue_hiding - ) AppreciatingFiatCollateral(config, revenue_hiding) { + /// @param config Configuration of this collateral. + /// config.erc20 must be a MorphoAAVEPositionWrapper + /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide + constructor(CollateralConfig memory config, uint192 revenueHiding) + AppreciatingFiatCollateral(config, revenueHiding) + { require(config.defaultThreshold == 0, "default threshold not supported"); require(address(config.erc20) != address(0), "missing erc20"); wrapper = MorphoAAVEPositionWrapper(address(config.erc20)); @@ -57,25 +59,12 @@ contract MorphoAAVESelfReferentialCollateral is AppreciatingFiatCollateral { /// @custom:interaction RCEI function refresh() public virtual override { // Update wrapper exchange rate for underlying token - wrapper.refresh_exchange_rate(); + wrapper.refreshExchangeRate(); super.refresh(); } /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens function _underlyingRefPerTok() internal view override returns (uint192) { - return wrapper.get_exchange_rate(); - } - - /// Claim rewards earned by holding a balance of the ERC20 token - /// @dev delegatecall - function claimRewards() external virtual override(Asset, IRewardable) { - // unfortunately Morpho uses a rewards scheme that requires the results - // of off-chain computation to be piped into an on-chain function, - // which is not possible to do with Reserve's collateral plugin interface. - - // https://integration.morpho.xyz/track-and-manage-position/manage-positions-on-morpho/claim-morpho-rewards - - // claiming rewards for this wrapper can be done by any account, and must be done on Morpho's rewards distributor contract - // https://etherscan.io/address/0x3b14e5c73e0a56d607a8688098326fd4b4292135 + return wrapper.getExchangeRate(); } } diff --git a/contracts/plugins/assets/morpho-aave/README.md b/contracts/plugins/assets/morpho-aave/README.md index c8b1100ff4..451582692d 100644 --- a/contracts/plugins/assets/morpho-aave/README.md +++ b/contracts/plugins/assets/morpho-aave/README.md @@ -9,7 +9,7 @@ For example for the USDT pool, the reference unit would be USDT, the target unit would be USD, and the collateral token would be an instance of MorphoAAVEPositionWrapper deployed alongside the plugin. -To deploy an instance of this plugin, you must deploy an instance of +To deploy an instance of this plugin, you must deploy an instance of MorphoAAVEPositionWrapper and then pass it as config to the MorphoAAVEFiatCollateral, MorphoAAVESelfReferentialCollateral, or MorphoAAVENonFiatCollateral, depending on the specific pool token that is @@ -31,3 +31,12 @@ is proportionately distributed to token burners. This plugin follows the established and accepted logic for cTokens for disabling the collateral plugin if it were to default. + +# Claiming rewards + +Unfortunately Morpho uses a rewards scheme that requires the results +of off-chain computation to be piped into an on-chain function, +which is not possible to do with Reserve's collateral plugin interface. +https://integration.morpho.xyz/track-and-manage-position/manage-positions-on-morpho/claim-morpho-rewards +claiming rewards for this wrapper can be done by any account, and must be done on Morpho's rewards distributor contract +https://etherscan.io/address/0x3b14e5c73e0a56d607a8688098326fd4b4292135 diff --git a/contracts/plugins/mocks/MorphoAAVEFiatCollateralMock.sol b/contracts/plugins/mocks/MorphoAAVEFiatCollateralMock.sol index d5ccd8aae5..9c630cd973 100644 --- a/contracts/plugins/mocks/MorphoAAVEFiatCollateralMock.sol +++ b/contracts/plugins/mocks/MorphoAAVEFiatCollateralMock.sol @@ -1,20 +1,16 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; -import "../assets/morpho-aave/MorphoAAVEPositionWrapper.sol"; -import "../../libraries/Fixed.sol"; +import { MorphoAAVEPositionWrapper, MorphoAAVEWrapperConfig } from "../assets/morpho-aave/MorphoAAVEPositionWrapper.sol"; contract MorphoAAVEPositionWrapperMock is MorphoAAVEPositionWrapper { - using FixLib for uint192; - uint192 private exchange_rate; - constructor(MorphoAAVEWrapperConfig memory config) MorphoAAVEPositionWrapper(config) {} - function set_exchange_rate(uint192 rate) external { - exchange_rate = rate; + function setExchangeRate(uint192 rate) external { + exchangeRate = rate; } - function get_exchange_rate() external override view returns (uint192) { - return exchange_rate; + function getExchangeRate() external view override returns (uint192) { + return exchangeRate; } } diff --git a/tasks/deployment/collateral/deploy-morpho-fiat-collateral.ts b/tasks/deployment/collateral/deploy-morpho-fiat-collateral.ts new file mode 100644 index 0000000000..2652b19ce4 --- /dev/null +++ b/tasks/deployment/collateral/deploy-morpho-fiat-collateral.ts @@ -0,0 +1,60 @@ +import { getChainId } from '../../../common/blockchain-utils' +import { task } from 'hardhat/config' + +interface Params { + priceTimeout: string + priceFeed: string + oracleError: string + wrapperToken: string + maxTradeVolume: string + oracleTimeout: string + targetName: string + defaultThreshold: string + delayUntilDefault: string + revenueHiding: string + noOutput?: boolean +} + +task('deploy-fiat-collateral', 'Deploys a Fiat Collateral') + .addParam('priceTimeout', 'The amount of time before a price decays to 0') + .addParam('priceFeed', 'Price Feed address') + .addParam('oracleError', 'The % error in the price feed as a fix') + .addParam('wrapperToken', 'ERC20 token address of the morpho position wrapper') + .addParam('maxTradeVolume', 'Max Trade Volume (in UoA)') + .addParam('oracleTimeout', 'Max oracle timeout') + .addParam('targetName', 'Target Name') + .addParam('defaultThreshold', 'Default Threshold') + .addParam('delayUntilDefault', 'Delay until default') + .addParam('revenueHiding', 'Revenue Hiding') + .setAction(async (params: Params, hre) => { + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + const FiatCollateralFactory = await hre.ethers.getContractFactory( + "MorphoAAVEFiatCollateral" + ) + + const collateral = await FiatCollateralFactory.connect(deployer).deploy({ + priceTimeout: params.priceTimeout, + chainlinkFeed: params.priceFeed, + oracleError: params.oracleError, + erc20: params.wrapperToken, + maxTradeVolume: params.maxTradeVolume, + oracleTimeout: params.oracleTimeout, + targetName: params.targetName, + defaultThreshold: params.defaultThreshold, + delayUntilDefault: params.delayUntilDefault, + }, + params.revenueHiding + ); + await collateral.deployed() + + if (!params.noOutput) { + console.log( + `Deployed Morpho Fiat Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + } + + return { collateral: collateral.address } + }) diff --git a/tasks/deployment/collateral/deploy-morpho-non-fiat-collateral.ts b/tasks/deployment/collateral/deploy-morpho-non-fiat-collateral.ts new file mode 100644 index 0000000000..c91f2cfc73 --- /dev/null +++ b/tasks/deployment/collateral/deploy-morpho-non-fiat-collateral.ts @@ -0,0 +1,67 @@ +import { getChainId } from '../../../common/blockchain-utils' +import { task } from 'hardhat/config' + +interface Params { + referenceUnitFeed: string + priceTimeout: string + targetUnitOracleTimeout: string + targetUnitFeed: string + combinedOracleError: string + wrapperToken: string + maxTradeVolume: string + oracleTimeout: string + targetName: string + defaultThreshold: string + delayUntilDefault: string + revenueHiding: string + + noOutput?: boolean +} + +task('deploy-fiat-collateral', 'Deploys a Fiat Collateral') + .addParam('referenceUnitFeed', 'Reference Price Feed address') + .addParam('priceTimeout', 'The amount of time before a price decays to 0') + .addParam('targetUnitOracleTimeout', 'The amount of time before a price decays to 0') + .addParam('targetUnitFeed', 'Target Unit Price Feed address') + .addParam('combinedOracleError', 'The combined % error from both oracle sources') + .addParam('wrapperToken', 'ERC20 token address of the wrapper token') + .addParam('maxTradeVolume', 'Max Trade Volume (in UoA)') + .addParam('oracleTimeout', 'Max oracle timeout') + .addParam('targetName', 'Target Name') + .addParam('defaultThreshold', 'Default Threshold') + .addParam('delayUntilDefault', 'Delay until default') + .addParam('revenueHiding', 'Revenue Hiding') + .setAction(async (params: Params, hre) => { + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + const FiatCollateralFactory = await hre.ethers.getContractFactory( + "MorphoAAVENonFiatCollateral" + ) + + const collateral = await FiatCollateralFactory.connect(deployer).deploy({ + priceTimeout: params.priceTimeout, + chainlinkFeed: params.referenceUnitFeed, + oracleError: params.combinedOracleError, + erc20: params.wrapperToken, + maxTradeVolume: params.maxTradeVolume, + oracleTimeout: params.oracleTimeout, + targetName: params.targetName, + defaultThreshold: params.defaultThreshold, + delayUntilDefault: params.delayUntilDefault, + }, + params.targetUnitFeed, + params.targetUnitOracleTimeout, + params.revenueHiding + ); + await collateral.deployed() + + if (!params.noOutput) { + console.log( + `Deployed Morpho Non-Fiat Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + } + + return { collateral: collateral.address } + }) diff --git a/tasks/deployment/collateral/deploy-morpho-self-referential-collateral.ts b/tasks/deployment/collateral/deploy-morpho-self-referential-collateral.ts new file mode 100644 index 0000000000..b80c328bc5 --- /dev/null +++ b/tasks/deployment/collateral/deploy-morpho-self-referential-collateral.ts @@ -0,0 +1,57 @@ +import { getChainId } from '../../../common/blockchain-utils' +import { task } from 'hardhat/config' +interface Params { + priceTimeout: string, + priceFeed: string, + oracleError: string, + maxTradeVolume: string, + wrapperToken: string, + oracleTimeout: string, + targetName: string, + revenueHiding: string, + referenceERC20Decimals: number, + + noOutput?: boolean +} + +task('deploy-ctoken-selfreferential-collateral', 'Deploys a CToken Self-referential Collateral') + .addParam('priceTimeout', 'The amount of time before a price decays to 0') + .addParam('priceFeed', 'Price Feed address') + .addParam('oracleError', 'The % error in the price feed as a fix') + .addParam('maxTradeVolume', 'Max Trade Volume (in UoA)') + .addParam('wrapperToken', 'ERC20 token address of the wrapper token') + .addParam('oracleTimeout', 'Max oracle timeout') + .addParam('targetName', 'Target Name') + .addParam('revenueHiding', 'Revenue Hiding') + .setAction(async (params: Params, hre) => { + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + const CTokenSelfReferentialCollateralFactory = await hre.ethers.getContractFactory( + "MorphoAAVESelfReferentialCollateral" + ) + + const collateral = await CTokenSelfReferentialCollateralFactory.connect(deployer).deploy( + { + priceTimeout: params.priceTimeout, + chainlinkFeed: params.priceFeed, + oracleError: params.oracleError, + erc20: params.wrapperToken, + maxTradeVolume: params.maxTradeVolume, + oracleTimeout: params.oracleTimeout, + targetName: params.targetName, + defaultThreshold: 0, + delayUntilDefault: 0, + }, + params.revenueHiding, + ) + await collateral.deployed(); + + if (!params.noOutput) { + console.log( + `Deployed Morpho Self-referential Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + } + return { collateral: collateral.address } + }) diff --git a/tasks/testing/upgrade-checker-utils/constants.ts b/tasks/testing/upgrade-checker-utils/constants.ts index eb64725251..1d214223fe 100644 --- a/tasks/testing/upgrade-checker-utils/constants.ts +++ b/tasks/testing/upgrade-checker-utils/constants.ts @@ -1,15 +1,17 @@ import { networkConfig } from '#/common/configuration' export const whales: { [key: string]: string } = { - [networkConfig['1'].tokens.USDT!.toLowerCase()]: '0x5754284f345afc66a98fbB0a0Afe71e0F007B949', + [networkConfig['1'].tokens.USDT!.toLowerCase()]: '0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503', + [networkConfig['1'].tokens.USDC!.toLowerCase()]: '0x756D64Dc5eDb56740fC617628dC832DDBCfd373c', + [networkConfig['1'].tokens.RSR!.toLowerCase()]: '0x6bab6EB87Aa5a1e4A8310C73bDAAA8A5dAAd81C1', [networkConfig['1'].tokens.cUSDT!.toLowerCase()]: '0x5754284f345afc66a98fbB0a0Afe71e0F007B949', [networkConfig['1'].tokens.aUSDT!.toLowerCase()]: '0x5754284f345afc66a98fbB0a0Afe71e0F007B949', ['0x21fe646D1Ed0733336F2D4d9b2FE67790a6099D9'.toLowerCase()]: '0x5754284f345afc66a98fbB0a0Afe71e0F007B949', // saUSDT - [networkConfig['1'].tokens.USDC!.toLowerCase()]: '0x51eDF02152EBfb338e03E30d65C15fBf06cc9ECC', [networkConfig['1'].tokens.aUSDC!.toLowerCase()]: '0x51eDF02152EBfb338e03E30d65C15fBf06cc9ECC', [networkConfig['1'].tokens.cUSDC!.toLowerCase()]: '0x51eDF02152EBfb338e03E30d65C15fBf06cc9ECC', ['0x60C384e226b120d93f3e0F4C502957b2B9C32B15'.toLowerCase()]: '0x51eDF02152EBfb338e03E30d65C15fBf06cc9ECC', // saUSDC - [networkConfig['1'].tokens.RSR!.toLowerCase()]: '0x6bab6EB87Aa5a1e4A8310C73bDAAA8A5dAAd81C1', + [networkConfig['1'].tokens.WBTC!.toLowerCase()]: '0x8eb8a3b98659cce290402893d0123abb75e3ab28', + [networkConfig['1'].tokens.stETH!.toLowerCase()]: '0x176F3DAb24a159341c0509bB36B833E7fdd0a132', } export const collateralToUnderlying: { [key: string]: string } = { diff --git a/tasks/testing/upgrade-checker-utils/trades.ts b/tasks/testing/upgrade-checker-utils/trades.ts index fd3e1aa040..541056ecf3 100644 --- a/tasks/testing/upgrade-checker-utils/trades.ts +++ b/tasks/testing/upgrade-checker-utils/trades.ts @@ -5,11 +5,9 @@ import { TestITrading } from '@typechain/TestITrading' import { BigNumber } from 'ethers' import { HardhatRuntimeEnvironment } from 'hardhat/types' import { QUEUE_START } from '#/common/constants' +import { whales } from './constants' import { bn, fp } from '#/common/numbers' import { logToken } from './logs' -import { collateralToUnderlying, whales } from "./constants" -import { networkConfig } from "#/common/configuration" -import { ERC20Mock } from "@typechain/ERC20Mock" export const runTrade = async ( hre: HardhatRuntimeEnvironment, @@ -42,10 +40,8 @@ export const runTrade = async ( buyAmount = buyAmount.add(fp('1').div(bn(10 ** (18 - buyDecimals)))) const gnosis = await hre.ethers.getContractAt('EasyAuction', await trade.gnosis()) - console.log('impersonate', whales[buyTokenAddress.toLowerCase()], buyTokenAddress) await whileImpersonating(hre, whales[buyTokenAddress.toLowerCase()], async (whale) => { const sellToken = await hre.ethers.getContractAt('ERC20Mock', buyTokenAddress) - // await mintTokensIfNeeded(hre, buyTokenAddress, buyAmount, whale.address) await sellToken.connect(whale).approve(gnosis.address, buyAmount) await gnosis .connect(whale) @@ -63,41 +59,3 @@ export const runTrade = async ( await trader.settleTrade(tradeToken) console.log(`Settled trade for ${logToken(buyTokenAddress)}.`) } - -// impersonate the whale to get the token -const mintTokensIfNeeded = async (hre: HardhatRuntimeEnvironment, tokenAddress: string, amount: BigNumber, recipient: string) => { - switch (tokenAddress) { - case networkConfig['1'].tokens.aUSDC: - case networkConfig['1'].tokens.aUSDT: - await mintAToken(hre, tokenAddress, amount, recipient) - case networkConfig['1'].tokens.cUSDC: - case networkConfig['1'].tokens.cUSDT: - await mintCToken(hre, tokenAddress, amount, recipient) - default: - return - } -} - -const mintCToken = async (hre: HardhatRuntimeEnvironment, tokenAddress: string, amount: BigNumber, recipient: string) => { - const collateral = await hre.ethers.getContractAt('ICToken', tokenAddress) - const underlying = await hre.ethers.getContractAt('ERC20Mock', collateralToUnderlying[tokenAddress.toLowerCase()]) - await whileImpersonating(hre, whales[tokenAddress.toLowerCase()], async (whaleSigner) => { - console.log('0', amount, recipient, collateral.address, underlying.address, whaleSigner.address) - await underlying.connect(whaleSigner).approve(collateral.address, amount) - console.log('1', amount, recipient) - await collateral.connect(whaleSigner).mint(amount) - console.log('2', amount, recipient) - const bal = await collateral.balanceOf(whaleSigner.address) - console.log('3', amount, recipient, bal) - await collateral.connect(whaleSigner).transfer(recipient, bal) - }) -} - -const mintAToken = async (hre: HardhatRuntimeEnvironment, tokenAddress: string, amount: BigNumber, recipient: string) => { - const collateral = await hre.ethers.getContractAt('StaticATokenLM', tokenAddress) - const underlying = await hre.ethers.getContractAt('ERC20Mock', collateralToUnderlying[tokenAddress.toLowerCase()]) - await whileImpersonating(hre, whales[tokenAddress.toLowerCase()], async (usdtSigner) => { - await underlying.connect(usdtSigner).approve(collateral.address, amount) - await collateral.connect(usdtSigner).deposit(recipient, amount, 0, true) - }) -} diff --git a/tasks/testing/upgrade-checker-utils/upgrades/2_1_0.ts b/tasks/testing/upgrade-checker-utils/upgrades/2_1_0.ts index b0e60f5665..ae507db89a 100644 --- a/tasks/testing/upgrade-checker-utils/upgrades/2_1_0.ts +++ b/tasks/testing/upgrade-checker-utils/upgrades/2_1_0.ts @@ -167,27 +167,16 @@ export default async ( const gnosis = await hre.ethers.getContractAt('EasyAuction', await trade.gnosis()) await whileImpersonating(hre, whales[buyTokenAddress.toLowerCase()], async (whale) => { const sellToken = await hre.ethers.getContractAt('ERC20Mock', buyTokenAddress) - let repeat = true - while(repeat) { - try { - await sellToken.connect(whale).approve(gnosis.address, 0) - await sellToken.connect(whale).approve(gnosis.address, buyAmount) - await gnosis - .connect(whale) - .placeSellOrders( - auctionId, - [sellAmount], - [buyAmount], - [QUEUE_START], - hre.ethers.constants.HashZero - ) - repeat = false - } catch (e) { - console.log(e) - buyAmount = buyAmount.add(1) - console.log('Trying again...') - } - } + await sellToken.connect(whale).approve(gnosis.address, buyAmount) + await gnosis + .connect(whale) + .placeSellOrders( + auctionId, + [sellAmount], + [buyAmount], + [QUEUE_START], + hre.ethers.constants.HashZero + ) }) const lastTimestamp = await getLatestBlockTimestamp(hre) diff --git a/tasks/testing/upgrade-checker.ts b/tasks/testing/upgrade-checker.ts index 3d766a2765..3e19c90dd2 100644 --- a/tasks/testing/upgrade-checker.ts +++ b/tasks/testing/upgrade-checker.ts @@ -179,31 +179,6 @@ task('upgrade-checker', 'Mints all the tokens to an address') console.log('successfully minted RTokens') - - // get saUsdt - await whileImpersonating(hre, whales[networkConfig['1'].tokens.USDT!.toLowerCase()], async (usdtSigner) => { - await usdt.connect(usdtSigner).approve(saUsdt.address, initialBal.mul(20)) - await saUsdt.connect(usdtSigner).deposit(usdtSigner.address, initialBal.mul(20), 0, true) - }) - - // get cUsdt - await whileImpersonating(hre, whales[networkConfig['1'].tokens.USDT!.toLowerCase()], async (usdtSigner) => { - console.log(cUsdt.address, usdt.address, usdtSigner.address) - await usdt.connect(usdtSigner).approve(cUsdt.address, initialBal.mul(20)) - await cUsdt.connect(usdtSigner).mint(initialBal.mul(20)) - }) - - // get saUsdc - await whileImpersonating(hre, whales[networkConfig['1'].tokens.USDC!.toLowerCase()], async (usdcSigner) => { - await usdc.connect(usdcSigner).approve(saUsdc.address, initialBal.mul(20)) - await saUsdc.connect(usdcSigner).deposit(usdcSigner.address, initialBal.mul(20), 0, true) - }) - - // get cUsdc - await whileImpersonating(hre, whales[networkConfig['1'].tokens.USDC!.toLowerCase()], async (usdcSigner) => { - await usdc.connect(usdcSigner).approve(cUsdc.address, initialBal.mul(20)) - await cUsdc.connect(usdcSigner).mint(initialBal.mul(20)) - }) /* redeem diff --git a/test/plugins/individual-collateral/morpho-aave/MAAVEFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MAAVEFiatCollateral.test.ts deleted file mode 100644 index 2e7580fb8a..0000000000 --- a/test/plugins/individual-collateral/morpho-aave/MAAVEFiatCollateral.test.ts +++ /dev/null @@ -1,739 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { BigNumber, ContractFactory } from 'ethers' -import hre, { ethers } from 'hardhat' -import { IMPLEMENTATION, ORACLE_ERROR, PRICE_TIMEOUT, REVENUE_HIDING } from '../../../fixtures' -import { defaultFixture, ORACLE_TIMEOUT } from '../fixtures' -import { getChainId } from '../../../../common/blockchain-utils' -import forkBlockNumber from '../../../integration/fork-block-numbers' -import { - IConfig, - IGovParams, - IRevenueShare, - IRTokenConfig, - IRTokenSetup, - networkConfig, -} from '../../../../common/configuration' -import { CollateralStatus, MAX_UINT48, ZERO_ADDRESS } from '../../../../common/constants' -import { expectEvents, expectInIndirectReceipt } from '../../../../common/events' -import { bn, fp, toBNDecimals } from '../../../../common/numbers' -import { whileImpersonating } from '../../../utils/impersonation' -import { - expectPrice, - expectRTokenPrice, - expectUnpriced, - setOraclePrice, -} from '../../../utils/oracles' -import { advanceBlocks, advanceTime, getLatestBlockTimestamp } from '../../../utils/time' -import { - Asset, - MorphoAAVEFiatCollateral, - ERC20Mock, - FacadeRead, - FacadeTest, - FacadeWrite, - IAssetRegistry, - IBasketHandler, - InvalidMockV3Aggregator, - MockV3Aggregator, - RTokenAsset, - TestIBackingManager, - TestIDeployer, - TestIMain, - TestIRToken, - MorphoAAVEPositionWrapper, - MorphoAAVEPositionWrapperMock, -} from '../../../../typechain' -import { useEnv } from '#/utils/env' - -// Setup test environment -const setup = async (blockNumber: number) => { - // Use Mainnet fork - await hre.network.provider.request({ - method: 'hardhat_reset', - params: [ - { - forking: { - jsonRpcUrl: useEnv('MAINNET_RPC_URL'), - blockNumber: blockNumber, - }, - }, - ], - }) -} - -// Holder address in Mainnet -const holderUSDT = '0xd6216fc19db775df9774a6e33526131da7d19a2c' - -const NO_PRICE_DATA_FEED = '0x51597f405303C4377E36123cBc172b13269EA163' - -const describeFork = useEnv('FORK') ? describe : describe.skip - -describeFork(`MAAVEFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, function () { - let owner: SignerWithAddress - let addr1: SignerWithAddress - let addr2: SignerWithAddress - - // Tokens/Assets - let usdt: ERC20Mock - - let usdtMorphoPlugin: MorphoAAVEFiatCollateral - let usdtMorphoWrapper: MorphoAAVEPositionWrapper - - let rsr: ERC20Mock - let rsrAsset: Asset - - // Core Contracts - let main: TestIMain - let rToken: TestIRToken - let rTokenAsset: RTokenAsset - let assetRegistry: IAssetRegistry - let backingManager: TestIBackingManager - let basketHandler: IBasketHandler - - let deployer: TestIDeployer - let facade: FacadeRead - let facadeTest: FacadeTest - let facadeWrite: FacadeWrite - let govParams: IGovParams - - // RToken Configuration - const dist: IRevenueShare = { - rTokenDist: bn(40), // 2/5 RToken - rsrDist: bn(60), // 3/5 RSR - } - const config: IConfig = { - dist: dist, - minTradeVolume: fp('1e4'), // $10k - rTokenMaxTradeVolume: fp('1e6'), // $1M - shortFreeze: bn('259200'), // 3 days - longFreeze: bn('2592000'), // 30 days - rewardRatio: bn('1069671574938'), // approx. half life of 90 days - unstakingDelay: bn('1209600'), // 2 weeks - tradingDelay: bn('0'), // (the delay _after_ default has been confirmed) - auctionLength: bn('900'), // 15 minutes - backingBuffer: fp('0.0001'), // 0.01% - maxTradeSlippage: fp('0.01'), // 1% - issuanceThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% - }, - redemptionThrottle: { - amtRate: fp('1e6'), // 1M RToken - pctRate: fp('0.05'), // 5% - }, - } - - const defaultThreshold = fp('0.01') // 1% - const delayUntilDefault = bn('86400') // 24h - - let initialBal: BigNumber - - let chainId: number - - let MorphoAAVECollateralFactory: ContractFactory - let MorphoAAVEPositionWrapperFactory: ContractFactory - let MockV3AggregatorFactory: ContractFactory - let mockChainlinkFeed: MockV3Aggregator - - before(async () => { - await setup(forkBlockNumber['morpho-aave']) // https://etherscan.io/block/16859314, March 19 2023 - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - }) - - beforeEach(async () => { - ;[owner, addr1, addr2] = await ethers.getSigners() - ;({ rsr, rsrAsset, deployer, facade, facadeTest, facadeWrite, govParams } = await loadFixture( - defaultFixture - )) - - // Get required contracts for USDT - // USDT token - usdt = ( - await ethers.getContractAt('ERC20Mock', networkConfig[chainId].tokens.USDT || '') - ) - - //TODO: Add support for MORPHO rewards once chainlink releases MORPHO / USDT - - MorphoAAVEPositionWrapperFactory = await ethers.getContractFactory('MorphoAAVEPositionWrapper') - usdtMorphoWrapper = await MorphoAAVEPositionWrapperFactory.deploy( - { - morpho_controller: networkConfig[chainId].MORPHO_AAVE_CONTROLLER, - morpho_lens: networkConfig[chainId].MORPHO_AAVE_LENS, - underlying_erc20: usdt.address, - pool_token: networkConfig[chainId].tokens.aUSDT, - underlying_symbol: ethers.utils.formatBytes32String('USDT') - } - ) - - MorphoAAVECollateralFactory = await ethers.getContractFactory('MorphoAAVEFiatCollateral') - usdtMorphoPlugin = await MorphoAAVECollateralFactory.deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDT as string, - oracleError: ORACLE_ERROR, - erc20: usdtMorphoWrapper.address, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('USD'), - defaultThreshold, - delayUntilDefault, - }, - REVENUE_HIDING, - ) - - // Setup balances for addr1 - Transfer from Mainnet holder - // wBTC - // Send 10000 usdt from rich acct to addr1 - initialBal = bn('10000e6') - await whileImpersonating(holderUSDT, async (usdtHolderSigner) => { - await usdt.connect(usdtHolderSigner).transfer(addr1.address, initialBal) - }) - - // Send 10000 usdt from rich acct to addr2 - await whileImpersonating(holderUSDT, async (usdtHolderSigner) => { - await usdt.connect(usdtHolderSigner).transfer(addr2.address, initialBal) - }) - - // Set parameters - const rTokenConfig: IRTokenConfig = { - name: 'RTKN RToken', - symbol: 'RTKN', - mandate: 'mandate', - params: config, - } - - // Set primary basket - const rTokenSetup: IRTokenSetup = { - //TODO: Add morpho reward token as asset - assets: [], - primaryBasket: [usdtMorphoPlugin.address], - weights: [fp('1')], - backups: [], - beneficiaries: [], - } - - // Deploy RToken via FacadeWrite - const receipt = await ( - await facadeWrite.connect(owner).deployRToken(rTokenConfig, rTokenSetup) - ).wait() - - // Get Main - const mainAddr = expectInIndirectReceipt(receipt, deployer.interface, 'RTokenCreated').args.main - main = await ethers.getContractAt('TestIMain', mainAddr) - - // Get core contracts - assetRegistry = ( - await ethers.getContractAt('IAssetRegistry', await main.assetRegistry()) - ) - backingManager = ( - await ethers.getContractAt('TestIBackingManager', await main.backingManager()) - ) - basketHandler = ( - await ethers.getContractAt('IBasketHandler', await main.basketHandler()) - ) - rToken = await ethers.getContractAt('TestIRToken', await main.rToken()) - rTokenAsset = ( - await ethers.getContractAt('RTokenAsset', await assetRegistry.toAsset(rToken.address)) - ) - - // Setup owner and unpause - await facadeWrite.connect(owner).setupGovernance( - rToken.address, - false, // do not deploy governance - true, // unpaused - govParams, // mock values, not relevant - owner.address, // owner - ZERO_ADDRESS, // no guardian - ZERO_ADDRESS // no pauser - ) - - //console.log(await usdtMorphoWrapper.test_underlying_to_fix()) - - // Setup mock chainlink feed for some of the tests (so we can change the value) - MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') - mockChainlinkFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - }) - - describe('Deployment', () => { - // Check the initial state - it('Should setup RToken, Assets, and Collateral correctly', async () => { - // Check Rewards assets (if applies) (no rewards) - - // Check Wrapper Deployment - expect(await usdtMorphoWrapper.decimals()).to.equal(18) - expect(await usdtMorphoWrapper.get_exchange_rate()).to.equal(fp('1')) - - // Check Collateral plugin - // maUSDT (MorphoAAVEFiatCollateral) - expect(await usdtMorphoPlugin.isCollateral()).to.equal(true) - expect(await usdtMorphoPlugin.erc20()).to.equal(usdtMorphoWrapper.address) - expect(await usdtMorphoPlugin.targetName()).to.equal(ethers.utils.formatBytes32String('USD')) - expect(await usdtMorphoPlugin.refPerTok()).to.be.closeTo(fp('1'), fp('0.001')) - expect(await usdtMorphoPlugin.targetPerRef()).to.equal(fp('1')) - expect(await usdtMorphoPlugin.exposedReferencePrice()).to.equal( - await usdtMorphoPlugin.refPerTok() - ) - - await expectPrice( - usdtMorphoPlugin.address, - fp('1.00278919'), - ORACLE_ERROR, - true, - bn('1e5') - ) // close to $1.00278919 cents - - expect(await usdtMorphoPlugin.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) - - // Should setup contracts - expect(main.address).to.not.equal(ZERO_ADDRESS) - expect(usdtMorphoPlugin.address).to.not.equal(ZERO_ADDRESS) - expect(usdtMorphoWrapper.address).to.not.equal(ZERO_ADDRESS) - }) - - // Check assets/collaterals in the Asset Registry - it('Should register ERC20s and Assets/Collateral correctly', async () => { - // Check assets/collateral - const ERC20s = await assetRegistry.erc20s() - expect(ERC20s[0]).to.equal(rToken.address) - expect(ERC20s[1]).to.equal(rsr.address) - expect(ERC20s[2]).to.equal(usdtMorphoWrapper.address) - expect(ERC20s.length).to.eql(3) - - // Assets - expect(await assetRegistry.toAsset(ERC20s[0])).to.equal(rTokenAsset.address) - expect(await assetRegistry.toAsset(ERC20s[1])).to.equal(rsrAsset.address) - expect(await assetRegistry.toAsset(ERC20s[2])).to.equal(usdtMorphoPlugin.address) - - // Collaterals - expect(await assetRegistry.toColl(ERC20s[2])).to.equal(usdtMorphoPlugin.address) - }) - - // Check RToken basket - it('Should register Basket correctly', async () => { - // Basket - expect(await basketHandler.fullyCollateralized()).to.equal(true) - const backing = await facade.basketTokens(rToken.address) - expect(backing[0]).to.equal(usdtMorphoWrapper.address) - expect(backing.length).to.equal(1) - - // Check other values - expect(await basketHandler.timestamp()).to.be.gt(bn(0)) - expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) - expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.equal(0) - - // Check RToken price - await expectPrice( - basketHandler.address, - fp('1.00278919'), - ORACLE_ERROR, - true, - bn('1e5') - ) - - // Approve usdtMorphoWrapper to spend 5000 of addr1 usdt, then mint usdtMorphoWrapper - await usdt.connect(addr1).approve(usdtMorphoWrapper.address, bn("5000e6")) - await usdtMorphoWrapper.connect(addr1).mint(addr1.address, bn("100e18")) - - // Addr1 approves rToken to spend its wrapper tokens - await usdtMorphoWrapper.connect(addr1).approve(rToken.address, bn("100e18")) - - // Issue tokens and check price - await advanceTime(3600) - await expect(rToken.connect(addr1).issue(bn("100e18"))).to.emit(rToken, 'Issuance') - await expectRTokenPrice( - rTokenAsset.address, - fp('1.00278919'), - ORACLE_ERROR, - await backingManager.maxTradeSlippage(), - config.minTradeVolume.mul((await assetRegistry.erc20s()).length) - ) - }) - - // Validate constructor arguments - // Note: Adapt it to your plugin constructor validations - it('Should validate constructor arguments correctly', async () => { - // Missing erc20 - await expect( - MorphoAAVECollateralFactory.deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI as string, - oracleError: ORACLE_ERROR, - erc20: ZERO_ADDRESS, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('USD'), - defaultThreshold, - delayUntilDefault, - }, - REVENUE_HIDING, - ) - ).to.be.revertedWith('missing erc20') - }) - }) - - describe('Issuance/Appreciation/Redemption', () => { - const MIN_ISSUANCE_PER_BLOCK = bn('10000e18') - - // Issuance and redemption, making the collateral appreciate over time - it('Should issue, redeem, and handle appreciation rates correctly', async () => { - const issueAmount: BigNumber = MIN_ISSUANCE_PER_BLOCK // instant issuance - - // Approve usdtMorphoWrapper to spend 5000 of addr1 usdt, then mint usdtMorphoWrapper - await usdt.connect(addr1).approve(usdtMorphoWrapper.address, bn("10000e6")) - await usdtMorphoWrapper.connect(addr1).mint(addr1.address, issueAmount) - - // Addr1 approves rToken to spend its wrapper tokens - await usdtMorphoWrapper.connect(addr1).approve(rToken.address, issueAmount); - - await advanceTime(3600) - - // Issue rTokens - await expect(rToken.connect(addr1).issue(issueAmount)).to.emit(rToken, 'Issuance') - - // Check RTokens issued to user - expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) - - // Store Balances after issuance - const balanceAddr1USDT: BigNumber = await usdtMorphoWrapper.balanceOf(addr1.address) - - // Check rates and prices - const [usdtPriceLow1, usdtPriceHigh1] = await usdtMorphoPlugin.price() // ~ 0.022015 cents - const usdtRefPerTok1: BigNumber = await usdtMorphoPlugin.refPerTok() // ~ 0.022015 cents - - await expectPrice( - usdtMorphoPlugin.address, - fp('1.00278919'), - ORACLE_ERROR, - true, - bn('1e5') - ) - expect(usdtRefPerTok1).to.be.closeTo(fp('1'), fp('0.001')) - - // Check total asset value - const totalAssetValue1: BigNumber = await facadeTest.callStatic.totalAssetValue( - rToken.address - ) - expect(totalAssetValue1).to.be.closeTo(issueAmount, fp('150')) // approx 10K in value - - // Advance time and blocks slightly, causing refPerTok() to increase - await advanceTime(10000) - await advanceBlocks(10000) - - // Refresh cToken manually (required) - await usdtMorphoPlugin.refresh() - expect(await usdtMorphoPlugin.status()).to.equal(CollateralStatus.SOUND) - - // Check rates and prices - Have changed, slight inrease - const [usdtPriceLow2, usdtPriceHigh2] = await usdtMorphoPlugin.price() // ~0.022016 - const usdtRefPerTok2: BigNumber = await usdtMorphoPlugin.refPerTok() // ~0.022016 - - // Check rates and price increase - expect(usdtPriceLow2).to.be.gt(usdtPriceLow1) - expect(usdtPriceHigh2).to.be.gt(usdtPriceHigh1) - expect(usdtRefPerTok2).to.be.gt(usdtRefPerTok1) - - // Still close to the original values - await expectPrice( - usdtMorphoPlugin.address, - fp('1.00278919'), - ORACLE_ERROR, - true, - bn('1e3') - ) - expect(usdtRefPerTok2).to.be.closeTo(fp('1'), fp('0.001')) - - // Check total asset value increased - const totalAssetValue2: BigNumber = await facadeTest.callStatic.totalAssetValue( - rToken.address - ) - expect(totalAssetValue2).to.be.gt(totalAssetValue1) - - // Advance time and blocks slightly, causing refPerTok() to increase - await advanceTime(100000000) - await advanceBlocks(100000000) - - // Refresh cToken manually (required) - await usdtMorphoPlugin.refresh() - expect(await usdtMorphoPlugin.status()).to.equal(CollateralStatus.SOUND) - - // Check rates and prices - Have changed significantly - const [usdtPriceLow3, usdtPriceHigh3] = await usdtMorphoPlugin.price() // ~0.03294 - const usdtRefPerTok3: BigNumber = await usdtMorphoPlugin.refPerTok() // ~0.03294 - - // Check rates and price increase - expect(usdtPriceLow3).to.be.gt(usdtPriceLow2) - expect(usdtPriceHigh3).to.be.gt(usdtPriceHigh2) - expect(usdtRefPerTok3).to.be.gt(usdtRefPerTok2) - - expect(usdtRefPerTok3).to.be.closeTo(fp('1.14492'), fp('0.001')) - await expectPrice( - usdtMorphoPlugin.address, - fp('1.148122554980383617'), - ORACLE_ERROR, - true, - bn('1e5') - ) - - // Check total asset value increased - const totalAssetValue3: BigNumber = await facadeTest.callStatic.totalAssetValue( - rToken.address - ) - expect(totalAssetValue3).to.be.gt(totalAssetValue2) - - // Redeem Rtokens with the updated rates - await expect(rToken.connect(addr1).redeem(issueAmount, await basketHandler.nonce())).to.emit( - rToken, - 'Redemption' - ) - - // Check funds were transferred - expect(await rToken.balanceOf(addr1.address)).to.equal(0) - expect(await rToken.totalSupply()).to.equal(0) - - // Check balances - Fewer cTokens should have been sent to the user - const newBalanceAddr1usdtWrapper: BigNumber = await usdtMorphoWrapper.balanceOf(addr1.address) - - // Check received tokens represent ~10K in value at current prices - expect(newBalanceAddr1usdtWrapper.sub(balanceAddr1USDT)).to.be.closeTo(bn('8734.1650501e18'), bn('0.01e18')) // ~8734.1650501 * 1.14812 ~= 10K (100% of basket) - - // Check remainders in Backing Manager - expect(await usdtMorphoWrapper.balanceOf(backingManager.address)).to.be.closeTo(bn('1265.8104e18'), bn('0.01e18')) // ~= 1453.8 usd in value - - // Check total asset value (remainder) - expect(await facadeTest.callStatic.totalAssetValue(rToken.address)).to.be.closeTo( - fp('1453.8'), // ~= 1453.8 usd (from above) - fp('0.5') - ) - }) - }) - - // Note: Even if the collateral does not provide reward tokens, this test should be performed to check that - // claiming calls throughout the protocol are handled correctly and do not revert. - describe('Rewards', () => { - it('Should be able to claim rewards (if applicable)', async () => { - // Claim rewards - await expect(backingManager.claimRewards()).to.not.emit(backingManager, 'RewardsClaimed').and.to.not.be.reverted - }) - }) - - describe('Price Handling', () => { - it('Should handle invalid/stale Price', async () => { - // Does not revert with stale price - await advanceTime(ORACLE_TIMEOUT.toString()) - - // Compound - await expectUnpriced(usdtMorphoPlugin.address) - - // Refresh should mark status IFFY - await usdtMorphoPlugin.refresh() - expect(await usdtMorphoPlugin.status()).to.equal(CollateralStatus.IFFY) - - // CTokens Collateral with no price - const nonpriceCtokenCollateral: MorphoAAVEFiatCollateral = await ( - await ethers.getContractFactory('MorphoAAVEFiatCollateral') - ).deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: NO_PRICE_DATA_FEED, - oracleError: ORACLE_ERROR, - erc20: usdtMorphoWrapper.address, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('USD'), - defaultThreshold, - delayUntilDefault, - }, - REVENUE_HIDING, - ) - - // CTokens - Collateral with no price info should revert - await expect(nonpriceCtokenCollateral.price()).to.be.reverted - - // Refresh should also revert - status is not modified - await expect(nonpriceCtokenCollateral.refresh()).to.be.reverted - expect(await nonpriceCtokenCollateral.status()).to.equal(CollateralStatus.SOUND) - - // Does not revert with zero price - const zeropriceCtokenCollateral: MorphoAAVEFiatCollateral = await ( - await ethers.getContractFactory('MorphoAAVEFiatCollateral') - ).deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: mockChainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: usdtMorphoWrapper.address, - maxTradeVolume: config.rTokenMaxTradeVolume, - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('USD'), - defaultThreshold, - delayUntilDefault, - }, - REVENUE_HIDING, - ) - - await setOraclePrice(zeropriceCtokenCollateral.address, bn(0)) - - // Does not revert with zero price - await expectPrice(zeropriceCtokenCollateral.address, bn('0'), bn('0'), false) - - // Refresh should mark status IFFY - await zeropriceCtokenCollateral.refresh() - expect(await zeropriceCtokenCollateral.status()).to.equal(CollateralStatus.IFFY) - }) - }) - - // Note: Here the idea is to test all possible statuses and check all possible paths to default - // soft default = SOUND -> IFFY -> DISABLED due to sustained misbehavior - // hard default = SOUND -> DISABLED due to an invariant violation - // This may require to deploy some mocks to be able to force some of these situations - describe('Collateral Status', () => { - // Test for soft default - it('Updates status in case of soft default', async () => { - // Redeploy plugin using a Chainlink mock feed where we can change the price - const newUSDTCollateral: MorphoAAVEFiatCollateral = await ( - await ethers.getContractFactory('MorphoAAVEFiatCollateral') - ).deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: mockChainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: await usdtMorphoPlugin.erc20(), - maxTradeVolume: await usdtMorphoPlugin.maxTradeVolume(), - oracleTimeout: await usdtMorphoPlugin.oracleTimeout(), - targetName: await usdtMorphoPlugin.targetName(), - defaultThreshold, - delayUntilDefault: await usdtMorphoPlugin.delayUntilDefault(), - }, - REVENUE_HIDING - ) - - // Check initial state - expect(await newUSDTCollateral.status()).to.equal(CollateralStatus.SOUND) - expect(await newUSDTCollateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg one of the underlying tokens - Reducing price 20% - await setOraclePrice(newUSDTCollateral.address, bn('8e7')) // -20% - - // Force updates - Should update whenDefault and status - await expect(newUSDTCollateral.refresh()) - .to.emit(newUSDTCollateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await newUSDTCollateral.status()).to.equal(CollateralStatus.IFFY) - - const expectedDefaultTimestamp: BigNumber = bn(await getLatestBlockTimestamp()).add( - delayUntilDefault - ) - expect(await newUSDTCollateral.whenDefault()).to.equal(expectedDefaultTimestamp) - - // Move time forward past delayUntilDefault - await advanceTime(Number(delayUntilDefault)) - expect(await newUSDTCollateral.status()).to.equal(CollateralStatus.DISABLED) - - // Nothing changes if attempt to refresh after default - // CToken - const prevWhenDefault: BigNumber = await newUSDTCollateral.whenDefault() - await expect(newUSDTCollateral.refresh()).to.not.emit( - newUSDTCollateral, - 'CollateralStatusChanged' - ) - expect(await newUSDTCollateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await newUSDTCollateral.whenDefault()).to.equal(prevWhenDefault) - }) - - // Test for hard default - it('Updates status in case of hard default', async () => { - // Note: In this case requires to use a CToken mock to be able to change the rate - const MorphoAAVEPositionWrapperMockFactory: ContractFactory = await ethers.getContractFactory('MorphoAAVEPositionWrapperMock') - const usdtMorphoWrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy( - { - morpho_controller: networkConfig[chainId].MORPHO_AAVE_CONTROLLER, - morpho_lens: networkConfig[chainId].MORPHO_AAVE_LENS, - underlying_erc20: usdt.address, - pool_token: networkConfig[chainId].tokens.aUSDT, - underlying_symbol: ethers.utils.formatBytes32String('WBTC') - } - ) - - // Set initial exchange rate to the new USDT Mock - await usdtMorphoWrapperMock.set_exchange_rate(fp('1')) - - // Redeploy plugin using the new USDT mock - const newUSDTCollateral: MorphoAAVEFiatCollateral = await ( - await ethers.getContractFactory('MorphoAAVEFiatCollateral') - ).deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: mockChainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: usdtMorphoWrapperMock.address, - maxTradeVolume: await usdtMorphoPlugin.maxTradeVolume(), - oracleTimeout: await usdtMorphoPlugin.oracleTimeout(), - targetName: await usdtMorphoPlugin.targetName(), - defaultThreshold, - delayUntilDefault: await usdtMorphoPlugin.delayUntilDefault(), - }, - REVENUE_HIDING - ) - await newUSDTCollateral.refresh() - - // Check initial state - expect(await newUSDTCollateral.status()).to.equal(CollateralStatus.SOUND) - expect(await newUSDTCollateral.whenDefault()).to.equal(MAX_UINT48) - - // Decrease rate for USDT, will disable collateral immediately - await usdtMorphoWrapperMock.set_exchange_rate(fp('0.9')) - - // Force updates - Should update whenDefault and status for Atokens/CTokens - await expect(newUSDTCollateral.refresh()) - .to.emit(newUSDTCollateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.DISABLED) - - expect(await newUSDTCollateral.status()).to.equal(CollateralStatus.DISABLED) - const expectedDefaultTimestamp: BigNumber = bn(await getLatestBlockTimestamp()) - expect(await newUSDTCollateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('Reverts if oracle reverts or runs out of gas, maintains status', async () => { - const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( - 'InvalidMockV3Aggregator' - ) - const invalidChainlinkFeed: InvalidMockV3Aggregator = ( - await InvalidMockV3AggregatorFactory.deploy(8, bn('1e8')) - ) - - const invalidCTokenCollateral: MorphoAAVEFiatCollateral = ( - await MorphoAAVECollateralFactory.deploy( - { - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: invalidChainlinkFeed.address, - oracleError: ORACLE_ERROR, - erc20: await usdtMorphoPlugin.erc20(), - maxTradeVolume: await usdtMorphoPlugin.maxTradeVolume(), - oracleTimeout: await usdtMorphoPlugin.oracleTimeout(), - targetName: await usdtMorphoPlugin.targetName(), - defaultThreshold, - delayUntilDefault: await usdtMorphoPlugin.delayUntilDefault(), - }, - REVENUE_HIDING, - ) - ) - - // Reverting with no reason - await invalidChainlinkFeed.setSimplyRevert(true) - await expect(invalidCTokenCollateral.refresh()).to.be.reverted - expect(await invalidCTokenCollateral.status()).to.equal(CollateralStatus.SOUND) - - // Runnning out of gas (same error) - await invalidChainlinkFeed.setSimplyRevert(false) - await expect(invalidCTokenCollateral.refresh()).to.be.reverted - expect(await invalidCTokenCollateral.status()).to.equal(CollateralStatus.SOUND) - }) - }) -}) diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts new file mode 100644 index 0000000000..778c453ef5 --- /dev/null +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVEFiatCollateral.test.ts @@ -0,0 +1,225 @@ +import { networkConfig } from '#/common/configuration' +import { bn, fp } from '#/common/numbers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MockV3Aggregator } from '@typechain/MockV3Aggregator' +import { TestICollateral } from '@typechain/TestICollateral' +import { ERC20Mock, MockV3Aggregator__factory } from '@typechain/index' +import { expect } from 'chai' +import { BigNumber, BigNumberish, ContractFactory } from 'ethers' +import { ethers } from 'hardhat' +import collateralTests from '../collateralTests' +import { getResetFork } from '../helpers' +import { CollateralOpts } from '../pluginTestTypes' +import { + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + FORK_BLOCK, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + USDC_USD_PRICE_FEED, +} from './constants' +import { MorphoAaveCollateralFixtureContext, mintCollateralTo } from './mintCollateralTo' + +interface MAFiatCollateralOpts extends CollateralOpts { + underlyingToken?: string + poolToken?: string + defaultPrice?: BigNumberish + defaultRefPerTok?: BigNumberish +} + +export const deployCollateral = async ( + opts: MAFiatCollateralOpts = {} +): Promise => { + opts = { ...defaultCollateralOpts, ...opts } + + const MorphoAAVECollateralFactory: ContractFactory = await ethers.getContractFactory( + 'MorphoAAVEFiatCollateral' + ) + if (opts.erc20 == null) { + const MorphoAAVEPositionWrapperMockFactory = await ethers.getContractFactory( + 'MorphoAAVEPositionWrapperMock' + ) + const wrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy({ + morphoController: networkConfig[1].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[1].MORPHO_AAVE_LENS!, + underlyingERC20: opts.underlyingToken!, + poolToken: opts.poolToken!, + underlyingSymbol: opts.targetName!, + }) + opts.erc20 = wrapperMock.address + } + + const collateral = await MorphoAAVECollateralFactory.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, + }, + opts.revenueHiding, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + + await expect(collateral.refresh()) + + return collateral +} + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + inOpts: MAFiatCollateralOpts = {} +): Fixture => { + const makeCollateralFixtureContext = async () => { + const opts = { ...defaultCollateralOpts, ...inOpts } + + const MorphoAAVEPositionWrapperMockFactory = await ethers.getContractFactory( + 'MorphoAAVEPositionWrapperMock' + ) + const erc20Factory = await ethers.getContractFactory('ERC20Mock') + const underlyingErc20 = await erc20Factory.attach(opts.underlyingToken!) + const underlyingErc20Symbol = await underlyingErc20.symbol() + const wrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy({ + morphoController: networkConfig[1].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[1].MORPHO_AAVE_LENS!, + underlyingERC20: opts.underlyingToken!, + poolToken: opts.poolToken!, + underlyingSymbol: ethers.utils.formatBytes32String(underlyingErc20Symbol), + }) + + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, opts.defaultPrice!) + ) + const collateralOpts = { + ...opts, + erc20: wrapperMock.address, + chainlinkFeed: chainlinkFeed.address, + } + + const collateral = await deployCollateral(collateralOpts) + + return { + alice, + collateral, + underlyingErc20: underlyingErc20, + chainlinkFeed, + tok: wrapperMock as unknown as ERC20Mock, + morphoWrapper: wrapperMock, + } as MorphoAaveCollateralFixtureContext + } + + return makeCollateralFixtureContext +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const reduceTargetPerRef = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const increaseTargetPerRef = async () => {} + +const changeRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + percentChange: BigNumber +) => { + const rate = await ctx.morphoWrapper.getExchangeRate() + await ctx.morphoWrapper.setExchangeRate(rate.add(rate.mul(percentChange).div(bn('100')))) +} + +// prettier-ignore +const reduceRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + pctDecrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctDecrease).mul(-1) + ) +} +// prettier-ignore +const increaseRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + pctIncrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctIncrease) + ) +} +const getExpectedPrice = async (ctx: MorphoAaveCollateralFixtureContext): Promise => { + const clData = await ctx.chainlinkFeed.latestRoundData() + const clDecimals = await ctx.chainlinkFeed.decimals() + + const refPerTok = await ctx.collateral.refPerTok() + return clData.answer + .mul(bn(10).pow(18 - clDecimals)) + .mul(refPerTok) + .div(fp('1')) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} +// eslint-disable-next-line @typescript-eslint/no-empty-function +const beforeEachRewardsTest = async () => {} + +export const defaultCollateralOpts: MAFiatCollateralOpts = { + targetName: ethers.utils.formatBytes32String('USDT'), + underlyingToken: networkConfig[1].tokens.USDT!, + poolToken: networkConfig[1].tokens.aUSDT!, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: USDC_USD_PRICE_FEED, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: bn(1000000), + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), + defaultPrice: bn('1e8'), + defaultRefPerTok: fp('1'), +} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it.skip, + itChecksTargetPerRefDefault: it.skip, + itChecksRefPerTokDefault: it, + itChecksPriceChanges: it, + itHasRevenueHiding: it, + resetFork: getResetFork(FORK_BLOCK), + collateralName: 'MorphoAAVEFiatCollateral', + chainlinkDefaultAnswer: defaultCollateralOpts.defaultPrice!, +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts new file mode 100644 index 0000000000..535f177977 --- /dev/null +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVENonFiatCollateral.test.ts @@ -0,0 +1,259 @@ +import { networkConfig } from '#/common/configuration' +import { bn, fp } from '#/common/numbers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MockV3Aggregator } from '@typechain/MockV3Aggregator' +import { TestICollateral } from '@typechain/TestICollateral' +import { + ERC20Mock, + MockV3Aggregator__factory, + MorphoAAVENonFiatCollateral__factory, +} from '@typechain/index' +import { expect } from 'chai' +import { BigNumber, BigNumberish } from 'ethers' +import { parseEther } from 'ethers/lib/utils' +import { ethers } from 'hardhat' +import collateralTests from '../collateralTests' +import { getResetFork } from '../helpers' +import { CollateralOpts } from '../pluginTestTypes' +import { + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + FORK_BLOCK, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, +} from './constants' +import { MorphoAaveCollateralFixtureContext, mintCollateralTo } from './mintCollateralTo' + +interface MAFiatCollateralOpts extends CollateralOpts { + underlyingToken?: string + poolToken?: string + defaultPrice?: BigNumberish + defaultRefPerTok?: BigNumberish + + refPerTokChainlinkFeed?: string + refPerTokChainlinkTimeout?: BigNumberish +} + +export const deployCollateral = async ( + opts: MAFiatCollateralOpts = {} +): Promise => { + opts = { ...defaultCollateralOpts, ...opts } + + const MorphoAAVECollateralFactory: MorphoAAVENonFiatCollateral__factory = + await ethers.getContractFactory('MorphoAAVENonFiatCollateral') + if (opts.erc20 == null) { + const MorphoAAVEPositionWrapperMockFactory = await ethers.getContractFactory( + 'MorphoAAVEPositionWrapperMock' + ) + const wrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy({ + morphoController: networkConfig[1].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[1].MORPHO_AAVE_LENS!, + underlyingERC20: opts.underlyingToken!, + poolToken: opts.poolToken!, + underlyingSymbol: opts.targetName!, + }) + opts.erc20 = wrapperMock.address + } + const collateral = (await MorphoAAVECollateralFactory.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!, + }, + opts.revenueHiding!, + opts.refPerTokChainlinkFeed!, + opts.refPerTokChainlinkTimeout!, + { gasLimit: 2000000000 } + )) as unknown as TestICollateral + await collateral.deployed() + + await expect(collateral.refresh()) + + return collateral +} + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + inOpts: MAFiatCollateralOpts = {} +): Fixture => { + const makeCollateralFixtureContext = async () => { + const opts = { ...defaultCollateralOpts, ...inOpts } + const MorphoAAVEPositionWrapperMockFactory = await ethers.getContractFactory( + 'MorphoAAVEPositionWrapperMock' + ) + const erc20Factory = await ethers.getContractFactory('ERC20Mock') + const underlyingErc20 = erc20Factory.attach(opts.underlyingToken!) + const underlyingErc20Symbol = await underlyingErc20.symbol() + const wrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy({ + morphoController: networkConfig[1].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[1].MORPHO_AAVE_LENS!, + underlyingERC20: opts.underlyingToken!, + poolToken: opts.poolToken!, + underlyingSymbol: ethers.utils.formatBytes32String(underlyingErc20Symbol), + }) + + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, opts.defaultPrice!) + ) + + const refPerTokChainlinkFeed = ( + await MockV3AggregatorFactory.deploy(18, opts.defaultRefPerTok!) + ) + + const collateralOpts = { + ...opts, + erc20: wrapperMock.address, + chainlinkFeed: chainlinkFeed.address, + refPerTokChainlinkFeed: refPerTokChainlinkFeed.address, + } + const collateral = await deployCollateral(collateralOpts) + + return { + alice, + collateral, + chainlinkFeed, + refPerTokChainlinkFeed, + tok: wrapperMock as unknown as ERC20Mock, + morphoWrapper: wrapperMock, + underlyingErc20: underlyingErc20, + } as MorphoAaveCollateralFixtureContext + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const reduceTargetPerRef = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const increaseTargetPerRef = async () => {} + +const changeRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + percentChange: BigNumber +) => { + const rate = await ctx.morphoWrapper.getExchangeRate() + await ctx.morphoWrapper.setExchangeRate(rate.add(rate.mul(percentChange).div(bn('100')))) + + { + const lastRound = await ctx.refPerTokChainlinkFeed!.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(percentChange).div(100)) + await ctx.refPerTokChainlinkFeed!.updateAnswer(nextAnswer) + } + + { + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(percentChange).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) + } +} + +// prettier-ignore +const reduceRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + pctDecrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctDecrease).mul(-1) + ) +} +// prettier-ignore +const increaseRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + pctIncrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctIncrease) + ) +} + +const getExpectedPrice = async (ctx: MorphoAaveCollateralFixtureContext): Promise => { + const clData = await ctx.chainlinkFeed.latestRoundData() + const clDecimals = await ctx.chainlinkFeed.decimals() + + const clRptData = await ctx.refPerTokChainlinkFeed!.latestRoundData() + const clRptDecimals = await ctx.refPerTokChainlinkFeed!.decimals() + + const expctPrice = clData.answer + .mul(bn(10).pow(18 - clDecimals)) + .mul(clRptData.answer.mul(bn(10).pow(18 - clRptDecimals))) + .div(fp('1')) + return expctPrice +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} +// eslint-disable-next-line @typescript-eslint/no-empty-function +const beforeEachRewardsTest = async () => {} + +export const defaultCollateralOpts: MAFiatCollateralOpts = { + targetName: ethers.utils.formatBytes32String('WBTC'), + underlyingToken: networkConfig[1].tokens.WBTC!, + poolToken: networkConfig[1].tokens.aWBTC!, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: networkConfig[1].chainlinkFeeds.WBTC!, + refPerTokChainlinkFeed: networkConfig[1].chainlinkFeeds.wBTCBTC!, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: parseEther('100'), + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), + defaultPrice: bn('30000e8'), + defaultRefPerTok: fp('1'), + refPerTokChainlinkTimeout: PRICE_TIMEOUT, +} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it.skip, + itChecksTargetPerRefDefault: it.skip, + itChecksRefPerTokDefault: it, + itChecksPriceChanges: it, + itHasRevenueHiding: it, + resetFork: getResetFork(FORK_BLOCK), + collateralName: 'MorphoAAVENonFiatCollateral', + chainlinkDefaultAnswer: defaultCollateralOpts.defaultPrice!, +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts new file mode 100644 index 0000000000..18020aa837 --- /dev/null +++ b/test/plugins/individual-collateral/morpho-aave/MorphoAAVESelfReferentialCollateral.test.ts @@ -0,0 +1,235 @@ +import { networkConfig } from '#/common/configuration' +import { bn, fp } from '#/common/numbers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MockV3Aggregator } from '@typechain/MockV3Aggregator' +import { TestICollateral } from '@typechain/TestICollateral' +import { + ERC20Mock, + MockV3Aggregator__factory, + MorphoAAVESelfReferentialCollateral__factory, +} from '@typechain/index' +import { expect } from 'chai' +import { BigNumber, BigNumberish } from 'ethers' +import { parseEther } from 'ethers/lib/utils' +import { ethers } from 'hardhat' +import collateralTests from '../collateralTests' +import { getResetFork } from '../helpers' +import { CollateralOpts } from '../pluginTestTypes' +import { + DELAY_UNTIL_DEFAULT, + FORK_BLOCK, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, +} from './constants' +import { MorphoAaveCollateralFixtureContext, mintCollateralTo } from './mintCollateralTo' + +interface MAFiatCollateralOpts extends CollateralOpts { + underlyingToken?: string + poolToken?: string + defaultPrice?: BigNumberish + defaultRefPerTok?: BigNumberish +} + +export const deployCollateral = async ( + opts: MAFiatCollateralOpts = {} +): Promise => { + if (opts.defaultThreshold == null && opts.delayUntilDefault === 0) { + opts.defaultThreshold = fp('0.001') + } + opts = { ...defaultCollateralOpts, ...opts } + + const MorphoAAVESelfReferentialCollateral: MorphoAAVESelfReferentialCollateral__factory = + await ethers.getContractFactory('MorphoAAVESelfReferentialCollateral') + if (opts.erc20 == null) { + const MorphoAAVEPositionWrapperMockFactory = await ethers.getContractFactory( + 'MorphoAAVEPositionWrapperMock' + ) + const wrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy({ + morphoController: networkConfig[1].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[1].MORPHO_AAVE_LENS!, + underlyingERC20: opts.underlyingToken!, + poolToken: opts.poolToken!, + underlyingSymbol: opts.targetName!, + }) + opts.erc20 = wrapperMock.address + } + const collateral = (await MorphoAAVESelfReferentialCollateral.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!, + }, + opts.revenueHiding!, + { gasLimit: 2000000000 } + )) as unknown as TestICollateral + await collateral.deployed() + + await expect(collateral.refresh()) + + return collateral +} + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + inOpts: MAFiatCollateralOpts = {} +): Fixture => { + const makeCollateralFixtureContext = async () => { + const opts = { ...defaultCollateralOpts, ...inOpts } + const MorphoAAVEPositionWrapperMockFactory = await ethers.getContractFactory( + 'MorphoAAVEPositionWrapperMock' + ) + const erc20Factory = await ethers.getContractFactory('ERC20Mock') + const underlyingErc20 = erc20Factory.attach(opts.underlyingToken!) + const underlyingErc20Symbol = await underlyingErc20.symbol() + const wrapperMock = await MorphoAAVEPositionWrapperMockFactory.deploy({ + morphoController: networkConfig[1].MORPHO_AAVE_CONTROLLER!, + morphoLens: networkConfig[1].MORPHO_AAVE_LENS!, + underlyingERC20: opts.underlyingToken!, + poolToken: opts.poolToken!, + underlyingSymbol: ethers.utils.formatBytes32String(underlyingErc20Symbol), + }) + + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, opts.defaultPrice!) + ) + const collateralOpts = { + ...opts, + erc20: wrapperMock.address, + chainlinkFeed: chainlinkFeed.address, + } + const collateral = await deployCollateral(collateralOpts) + + return { + alice, + collateral, + chainlinkFeed, + tok: wrapperMock as unknown as ERC20Mock, + morphoWrapper: wrapperMock, + underlyingErc20: underlyingErc20, + } as MorphoAaveCollateralFixtureContext + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const reduceTargetPerRef = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const increaseTargetPerRef = async () => {} + +const changeRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + percentChange: BigNumber +) => { + const rate = await ctx.morphoWrapper.getExchangeRate() + await ctx.morphoWrapper.setExchangeRate(rate.add(rate.mul(percentChange).div(bn('100')))) + + const lastRound = await ctx.chainlinkFeed.latestRoundData() + const nextAnswer = lastRound.answer.add(lastRound.answer.mul(percentChange).div(100)) + await ctx.chainlinkFeed.updateAnswer(nextAnswer) +} + +// prettier-ignore +const reduceRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + pctDecrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctDecrease).mul(-1) + ) +} +// prettier-ignore +const increaseRefPerTok = async ( + ctx: MorphoAaveCollateralFixtureContext, + pctIncrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctIncrease) + ) +} + +const getExpectedPrice = async (ctx: MorphoAaveCollateralFixtureContext): Promise => { + // UoA/tok feed + const clData = await ctx.chainlinkFeed.latestRoundData() + const clDecimals = await ctx.chainlinkFeed.decimals() + + const refPerTok = await ctx.collateral.refPerTok() + const expectedPegPrice = clData.answer.mul(bn(10).pow(18 - clDecimals)) + return expectedPegPrice.mul(refPerTok).div(fp('1')) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} +// eslint-disable-next-line @typescript-eslint/no-empty-function +const beforeEachRewardsTest = async () => {} + +export const defaultCollateralOpts: MAFiatCollateralOpts = { + targetName: ethers.utils.formatBytes32String('ETH'), + underlyingToken: networkConfig[1].tokens.stETH!, + poolToken: networkConfig[1].tokens.astETH!, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: networkConfig[1].chainlinkFeeds.stETHUSD!, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: parseEther('1000'), + defaultThreshold: bn(0), // 0.05 + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), + defaultPrice: bn('1600e8'), + defaultRefPerTok: fp('1'), +} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + beforeEachRewardsTest, + makeCollateralFixtureContext, + mintCollateralTo, + reduceTargetPerRef, + increaseTargetPerRef, + reduceRefPerTok, + increaseRefPerTok, + getExpectedPrice, + itClaimsRewards: it.skip, + itChecksTargetPerRefDefault: it.skip, + itChecksRefPerTokDefault: it, + itChecksPriceChanges: it, + itHasRevenueHiding: it, + resetFork: getResetFork(FORK_BLOCK), + collateralName: 'MorphoAAVENonFiatCollateral', + chainlinkDefaultAnswer: defaultCollateralOpts.defaultPrice!, +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/morpho-aave/constants.ts b/test/plugins/individual-collateral/morpho-aave/constants.ts new file mode 100644 index 0000000000..d4060fcf9a --- /dev/null +++ b/test/plugins/individual-collateral/morpho-aave/constants.ts @@ -0,0 +1,17 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const USDC_USD_PRICE_FEED = networkConfig['1'].chainlinkFeeds.USDC as string +export const ETH_USD_PRICE_FEED = networkConfig['1'].chainlinkFeeds.ETH as string +export const USDC = networkConfig['1'].tokens.USDC as string + +export const PRICE_TIMEOUT = bn(604800) // 1 week +export const ORACLE_TIMEOUT = bn(86400) // 24 hours in seconds +export const ORACLE_ERROR = fp('0.005') +export const DEFAULT_THRESHOLD = bn(5).mul(bn(10).pow(16)) // 0.05 +export const DELAY_UNTIL_DEFAULT = bn(86400) + +export const USDC_DECIMALS = bn(6) + +export const FORK_BLOCK = 17528677 diff --git a/test/plugins/individual-collateral/morpho-aave/mintCollateralTo.ts b/test/plugins/individual-collateral/morpho-aave/mintCollateralTo.ts new file mode 100644 index 0000000000..7d5167ee85 --- /dev/null +++ b/test/plugins/individual-collateral/morpho-aave/mintCollateralTo.ts @@ -0,0 +1,46 @@ +import { CollateralFixtureContext, MintCollateralFunc } from '../pluginTestTypes' +import hre from 'hardhat' +import { BigNumberish, constants } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { whales } from '#/tasks/testing/upgrade-checker-utils/constants' +import { whileImpersonating } from '#/utils/impersonation' +import { MorphoAAVEPositionWrapperMock } from '@typechain/MorphoAAVEPositionWrapperMock' +import { IERC20 } from '@typechain/IERC20' +import { MockV3Aggregator } from '@typechain/MockV3Aggregator' + +/** + * Interface representing the context object for the MorphoAaveCollateralFixture. + * Extends the CollateralFixtureContext interface. + * Contains the MorphoAAVEPositionWrapperMock contract and the underlying ERC20 token. + */ +export interface MorphoAaveCollateralFixtureContext extends CollateralFixtureContext { + morphoWrapper: MorphoAAVEPositionWrapperMock + underlyingErc20: IERC20 + refPerTokChainlinkFeed?: MockV3Aggregator +} + +/** + * Mint collateral to a recipient using the MorphoAAVEPositionWrapperMock contract. + * @param ctx The MorphoAaveCollateralFixtureContext object. + * @param amount The amount of collateral to mint. + * @param _ The signer with address (not used in this function). + * @param recipient The address of the recipient of the minted collateral. + */ +export const mintCollateralTo: MintCollateralFunc = async ( + ctx: MorphoAaveCollateralFixtureContext, + amount: BigNumberish, + _: SignerWithAddress, + recipient: string +) => { + await whileImpersonating( + hre, + whales[ctx.underlyingErc20.address.toLowerCase()], + async (whaleSigner) => { + await ctx.underlyingErc20.connect(whaleSigner).approve(ctx.morphoWrapper.address, 0) + await ctx.underlyingErc20 + .connect(whaleSigner) + .approve(ctx.morphoWrapper.address, constants.MaxUint256) + await ctx.morphoWrapper.connect(whaleSigner).mint(recipient, amount) + } + ) +}