From 9d0e65f41a02dddd6241982b68a37b15feb49a6a Mon Sep 17 00:00:00 2001 From: "Julian M. Rodriguez" <56316686+julianmrodri@users.noreply.github.com> Date: Mon, 3 Jul 2023 18:07:36 -0300 Subject: [PATCH] 3.0.0 rc3 (#837) Co-authored-by: Taylor Brent Co-authored-by: Patrick McKelvy Co-authored-by: Jan Co-authored-by: Akshat Mittal --- .github/workflows/tests.yml | 9 +- .gitignore | 5 +- .../{goerli.json => goerli-old.json} | 3093 +++++++++++++++++ .solcover.js | 16 +- .solhintignore | 3 +- CHANGELOG.md | 2 + README.md | 2 + common/configuration.ts | 39 +- contracts/facade/DeployerRegistry.sol | 2 +- contracts/facade/FacadeAct.sol | 3 +- contracts/facade/FacadeRead.sol | 2 +- contracts/facade/FacadeTest.sol | 2 +- contracts/facade/FacadeWrite.sol | 2 +- contracts/facade/lib/FacadeWriteLib.sol | 2 +- contracts/interfaces/IAsset.sol | 2 +- contracts/interfaces/IAssetRegistry.sol | 2 +- contracts/interfaces/IBackingManager.sol | 2 +- contracts/interfaces/IBasketHandler.sol | 2 +- contracts/interfaces/IBroker.sol | 2 +- contracts/interfaces/IComponent.sol | 2 +- contracts/interfaces/IDeployer.sol | 4 +- contracts/interfaces/IDeployerRegistry.sol | 2 +- contracts/interfaces/IDistributor.sol | 2 +- contracts/interfaces/IFacadeAct.sol | 3 +- contracts/interfaces/IFacadeRead.sol | 2 +- contracts/interfaces/IFacadeTest.sol | 2 +- contracts/interfaces/IFacadeWrite.sol | 2 +- contracts/interfaces/IFurnace.sol | 2 +- contracts/interfaces/IGnosis.sol | 2 +- contracts/interfaces/IMain.sol | 6 +- contracts/interfaces/IRToken.sol | 2 +- contracts/interfaces/IRTokenOracle.sol | 19 + contracts/interfaces/IRevenueTrader.sol | 2 +- contracts/interfaces/IRewardable.sol | 2 +- contracts/interfaces/IStRSR.sol | 2 +- contracts/interfaces/IStRSRVotes.sol | 2 +- contracts/interfaces/ITrade.sol | 2 +- contracts/interfaces/ITrading.sol | 11 +- contracts/interfaces/IVersioned.sol | 2 +- contracts/libraries/Array.sol | 2 +- contracts/libraries/Fixed.sol | 69 +- contracts/libraries/Permit.sol | 2 +- contracts/libraries/String.sol | 2 +- contracts/libraries/Throttle.sol | 2 +- contracts/libraries/test/ArrayCallerMock.sol | 2 +- contracts/libraries/test/FixedCallerMock.sol | 224 +- contracts/libraries/test/StringCallerMock.sol | 2 +- contracts/mixins/Auth.sol | 2 +- contracts/mixins/ComponentRegistry.sol | 2 +- contracts/mixins/NetworkConfigLib.sol | 26 + contracts/mixins/Versioned.sol | 2 +- contracts/p0/AssetRegistry.sol | 2 +- contracts/p0/BackingManager.sol | 11 +- contracts/p0/BasketHandler.sol | 34 +- contracts/p0/Broker.sol | 10 +- contracts/p0/Deployer.sol | 2 +- contracts/p0/Distributor.sol | 2 +- contracts/p0/Furnace.sol | 10 +- contracts/p0/Main.sol | 2 +- contracts/p0/RToken.sol | 2 +- contracts/p0/RevenueTrader.sol | 2 +- contracts/p0/StRSR.sol | 26 +- contracts/p0/mixins/Component.sol | 2 +- contracts/p0/mixins/Rewardable.sol | 2 +- contracts/p0/mixins/Trading.sol | 19 +- contracts/p0/mixins/TradingLib.sol | 35 +- contracts/p1/AssetRegistry.sol | 2 +- contracts/p1/BackingManager.sol | 13 +- contracts/p1/BasketHandler.sol | 59 +- contracts/p1/Broker.sol | 13 +- contracts/p1/Deployer.sol | 2 +- contracts/p1/Distributor.sol | 2 +- contracts/p1/Furnace.sol | 12 +- contracts/p1/Main.sol | 2 +- contracts/p1/RToken.sol | 2 +- contracts/p1/RevenueTrader.sol | 2 +- contracts/p1/StRSR.sol | 31 +- contracts/p1/StRSRVotes.sol | 2 +- contracts/p1/mixins/BasketLib.sol | 2 +- contracts/p1/mixins/Component.sol | 2 +- .../p1/mixins/RecollateralizationLib.sol | 9 +- contracts/p1/mixins/RewardableLib.sol | 2 +- contracts/p1/mixins/TradeLib.sol | 33 +- contracts/p1/mixins/Trading.sol | 22 +- .../assets/AppreciatingFiatCollateral.sol | 2 +- contracts/plugins/assets/Asset.sol | 2 +- .../plugins/assets/EURFiatCollateral.sol | 2 +- contracts/plugins/assets/FiatCollateral.sol | 2 +- .../plugins/assets/NonFiatCollateral.sol | 2 +- contracts/plugins/assets/OracleLib.sol | 2 +- contracts/plugins/assets/RTokenAsset.sol | 51 +- .../assets/SelfReferentialCollateral.sol | 2 +- contracts/plugins/assets/VersionedAsset.sol | 2 +- .../assets/aave/ATokenFiatCollateral.sol | 2 +- .../aave/{vendor => }/IStaticATokenLM.sol | 2 +- .../aave/{vendor => }/StaticATokenErrors.sol | 0 .../aave/{vendor => }/StaticATokenLM.sol | 30 +- .../assets/ankr/AnkrStakedEthCollateral.sol | 2 +- .../plugins/assets/ankr/vendor/IAnkrETH.sol | 2 +- .../plugins/assets/cbeth/CBETHCollateral.sol | 70 + contracts/plugins/assets/cbeth/README.md | 19 + .../compoundv2/CTokenFiatCollateral.sol | 10 +- .../compoundv2/CTokenNonFiatCollateral.sol | 2 +- .../CTokenSelfReferentialCollateral.sol | 12 +- .../plugins/assets/compoundv2/CTokenVault.sol | 33 - .../assets/compoundv2/CTokenWrapper.sol | 42 + .../plugins/assets/compoundv2/ICToken.sol | 2 +- .../assets/compoundv3/CTokenV3Collateral.sol | 4 +- .../assets/compoundv3/CometHelpers.sol | 16 +- .../assets/compoundv3/CusdcV3Wrapper.sol | 4 +- .../assets/compoundv3/ICusdcV3Wrapper.sol | 2 +- .../assets/compoundv3/IWrappedERC20.sol | 2 +- .../assets/compoundv3/WrappedERC20.sol | 6 +- .../assets/compoundv3/vendor/CometCore.sol | 2 +- .../compoundv3/vendor/CometExtInterface.sol | 2 +- .../assets/compoundv3/vendor/CometExtMock.sol | 2 +- .../compoundv3/vendor/CometInterface.sol | 2 +- .../compoundv3/vendor/CometMainInterface.sol | 2 +- .../assets/compoundv3/vendor/CometStorage.sol | 2 +- .../assets/compoundv3/vendor/IComet.sol | 2 +- .../compoundv3/vendor/ICometConfigurator.sol | 2 +- .../compoundv3/vendor/ICometProxyAdmin.sol | 2 +- .../compoundv3/vendor/ICometRewards.sol | 2 +- .../CurveStableCollateral.sol} | 27 +- .../CurveStableMetapoolCollateral.sol} | 18 +- .../CurveStableRTokenMetapoolCollateral.sol} | 18 +- .../CurveVolatileCollateral.sol} | 23 +- .../assets/{convex => curve}/PoolTokens.sol | 18 +- .../assets/curve/crv/CurveGaugeWrapper.sol | 51 + .../assets/{convex => curve/cvx}/README.md | 0 .../cvx}/vendor/ConvexInterfaces.sol | 2 +- .../cvx}/vendor/ConvexStakingWrapper.sol | 0 .../cvx}/vendor/CvxMining.sol | 0 .../cvx}/vendor/IConvexStakingWrapper.sol | 2 +- .../cvx}/vendor/IRewardStaking.sol | 0 .../cvx}/vendor/StableSwap3Pool.vy | 0 contracts/plugins/assets/dsr/README.md | 27 + .../plugins/assets/dsr/SDaiCollateral.sol | 57 + .../RewardableERC20.sol} | 42 +- .../assets/erc20/RewardableERC20Wrapper.sol | 74 + .../assets/erc20/RewardableERC4626Vault.sol | 53 + .../assets/frax-eth/SFraxEthCollateral.sol | 2 +- .../assets/frax-eth/vendor/IfrxEthMinter.sol | 2 +- .../assets/frax-eth/vendor/IsfrxEth.sol | 2 +- .../assets/lido/LidoStakedEthCollateral.sol | 2 +- .../plugins/assets/lido/vendor/ISTETH.sol | 2 +- .../plugins/assets/lido/vendor/IWSTETH.sol | 2 +- .../assets/rocket-eth/RethCollateral.sol | 2 +- .../assets/rocket-eth/vendor/IReth.sol | 2 +- .../vendor/IRocketNetworkBalances.sol | 2 +- .../rocket-eth/vendor/IRocketStorage.sol | 2 +- contracts/plugins/governance/Governance.sol | 2 +- contracts/plugins/mocks/ATokenMock.sol | 2 +- .../plugins/mocks/ATokenNoController.sol | 30 + .../plugins/mocks/AaveLendingPoolMock.sol | 2 +- .../mocks/BackingMgrBackCompatible.sol | 38 + .../plugins/mocks/BadCollateralPlugin.sol | 2 +- contracts/plugins/mocks/BadERC20.sol | 14 +- contracts/plugins/mocks/CTokenMock.sol | 2 +- ...nVaultMock2.sol => CTokenWrapperMock2.sol} | 20 +- contracts/plugins/mocks/CometMock.sol | 2 +- contracts/plugins/mocks/ComptrollerMock.sol | 2 +- contracts/plugins/mocks/CurveMetapoolMock.sol | 2 +- contracts/plugins/mocks/CurvePoolMock.sol | 53 +- .../plugins/mocks/CusdcV3WrapperMock.sol | 2 +- .../plugins/mocks/EACAggregatorProxyMock.sol | 2 +- contracts/plugins/mocks/ERC1271Mock.sol | 2 +- contracts/plugins/mocks/ERC20Mock.sol | 2 +- contracts/plugins/mocks/ERC20MockDecimals.sol | 2 +- .../plugins/mocks/ERC20MockRewarding.sol | 2 +- .../mocks/GasGuzzlingFiatCollateral.sol | 2 +- contracts/plugins/mocks/GnosisMock.sol | 2 +- .../plugins/mocks/GnosisMockReentrant.sol | 2 +- .../mocks/InvalidATokenFiatCollateralMock.sol | 2 +- contracts/plugins/mocks/InvalidBrokerMock.sol | 2 +- .../plugins/mocks/InvalidFiatCollateral.sol | 2 +- .../mocks/InvalidRefPerTokCollateral.sol | 2 +- .../plugins/mocks/InvalidRevTraderP1Mock.sol | 59 + contracts/plugins/mocks/MakerPotMock.sol | 27 + .../plugins/mocks/MockableCollateral.sol | 2 +- .../plugins/mocks/NontrivialPegCollateral.sol | 2 +- contracts/plugins/mocks/RTokenCollateral.sol | 2 +- .../plugins/mocks/RevenueTraderBackComp.sol | 34 + .../mocks/RewardableERC20WrapperTest.sol | 18 + ...est.sol => RewardableERC4626VaultTest.sol} | 8 +- .../mocks/SelfdestructTransferMock.sol | 2 +- contracts/plugins/mocks/SfraxEthMock.sol | 2 +- contracts/plugins/mocks/USDCMock.sol | 2 +- contracts/plugins/mocks/UnpricedPlugins.sol | 2 +- contracts/plugins/mocks/WBTCMock.sol | 2 +- contracts/plugins/mocks/WETH.sol | 6 +- contracts/plugins/mocks/ZeroDecimalMock.sol | 2 +- .../mocks/upgrades/AssetRegistryV2.sol | 2 +- .../mocks/upgrades/BackingManagerV2.sol | 2 +- .../mocks/upgrades/BasketHandlerV2.sol | 3 +- contracts/plugins/mocks/upgrades/BrokerV2.sol | 2 +- .../plugins/mocks/upgrades/DistributorV2.sol | 2 +- .../plugins/mocks/upgrades/FurnaceV2.sol | 2 +- contracts/plugins/mocks/upgrades/MainV2.sol | 2 +- contracts/plugins/mocks/upgrades/RTokenV2.sol | 2 +- .../mocks/upgrades/RevenueTraderV2.sol | 2 +- contracts/plugins/mocks/upgrades/StRSRV2.sol | 2 +- contracts/plugins/trading/DutchTrade.sol | 12 +- contracts/plugins/trading/GnosisTrade.sol | 2 +- contracts/vendor/ERC20PermitUpgradeable.sol | 2 +- docs/dev-env.md | 17 +- docs/solidity-style.md | 2 +- hardhat.config.ts | 15 +- package.json | 7 +- scripts/addresses/5-RTKN-tmp-deployments.json | 35 + .../addresses/5-tmp-assets-collateral.json | 56 + scripts/addresses/5-tmp-deployments.json | 46 +- scripts/deploy.ts | 8 +- scripts/deployment/common.ts | 6 +- .../phase1-common/2_deploy_implementations.ts | 42 +- .../phase2-assets/2_deploy_collateral.ts | 2 +- .../collaterals/deploy_cbeth_collateral.ts | 84 + .../deploy_convex_rToken_metapool_plugin.ts | 68 +- .../deploy_convex_stable_metapool_plugin.ts | 12 +- .../deploy_convex_stable_plugin.ts | 10 +- .../deploy_convex_volatile_plugin.ts | 10 +- .../deploy_curve_rToken_metapool_plugin.ts | 135 + .../deploy_curve_stable_metapool_plugin.ts | 141 + .../collaterals/deploy_curve_stable_plugin.ts | 134 + .../deploy_curve_volatile_plugin.ts | 142 + .../collaterals/deploy_dsr_sdai.ts | 87 + .../deploy_flux_finance_collateral.ts | 2 +- .../deploy_lido_wsteth_collateral.ts | 28 +- .../deploy_rocket_pool_reth_collateral.ts | 19 +- scripts/deployment/utils.ts | 77 +- scripts/exhaustive-tests/run-1.sh | 2 +- .../verification/1_verify_implementations.ts | 6 + scripts/verification/5_verify_facadeWrite.ts | 3 +- scripts/verification/6_verify_collateral.ts | 38 +- .../collateral-plugins/verify_cbeth.ts | 55 + ...able_plugin.ts => verify_convex_stable.ts} | 9 +- ...in.ts => verify_convex_stable_metapool.ts} | 6 +- ...> verify_convex_stable_rtoken_metapool.ts} | 6 +- ...le_plugin.ts => verify_convex_volatile.ts} | 6 +- .../collateral-plugins/verify_curve_stable.ts | 102 + .../verify_curve_stable_metapool.ts | 96 + .../verify_curve_stable_rtoken_metapool.ts | 90 + .../verify_curve_volatile.ts | 98 + ...usdcv3_collateral.ts => verify_cusdcv3.ts} | 0 ...rify_reth_collateral.ts => verify_reth.ts} | 0 .../collateral-plugins/verify_sdai.ts | 55 + ..._wsteth_collateral.ts => verify_wsteth.ts} | 0 scripts/verify_etherscan.ts | 19 +- test/Broker.test.ts | 124 + test/Facade.test.ts | 702 +++- test/FacadeWrite.test.ts | 8 +- test/Furnace.test.ts | 18 +- test/Main.test.ts | 260 +- test/RToken.test.ts | 240 +- test/RTokenExtremes.test.ts | 232 ++ test/Recollateralization.test.ts | 24 +- test/Revenues.test.ts | 256 +- test/ZTradingExtremes.test.ts | 12 +- test/ZZStRSR.test.ts | 96 +- test/fixtures.ts | 12 +- test/integration/AssetPlugins.test.ts | 149 +- test/integration/fixtures.ts | 8 +- .../mainnet-test/StaticATokens.test.ts | 27 +- test/libraries/Fixed.test.ts | 154 +- test/plugins/Asset.test.ts | 21 +- test/plugins/Collateral.test.ts | 16 +- test/plugins/RewardableERC20.test.ts | 617 ++++ test/plugins/RewardableERC20Vault.test.ts | 574 --- .../RewardableERC20Vault.test.ts.snap | 12 +- .../aave/ATokenFiatCollateral.test.ts | 7 +- .../aave/StaticATokenLM.test.ts | 179 +- .../cbeth/CBETHCollateral.test.ts | 233 ++ .../individual-collateral/cbeth/constants.ts | 19 + .../individual-collateral/cbeth/helpers.ts | 17 + .../compoundv2/CTokenFiatCollateral.test.ts | 69 +- .../compoundv3/CusdcV3Wrapper.test.ts | 100 +- .../convex/CvxStableMetapoolSuite.test.ts | 768 ---- .../CvxStableRTokenMetapoolTestSuite.test.ts | 771 ---- .../convex/CvxStableTestSuite.test.ts | 756 ---- .../convex/CvxVolatileTestSuite.test.ts | 799 ----- .../curve/collateralTests.ts | 639 ++++ .../{convex => curve}/constants.ts | 21 +- .../curve/crv/CrvStableMetapoolSuite.test.ts | 229 ++ .../CrvStableRTokenMetapoolTestSuite.test.ts | 219 ++ .../curve/crv/CrvStableTestSuite.test.ts | 238 ++ .../curve/crv/CrvVolatileTestSuite.test.ts | 225 ++ .../curve/crv/helpers.ts | 320 ++ .../curve/cvx/CvxStableMetapoolSuite.test.ts | 237 ++ .../CvxStableRTokenMetapoolTestSuite.test.ts | 221 ++ .../curve/cvx/CvxStableTestSuite.test.ts | 436 +++ .../curve/cvx/CvxVolatileTestSuite.test.ts | 227 ++ .../{convex => curve/cvx}/helpers.ts | 152 +- .../curve/pluginTestTypes.ts | 89 + .../dsr/SDaiCollateralTestSuite.test.ts | 211 ++ .../individual-collateral/dsr/constants.ts | 19 + .../individual-collateral/dsr/helpers.ts | 19 + .../flux-finance/FTokenFiatCollateral.test.ts | 33 +- .../flux-finance/helpers.ts | 6 +- .../individual-collateral/pluginTestTypes.ts | 8 +- .../RethCollateralTestSuite.test.ts | 14 +- test/scenario/ComplexBasket.test.ts | 16 +- test/scenario/MaxBasketSize.test.ts | 14 +- test/scenario/RevenueHiding.test.ts | 6 +- test/scenario/cETH.test.ts | 6 +- test/scenario/cWBTC.test.ts | 10 +- test/utils/tokens.ts | 6 +- utils/subgraph.ts | 1 + yarn.lock | 614 +++- 308 files changed, 13534 insertions(+), 4918 deletions(-) rename .openzeppelin/{goerli.json => goerli-old.json} (50%) create mode 100644 contracts/interfaces/IRTokenOracle.sol create mode 100644 contracts/mixins/NetworkConfigLib.sol rename contracts/plugins/assets/aave/{vendor => }/IStaticATokenLM.sol (99%) rename contracts/plugins/assets/aave/{vendor => }/StaticATokenErrors.sol (100%) rename contracts/plugins/assets/aave/{vendor => }/StaticATokenLM.sol (93%) create mode 100644 contracts/plugins/assets/cbeth/CBETHCollateral.sol create mode 100644 contracts/plugins/assets/cbeth/README.md delete mode 100644 contracts/plugins/assets/compoundv2/CTokenVault.sol create mode 100644 contracts/plugins/assets/compoundv2/CTokenWrapper.sol rename contracts/plugins/assets/{convex/CvxStableCollateral.sol => curve/CurveStableCollateral.sol} (87%) rename contracts/plugins/assets/{convex/CvxStableMetapoolCollateral.sol => curve/CurveStableMetapoolCollateral.sol} (92%) rename contracts/plugins/assets/{convex/CvxStableRTokenMetapoolCollateral.sol => curve/CurveStableRTokenMetapoolCollateral.sol} (74%) rename contracts/plugins/assets/{convex/CvxVolatileCollateral.sol => curve/CurveVolatileCollateral.sol} (75%) rename contracts/plugins/assets/{convex => curve}/PoolTokens.sol (96%) create mode 100644 contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol rename contracts/plugins/assets/{convex => curve/cvx}/README.md (100%) rename contracts/plugins/assets/{convex => curve/cvx}/vendor/ConvexInterfaces.sol (99%) rename contracts/plugins/assets/{convex => curve/cvx}/vendor/ConvexStakingWrapper.sol (100%) rename contracts/plugins/assets/{convex => curve/cvx}/vendor/CvxMining.sol (100%) rename contracts/plugins/assets/{convex => curve/cvx}/vendor/IConvexStakingWrapper.sol (90%) rename contracts/plugins/assets/{convex => curve/cvx}/vendor/IRewardStaking.sol (100%) rename contracts/plugins/assets/{convex => curve/cvx}/vendor/StableSwap3Pool.vy (100%) create mode 100644 contracts/plugins/assets/dsr/README.md create mode 100644 contracts/plugins/assets/dsr/SDaiCollateral.sol rename contracts/plugins/assets/{vaults/RewardableERC20Vault.sol => erc20/RewardableERC20.sol} (75%) create mode 100644 contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol create mode 100644 contracts/plugins/assets/erc20/RewardableERC4626Vault.sol create mode 100644 contracts/plugins/mocks/ATokenNoController.sol create mode 100644 contracts/plugins/mocks/BackingMgrBackCompatible.sol rename contracts/plugins/mocks/{CTokenVaultMock2.sol => CTokenWrapperMock2.sol} (72%) create mode 100644 contracts/plugins/mocks/InvalidRevTraderP1Mock.sol create mode 100644 contracts/plugins/mocks/MakerPotMock.sol create mode 100644 contracts/plugins/mocks/RevenueTraderBackComp.sol create mode 100644 contracts/plugins/mocks/RewardableERC20WrapperTest.sol rename contracts/plugins/mocks/{RewardableERC20VaultTest.sol => RewardableERC4626VaultTest.sol} (60%) create mode 100644 scripts/addresses/5-RTKN-tmp-deployments.json create mode 100644 scripts/addresses/5-tmp-assets-collateral.json create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts create mode 100644 scripts/verification/collateral-plugins/verify_cbeth.ts rename scripts/verification/collateral-plugins/{verify_convex_stable_plugin.ts => verify_convex_stable.ts} (92%) rename scripts/verification/collateral-plugins/{verify_convex_stable_metapool_plugin.ts => verify_convex_stable_metapool.ts} (93%) rename scripts/verification/collateral-plugins/{verify_eusd_fraxbp_collateral.ts => verify_convex_stable_rtoken_metapool.ts} (92%) rename scripts/verification/collateral-plugins/{verify_convex_volatile_plugin.ts => verify_convex_volatile.ts} (93%) create mode 100644 scripts/verification/collateral-plugins/verify_curve_stable.ts create mode 100644 scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts create mode 100644 scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts create mode 100644 scripts/verification/collateral-plugins/verify_curve_volatile.ts rename scripts/verification/collateral-plugins/{verify_cusdcv3_collateral.ts => verify_cusdcv3.ts} (100%) rename scripts/verification/collateral-plugins/{verify_reth_collateral.ts => verify_reth.ts} (100%) create mode 100644 scripts/verification/collateral-plugins/verify_sdai.ts rename scripts/verification/collateral-plugins/{verify_wsteth_collateral.ts => verify_wsteth.ts} (100%) create mode 100644 test/RTokenExtremes.test.ts create mode 100644 test/plugins/RewardableERC20.test.ts delete mode 100644 test/plugins/RewardableERC20Vault.test.ts create mode 100644 test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts create mode 100644 test/plugins/individual-collateral/cbeth/constants.ts create mode 100644 test/plugins/individual-collateral/cbeth/helpers.ts delete mode 100644 test/plugins/individual-collateral/convex/CvxStableMetapoolSuite.test.ts delete mode 100644 test/plugins/individual-collateral/convex/CvxStableRTokenMetapoolTestSuite.test.ts delete mode 100644 test/plugins/individual-collateral/convex/CvxStableTestSuite.test.ts delete mode 100644 test/plugins/individual-collateral/convex/CvxVolatileTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/collateralTests.ts rename test/plugins/individual-collateral/{convex => curve}/constants.ts (79%) create mode 100644 test/plugins/individual-collateral/curve/crv/CrvStableMetapoolSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/crv/CrvStableTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/crv/CrvVolatileTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/crv/helpers.ts create mode 100644 test/plugins/individual-collateral/curve/cvx/CvxStableMetapoolSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/curve/cvx/CvxVolatileTestSuite.test.ts rename test/plugins/individual-collateral/{convex => curve/cvx}/helpers.ts (66%) create mode 100644 test/plugins/individual-collateral/curve/pluginTestTypes.ts create mode 100644 test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts create mode 100644 test/plugins/individual-collateral/dsr/constants.ts create mode 100644 test/plugins/individual-collateral/dsr/helpers.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c046a892d..1f3a2cdc5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,7 +62,7 @@ jobs: hardhat-network-fork- - run: yarn test:plugins:integration env: - NODE_OPTIONS: '--max-old-space-size=4096' + NODE_OPTIONS: '--max-old-space-size=8192' TS_NODE_SKIP_IGNORE: true MAINNET_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }} @@ -79,7 +79,7 @@ jobs: - run: yarn compile - run: yarn test:p0 env: - NODE_OPTIONS: '--max-old-space-size=4096' + NODE_OPTIONS: '--max-old-space-size=8192' p1-tests: name: 'P1 Tests' @@ -94,7 +94,7 @@ jobs: - run: yarn compile - run: yarn test:p1 env: - NODE_OPTIONS: '--max-old-space-size=4096' + NODE_OPTIONS: '--max-old-space-size=8192' scenario-tests: name: 'Scenario Tests' @@ -133,6 +133,7 @@ jobs: hardhat-network-fork- - run: yarn test:extreme:integration env: + NODE_OPTIONS: '--max-old-space-size=8192' TS_NODE_SKIP_IGNORE: true MAINNET_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }} @@ -159,6 +160,6 @@ jobs: - run: yarn compile - run: yarn test:integration env: - NODE_OPTIONS: '--max-old-space-size=4096' + NODE_OPTIONS: '--max-old-space-size=8192' TS_NODE_SKIP_IGNORE: true MAINNET_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }} diff --git a/.gitignore b/.gitignore index 719f036d1..1a1acd0a9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,7 @@ scripts/addresses/31337* # Scripts for local/test interactions scripts/test.ts -scripts/playground.ts \ No newline at end of file +scripts/playground.ts + +# tenderly deployment/verification artifacts +deployments/ \ No newline at end of file diff --git a/.openzeppelin/goerli.json b/.openzeppelin/goerli-old.json similarity index 50% rename from .openzeppelin/goerli.json rename to .openzeppelin/goerli-old.json index 0269997ae..ec087c2b1 100644 --- a/.openzeppelin/goerli.json +++ b/.openzeppelin/goerli-old.json @@ -3102,6 +3102,3099 @@ } } } + }, + "b17431f482da5a2bb9bd8511872fc102e0f69c0d53654dfb718e385aaca98522": { + "address": "0x15395aCCbF8c6b28671fe41624D599624709a2D6", + "txHash": "0xd3d08969c43f1ee652aff2df57bc8c34a8abb8d7f3562cdb11b00e618fffb2bb", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "101", + "type": "t_mapping(t_bytes32,t_struct(RoleData)80_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:259" + }, + { + "label": "longFreezes", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_address,t_uint256)", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:36" + }, + { + "label": "unfreezeAt", + "offset": 0, + "slot": "152", + "type": "t_uint48", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:38" + }, + { + "label": "shortFreeze", + "offset": 6, + "slot": "152", + "type": "t_uint48", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:39" + }, + { + "label": "longFreeze", + "offset": 12, + "slot": "152", + "type": "t_uint48", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:40" + }, + { + "label": "tradingPaused", + "offset": 18, + "slot": "152", + "type": "t_bool", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:45", + "renamedFrom": "paused" + }, + { + "label": "issuancePaused", + "offset": 19, + "slot": "152", + "type": "t_bool", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:46" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)48_storage", + "contract": "Auth", + "src": "contracts/mixins/Auth.sol:225" + }, + { + "label": "rToken", + "offset": 0, + "slot": "201", + "type": "t_contract(IRToken)22646", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:34" + }, + { + "label": "stRSR", + "offset": 0, + "slot": "202", + "type": "t_contract(IStRSR)22949", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:42" + }, + { + "label": "assetRegistry", + "offset": 0, + "slot": "203", + "type": "t_contract(IAssetRegistry)20716", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:50" + }, + { + "label": "basketHandler", + "offset": 0, + "slot": "204", + "type": "t_contract(IBasketHandler)21025", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:58" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "205", + "type": "t_contract(IBackingManager)20778", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:66" + }, + { + "label": "distributor", + "offset": 0, + "slot": "206", + "type": "t_contract(IDistributor)21478", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:74" + }, + { + "label": "rsrTrader", + "offset": 0, + "slot": "207", + "type": "t_contract(IRevenueTrader)22735", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:82" + }, + { + "label": "rTokenTrader", + "offset": 0, + "slot": "208", + "type": "t_contract(IRevenueTrader)22735", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:90" + }, + { + "label": "furnace", + "offset": 0, + "slot": "209", + "type": "t_contract(IFurnace)21973", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:98" + }, + { + "label": "broker", + "offset": 0, + "slot": "210", + "type": "t_contract(IBroker)21145", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:106" + }, + { + "label": "__gap", + "offset": 0, + "slot": "211", + "type": "t_array(t_uint256)40_storage", + "contract": "ComponentRegistry", + "src": "contracts/mixins/ComponentRegistry.sol:119" + }, + { + "label": "__gap", + "offset": 0, + "slot": "251", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "301", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "rsr", + "offset": 0, + "slot": "351", + "type": "t_contract(IERC20)11635", + "contract": "MainP1", + "src": "contracts/p1/Main.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "352", + "type": "t_array(t_uint256)49_storage", + "contract": "MainP1", + "src": "contracts/p1/Main.sol:71" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)40_storage": { + "label": "uint256[40]", + "numberOfBytes": "1280" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IAssetRegistry)20716": { + "label": "contract IAssetRegistry", + "numberOfBytes": "20" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IBasketHandler)21025": { + "label": "contract IBasketHandler", + "numberOfBytes": "20" + }, + "t_contract(IBroker)21145": { + "label": "contract IBroker", + "numberOfBytes": "20" + }, + "t_contract(IDistributor)21478": { + "label": "contract IDistributor", + "numberOfBytes": "20" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IFurnace)21973": { + "label": "contract IFurnace", + "numberOfBytes": "20" + }, + "t_contract(IRToken)22646": { + "label": "contract IRToken", + "numberOfBytes": "20" + }, + "t_contract(IRevenueTrader)22735": { + "label": "contract IRevenueTrader", + "numberOfBytes": "20" + }, + "t_contract(IStRSR)22949": { + "label": "contract IStRSR", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)80_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(RoleData)80_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "21d20dc99ca71c92408f469f19636b46b188f7821898d70183e299d69ff3f239": { + "address": "0xe981820A4Dd0d5168beb73F57E6dc827420D066C", + "txHash": "0xb8a35b3aedfc4af94e84a45bbafd673474173a5dcac2081fa9d4e4c306b242fb", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "basketHandler", + "offset": 0, + "slot": "201", + "type": "t_contract(IBasketHandler)21025", + "contract": "AssetRegistryP1", + "src": "contracts/p1/AssetRegistry.sol:18" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "202", + "type": "t_contract(IBackingManager)20778", + "contract": "AssetRegistryP1", + "src": "contracts/p1/AssetRegistry.sol:19" + }, + { + "label": "_erc20s", + "offset": 0, + "slot": "203", + "type": "t_struct(AddressSet)17883_storage", + "contract": "AssetRegistryP1", + "src": "contracts/p1/AssetRegistry.sol:22" + }, + { + "label": "assets", + "offset": 0, + "slot": "205", + "type": "t_mapping(t_contract(IERC20)11635,t_contract(IAsset)20473)", + "contract": "AssetRegistryP1", + "src": "contracts/p1/AssetRegistry.sol:25" + }, + { + "label": "lastRefresh", + "offset": 0, + "slot": "206", + "type": "t_uint48", + "contract": "AssetRegistryP1", + "src": "contracts/p1/AssetRegistry.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "207", + "type": "t_array(t_uint256)46_storage", + "contract": "AssetRegistryP1", + "src": "contracts/p1/AssetRegistry.sol:219" + } + ], + "types": { + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IAsset)20473": { + "label": "contract IAsset", + "numberOfBytes": "20" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IBasketHandler)21025": { + "label": "contract IBasketHandler", + "numberOfBytes": "20" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_contract(IERC20)11635,t_contract(IAsset)20473)": { + "label": "mapping(contract IERC20 => contract IAsset)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)17883_storage": { + "label": "struct EnumerableSet.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)17582_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)17582_storage": { + "label": "struct EnumerableSet.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "8fe7d0af60788c44e6cc7060cd99b68b5e670f8ed9514463c83b8507a8c51cc6": { + "address": "0xbe7B053E820c5FBe70a0f075DA0C931aD8816e4F", + "txHash": "0xabcd238cc8bf0eaca91a02a230b8b724c64c91d0683e11e34703011085f93682", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "_status", + "offset": 0, + "slot": "201", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:74" + }, + { + "label": "broker", + "offset": 0, + "slot": "251", + "type": "t_contract(IBroker)21145", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:27" + }, + { + "label": "trades", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_contract(IERC20)11635,t_contract(ITrade)23084)", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:30" + }, + { + "label": "tradesOpen", + "offset": 0, + "slot": "253", + "type": "t_uint48", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:31" + }, + { + "label": "maxTradeSlippage", + "offset": 6, + "slot": "253", + "type": "t_uint192", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:34" + }, + { + "label": "minTradeVolume", + "offset": 0, + "slot": "254", + "type": "t_uint192", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "255", + "type": "t_array(t_uint256)46_storage", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:161" + }, + { + "label": "assetRegistry", + "offset": 0, + "slot": "301", + "type": "t_contract(IAssetRegistry)20716", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:25" + }, + { + "label": "basketHandler", + "offset": 0, + "slot": "302", + "type": "t_contract(IBasketHandler)21025", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:26" + }, + { + "label": "distributor", + "offset": 0, + "slot": "303", + "type": "t_contract(IDistributor)21478", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:27" + }, + { + "label": "rToken", + "offset": 0, + "slot": "304", + "type": "t_contract(IRToken)22646", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:28" + }, + { + "label": "rsr", + "offset": 0, + "slot": "305", + "type": "t_contract(IERC20)11635", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:29" + }, + { + "label": "stRSR", + "offset": 0, + "slot": "306", + "type": "t_contract(IStRSR)22949", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:30" + }, + { + "label": "rsrTrader", + "offset": 0, + "slot": "307", + "type": "t_contract(IRevenueTrader)22735", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:31" + }, + { + "label": "rTokenTrader", + "offset": 0, + "slot": "308", + "type": "t_contract(IRevenueTrader)22735", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:32" + }, + { + "label": "tradingDelay", + "offset": 20, + "slot": "308", + "type": "t_uint48", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:36" + }, + { + "label": "backingBuffer", + "offset": 0, + "slot": "309", + "type": "t_uint192", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:37" + }, + { + "label": "furnace", + "offset": 0, + "slot": "310", + "type": "t_contract(IFurnace)21973", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:40" + }, + { + "label": "tradeEnd", + "offset": 0, + "slot": "311", + "type": "t_mapping(t_enum(TradeKind)21047,t_uint48)", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:41" + }, + { + "label": "__gap", + "offset": 0, + "slot": "312", + "type": "t_array(t_uint256)39_storage", + "contract": "BackingManagerP1", + "src": "contracts/p1/BackingManager.sol:299" + } + ], + "types": { + "t_array(t_uint256)39_storage": { + "label": "uint256[39]", + "numberOfBytes": "1248" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IAssetRegistry)20716": { + "label": "contract IAssetRegistry", + "numberOfBytes": "20" + }, + "t_contract(IBasketHandler)21025": { + "label": "contract IBasketHandler", + "numberOfBytes": "20" + }, + "t_contract(IBroker)21145": { + "label": "contract IBroker", + "numberOfBytes": "20" + }, + "t_contract(IDistributor)21478": { + "label": "contract IDistributor", + "numberOfBytes": "20" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IFurnace)21973": { + "label": "contract IFurnace", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_contract(IRToken)22646": { + "label": "contract IRToken", + "numberOfBytes": "20" + }, + "t_contract(IRevenueTrader)22735": { + "label": "contract IRevenueTrader", + "numberOfBytes": "20" + }, + "t_contract(IStRSR)22949": { + "label": "contract IStRSR", + "numberOfBytes": "20" + }, + "t_contract(ITrade)23084": { + "label": "contract ITrade", + "numberOfBytes": "20" + }, + "t_enum(TradeKind)21047": { + "label": "enum TradeKind", + "members": [ + "DUTCH_AUCTION", + "BATCH_AUCTION" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_contract(IERC20)11635,t_contract(ITrade)23084)": { + "label": "mapping(contract IERC20 => contract ITrade)", + "numberOfBytes": "32" + }, + "t_mapping(t_enum(TradeKind)21047,t_uint48)": { + "label": "mapping(enum TradeKind => uint48)", + "numberOfBytes": "32" + }, + "t_uint192": { + "label": "uint192", + "numberOfBytes": "24" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "06e74acc83f86954e87d2465720810cfada71be55c0cb9f1626d6af4e6192b34": { + "address": "0xA7eCF508CdF5a88ae93b899DE4fcACcB43112Ce8", + "txHash": "0x41b7579559dec433e30e6301072463aa5af90aa2e1d46969397fc1c84a41f064", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "assetRegistry", + "offset": 0, + "slot": "201", + "type": "t_contract(IAssetRegistry)20716", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:32" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "202", + "type": "t_contract(IBackingManager)20778", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:33" + }, + { + "label": "rsr", + "offset": 0, + "slot": "203", + "type": "t_contract(IERC20)11635", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:34" + }, + { + "label": "rToken", + "offset": 0, + "slot": "204", + "type": "t_contract(IRToken)22646", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:35" + }, + { + "label": "stRSR", + "offset": 0, + "slot": "205", + "type": "t_contract(IStRSR)22949", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:36" + }, + { + "label": "config", + "offset": 0, + "slot": "206", + "type": "t_struct(BasketConfig)49042_storage", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:40" + }, + { + "label": "basket", + "offset": 0, + "slot": "210", + "type": "t_struct(Basket)49052_storage", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:44" + }, + { + "label": "nonce", + "offset": 0, + "slot": "212", + "type": "t_uint48", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:46" + }, + { + "label": "timestamp", + "offset": 6, + "slot": "212", + "type": "t_uint48", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:47" + }, + { + "label": "disabled", + "offset": 12, + "slot": "212", + "type": "t_bool", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:51" + }, + { + "label": "_targetNames", + "offset": 0, + "slot": "213", + "type": "t_struct(Bytes32Set)17776_storage", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:57" + }, + { + "label": "_newBasket", + "offset": 0, + "slot": "215", + "type": "t_struct(Basket)49052_storage", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:58" + }, + { + "label": "warmupPeriod", + "offset": 0, + "slot": "217", + "type": "t_uint48", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:64" + }, + { + "label": "lastStatusTimestamp", + "offset": 6, + "slot": "217", + "type": "t_uint48", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:68" + }, + { + "label": "lastStatus", + "offset": 12, + "slot": "217", + "type": "t_enum(CollateralStatus)20505", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:69" + }, + { + "label": "basketHistory", + "offset": 0, + "slot": "218", + "type": "t_mapping(t_uint48,t_struct(Basket)49052_storage)", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:75" + }, + { + "label": "_targetAmts", + "offset": 0, + "slot": "219", + "type": "t_struct(Bytes32ToUintMap)17388_storage", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:78" + }, + { + "label": "__gap", + "offset": 0, + "slot": "222", + "type": "t_array(t_uint256)37_storage", + "contract": "BasketHandlerP1", + "src": "contracts/p1/BasketHandler.sol:684" + } + ], + "types": { + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_contract(IERC20)11635)dyn_storage": { + "label": "contract IERC20[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)37_storage": { + "label": "uint256[37]", + "numberOfBytes": "1184" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IAssetRegistry)20716": { + "label": "contract IAssetRegistry", + "numberOfBytes": "20" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_contract(IRToken)22646": { + "label": "contract IRToken", + "numberOfBytes": "20" + }, + "t_contract(IStRSR)22949": { + "label": "contract IStRSR", + "numberOfBytes": "20" + }, + "t_enum(CollateralStatus)20505": { + "label": "enum CollateralStatus", + "members": [ + "SOUND", + "IFFY", + "DISABLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_bytes32)": { + "label": "mapping(bytes32 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(BackupConfig)49022_storage)": { + "label": "mapping(bytes32 => struct BackupConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_contract(IERC20)11635,t_bytes32)": { + "label": "mapping(contract IERC20 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_contract(IERC20)11635,t_uint192)": { + "label": "mapping(contract IERC20 => uint192)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint48,t_struct(Basket)49052_storage)": { + "label": "mapping(uint48 => struct Basket)", + "numberOfBytes": "32" + }, + "t_struct(BackupConfig)49022_storage": { + "label": "struct BackupConfig", + "members": [ + { + "label": "max", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "erc20s", + "type": "t_array(t_contract(IERC20)11635)dyn_storage", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Basket)49052_storage": { + "label": "struct Basket", + "members": [ + { + "label": "erc20s", + "type": "t_array(t_contract(IERC20)11635)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "refAmts", + "type": "t_mapping(t_contract(IERC20)11635,t_uint192)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(BasketConfig)49042_storage": { + "label": "struct BasketConfig", + "members": [ + { + "label": "erc20s", + "type": "t_array(t_contract(IERC20)11635)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "targetAmts", + "type": "t_mapping(t_contract(IERC20)11635,t_uint192)", + "offset": 0, + "slot": "1" + }, + { + "label": "targetNames", + "type": "t_mapping(t_contract(IERC20)11635,t_bytes32)", + "offset": 0, + "slot": "2" + }, + { + "label": "backups", + "type": "t_mapping(t_bytes32,t_struct(BackupConfig)49022_storage)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(Bytes32Set)17776_storage": { + "label": "struct EnumerableSet.Bytes32Set", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)17582_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Bytes32ToBytes32Map)16465_storage": { + "label": "struct EnumerableMap.Bytes32ToBytes32Map", + "members": [ + { + "label": "_keys", + "type": "t_struct(Bytes32Set)17776_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_values", + "type": "t_mapping(t_bytes32,t_bytes32)", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(Bytes32ToUintMap)17388_storage": { + "label": "struct EnumerableMap.Bytes32ToUintMap", + "members": [ + { + "label": "_inner", + "type": "t_struct(Bytes32ToBytes32Map)16465_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "96" + }, + "t_struct(Set)17582_storage": { + "label": "struct EnumerableSet.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint192": { + "label": "uint192", + "numberOfBytes": "24" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "564ab7408bfb67e40d480b1e813e3e7111c32f2aba64d9fd45a09a262ce1794c": { + "address": "0xF4aB456F9FBA39b91F46c54185C218E4c32B014e", + "txHash": "0x88e5e4f3057cea882cf1e2be8059f3d84baafc48d50b096211387928f9a4f0a8", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "201", + "type": "t_contract(IBackingManager)20778", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:29" + }, + { + "label": "rsrTrader", + "offset": 0, + "slot": "202", + "type": "t_contract(IRevenueTrader)22735", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:30" + }, + { + "label": "rTokenTrader", + "offset": 0, + "slot": "203", + "type": "t_contract(IRevenueTrader)22735", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:31" + }, + { + "label": "batchTradeImplementation", + "offset": 0, + "slot": "204", + "type": "t_contract(ITrade)23084", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:35", + "renamedFrom": "tradeImplementation" + }, + { + "label": "gnosis", + "offset": 0, + "slot": "205", + "type": "t_contract(IGnosis)22073", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:38" + }, + { + "label": "batchAuctionLength", + "offset": 20, + "slot": "205", + "type": "t_uint48", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:42", + "renamedFrom": "auctionLength" + }, + { + "label": "disabled", + "offset": 26, + "slot": "205", + "type": "t_bool", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:46" + }, + { + "label": "trades", + "offset": 0, + "slot": "206", + "type": "t_mapping(t_address,t_bool)", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:49" + }, + { + "label": "dutchTradeImplementation", + "offset": 0, + "slot": "207", + "type": "t_contract(ITrade)23084", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:54" + }, + { + "label": "dutchAuctionLength", + "offset": 20, + "slot": "207", + "type": "t_uint48", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:57" + }, + { + "label": "__gap", + "offset": 0, + "slot": "208", + "type": "t_array(t_uint256)43_storage", + "contract": "BrokerP1", + "src": "contracts/p1/Broker.sol:232" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)43_storage": { + "label": "uint256[43]", + "numberOfBytes": "1376" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IGnosis)22073": { + "label": "contract IGnosis", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_contract(IRevenueTrader)22735": { + "label": "contract IRevenueTrader", + "numberOfBytes": "20" + }, + "t_contract(ITrade)23084": { + "label": "contract ITrade", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "c442f45e5a30a18c03ad3a19b0773ca6e596eacc7bb582e7317958ad769085c7": { + "address": "0xb120c3429900DDF665b34882d7685e39BB01897B", + "txHash": "0xbaa958e25ef08ac2b3c8141869e0e187b6aee0b98c32eaffae5c5a4e941cd7a1", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "destinations", + "offset": 0, + "slot": "201", + "type": "t_struct(AddressSet)17883_storage", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:17" + }, + { + "label": "distribution", + "offset": 0, + "slot": "203", + "type": "t_mapping(t_address,t_struct(RevenueShare)21416_storage)", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:18" + }, + { + "label": "rsr", + "offset": 0, + "slot": "204", + "type": "t_contract(IERC20)11635", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:36" + }, + { + "label": "rToken", + "offset": 0, + "slot": "205", + "type": "t_contract(IERC20)11635", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:37" + }, + { + "label": "furnace", + "offset": 0, + "slot": "206", + "type": "t_address", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:38" + }, + { + "label": "stRSR", + "offset": 0, + "slot": "207", + "type": "t_address", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:39" + }, + { + "label": "__gap", + "offset": 0, + "slot": "208", + "type": "t_array(t_uint256)46_storage", + "contract": "DistributorP1", + "src": "contracts/p1/Distributor.sol:190" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(RevenueShare)21416_storage)": { + "label": "mapping(address => struct RevenueShare)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)17883_storage": { + "label": "struct EnumerableSet.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)17582_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(RevenueShare)21416_storage": { + "label": "struct RevenueShare", + "members": [ + { + "label": "rTokenDist", + "type": "t_uint16", + "offset": 0, + "slot": "0" + }, + { + "label": "rsrDist", + "type": "t_uint16", + "offset": 2, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Set)17582_storage": { + "label": "struct EnumerableSet.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "e1eeaba6bbd307cda015b0bd4643179b81db28c5abddb6ac31b57095f8ccf1ba": { + "address": "0xa570BF93FC51406809dBf52aB898913541C91C20", + "txHash": "0x609b1c4df3e2925bfe332cf021145f11485f3d23e4ce746d748bc5c9f7f64c44", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "rToken", + "offset": 0, + "slot": "201", + "type": "t_contract(IRToken)22646", + "contract": "FurnaceP1", + "src": "contracts/p1/Furnace.sol:18" + }, + { + "label": "ratio", + "offset": 0, + "slot": "202", + "type": "t_uint192", + "contract": "FurnaceP1", + "src": "contracts/p1/Furnace.sol:21" + }, + { + "label": "lastPayout", + "offset": 24, + "slot": "202", + "type": "t_uint48", + "contract": "FurnaceP1", + "src": "contracts/p1/Furnace.sol:24" + }, + { + "label": "lastPayoutBal", + "offset": 0, + "slot": "203", + "type": "t_uint256", + "contract": "FurnaceP1", + "src": "contracts/p1/Furnace.sol:25" + }, + { + "label": "__gap", + "offset": 0, + "slot": "204", + "type": "t_array(t_uint256)47_storage", + "contract": "FurnaceP1", + "src": "contracts/p1/Furnace.sol:98" + } + ], + "types": { + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_contract(IRToken)22646": { + "label": "contract IRToken", + "numberOfBytes": "20" + }, + "t_uint192": { + "label": "uint192", + "numberOfBytes": "24" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "1b28a1aa7bfbba80a06938c752e89466601e419fa44682e5bb5fc79b56a7eba6": { + "address": "0x4c5FA27d1785d37B7Ac0942121f95370e61ff6a4", + "txHash": "0xc7fe8213c89b303da2094a8cb56a399bfd8ed436507b77d588145c6d544bbebb", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "_status", + "offset": 0, + "slot": "201", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:74" + }, + { + "label": "broker", + "offset": 0, + "slot": "251", + "type": "t_contract(IBroker)21145", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:27" + }, + { + "label": "trades", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_contract(IERC20)11635,t_contract(ITrade)23084)", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:30" + }, + { + "label": "tradesOpen", + "offset": 0, + "slot": "253", + "type": "t_uint48", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:31" + }, + { + "label": "maxTradeSlippage", + "offset": 6, + "slot": "253", + "type": "t_uint192", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:34" + }, + { + "label": "minTradeVolume", + "offset": 0, + "slot": "254", + "type": "t_uint192", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "255", + "type": "t_array(t_uint256)46_storage", + "contract": "TradingP1", + "src": "contracts/p1/mixins/Trading.sol:161" + }, + { + "label": "tokenToBuy", + "offset": 0, + "slot": "301", + "type": "t_contract(IERC20)11635", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:19" + }, + { + "label": "assetRegistry", + "offset": 0, + "slot": "302", + "type": "t_contract(IAssetRegistry)20716", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:20" + }, + { + "label": "distributor", + "offset": 0, + "slot": "303", + "type": "t_contract(IDistributor)21478", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:21" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "304", + "type": "t_contract(IBackingManager)20778", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:22" + }, + { + "label": "furnace", + "offset": 0, + "slot": "305", + "type": "t_contract(IFurnace)21973", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:23" + }, + { + "label": "rToken", + "offset": 0, + "slot": "306", + "type": "t_contract(IRToken)22646", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "307", + "type": "t_array(t_uint256)44_storage", + "contract": "RevenueTraderP1", + "src": "contracts/p1/RevenueTrader.sol:155" + } + ], + "types": { + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IAssetRegistry)20716": { + "label": "contract IAssetRegistry", + "numberOfBytes": "20" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IBroker)21145": { + "label": "contract IBroker", + "numberOfBytes": "20" + }, + "t_contract(IDistributor)21478": { + "label": "contract IDistributor", + "numberOfBytes": "20" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IFurnace)21973": { + "label": "contract IFurnace", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_contract(IRToken)22646": { + "label": "contract IRToken", + "numberOfBytes": "20" + }, + "t_contract(ITrade)23084": { + "label": "contract ITrade", + "numberOfBytes": "20" + }, + "t_mapping(t_contract(IERC20)11635,t_contract(ITrade)23084)": { + "label": "mapping(contract IERC20 => contract ITrade)", + "numberOfBytes": "32" + }, + "t_uint192": { + "label": "uint192", + "numberOfBytes": "24" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "ec5baa6e23f6979b7cdaac8bf22a9ff7c01800448a786091fa27df763d465f1a": { + "address": "0xd0cb758e918ac6973a2959343ECa4F333d8d25B1", + "txHash": "0xc330d191cbb055d130270257e88736b13fbc220bc95495eca6b8aa6a18bf58c9", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "_balances", + "offset": 0, + "slot": "201", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:37" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "202", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:39" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "203", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:41" + }, + { + "label": "_name", + "offset": 0, + "slot": "204", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:43" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "205", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "__gap", + "offset": 0, + "slot": "206", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:394" + }, + { + "label": "_HASHED_NAME", + "offset": 0, + "slot": "251", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol:32" + }, + { + "label": "_HASHED_VERSION", + "offset": 0, + "slot": "252", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol:33" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)50_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol:120" + }, + { + "label": "_nonces", + "offset": 0, + "slot": "303", + "type": "t_mapping(t_address,t_struct(Counter)2739_storage)", + "contract": "ERC20PermitUpgradeable", + "src": "contracts/vendor/ERC20PermitUpgradeable.sol:37" + }, + { + "label": "_PERMIT_TYPEHASH_DEPRECATED_SLOT", + "offset": 0, + "slot": "304", + "type": "t_bytes32", + "contract": "ERC20PermitUpgradeable", + "src": "contracts/vendor/ERC20PermitUpgradeable.sol:51", + "renamedFrom": "_PERMIT_TYPEHASH" + }, + { + "label": "__gap", + "offset": 0, + "slot": "305", + "type": "t_array(t_uint256)48_storage", + "contract": "ERC20PermitUpgradeable", + "src": "contracts/vendor/ERC20PermitUpgradeable.sol:129" + }, + { + "label": "mandate", + "offset": 0, + "slot": "353", + "type": "t_string_storage", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:44" + }, + { + "label": "assetRegistry", + "offset": 0, + "slot": "354", + "type": "t_contract(IAssetRegistry)20716", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:47" + }, + { + "label": "basketHandler", + "offset": 0, + "slot": "355", + "type": "t_contract(IBasketHandler)21025", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:48" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "356", + "type": "t_contract(IBackingManager)20778", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:49" + }, + { + "label": "furnace", + "offset": 0, + "slot": "357", + "type": "t_contract(IFurnace)21973", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:50" + }, + { + "label": "basketsNeeded", + "offset": 0, + "slot": "358", + "type": "t_uint192", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:55" + }, + { + "label": "issuanceThrottle", + "offset": 0, + "slot": "359", + "type": "t_struct(Throttle)25091_storage", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:58" + }, + { + "label": "redemptionThrottle", + "offset": 0, + "slot": "363", + "type": "t_struct(Throttle)25091_storage", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:59" + }, + { + "label": "__gap", + "offset": 0, + "slot": "367", + "type": "t_array(t_uint256)42_storage", + "contract": "RTokenP1", + "src": "contracts/p1/RToken.sol:532" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)42_storage": { + "label": "uint256[42]", + "numberOfBytes": "1344" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IAssetRegistry)20716": { + "label": "contract IAssetRegistry", + "numberOfBytes": "20" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IBasketHandler)21025": { + "label": "contract IBasketHandler", + "numberOfBytes": "20" + }, + "t_contract(IFurnace)21973": { + "label": "contract IFurnace", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Counter)2739_storage)": { + "label": "mapping(address => struct CountersUpgradeable.Counter)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Counter)2739_storage": { + "label": "struct CountersUpgradeable.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Params)25083_storage": { + "label": "struct ThrottleLib.Params", + "members": [ + { + "label": "amtRate", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "pctRate", + "type": "t_uint192", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Throttle)25091_storage": { + "label": "struct ThrottleLib.Throttle", + "members": [ + { + "label": "params", + "type": "t_struct(Params)25083_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "lastTimestamp", + "type": "t_uint48", + "offset": 0, + "slot": "2" + }, + { + "label": "lastAvailable", + "type": "t_uint256", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint192": { + "label": "uint192", + "numberOfBytes": "24" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "6fee1f2375271f998ec540323259cd0f6280f0f6fca2d1fba4c264fe6670253d": { + "address": "0xeC12e8412a7AE4598d754f4016D487c269719856", + "txHash": "0xc385c0254f25fa45a0857def570cc839ac915c729d2b1a284cc1e6f4e0528bfd", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" + }, + { + "label": "main", + "offset": 0, + "slot": "151", + "type": "t_contract(IMain)22420", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:21" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ComponentP1", + "src": "contracts/p1/mixins/Component.sol:69" + }, + { + "label": "_HASHED_NAME", + "offset": 0, + "slot": "201", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol:32" + }, + { + "label": "_HASHED_VERSION", + "offset": 0, + "slot": "202", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol:33" + }, + { + "label": "__gap", + "offset": 0, + "slot": "203", + "type": "t_array(t_uint256)50_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol:120" + }, + { + "label": "name", + "offset": 0, + "slot": "253", + "type": "t_string_storage", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:43" + }, + { + "label": "symbol", + "offset": 0, + "slot": "254", + "type": "t_string_storage", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:44" + }, + { + "label": "assetRegistry", + "offset": 0, + "slot": "255", + "type": "t_contract(IAssetRegistry)20716", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:49" + }, + { + "label": "backingManager", + "offset": 0, + "slot": "256", + "type": "t_contract(IBackingManager)20778", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:50" + }, + { + "label": "basketHandler", + "offset": 0, + "slot": "257", + "type": "t_contract(IBasketHandler)21025", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:51" + }, + { + "label": "rsr", + "offset": 0, + "slot": "258", + "type": "t_contract(IERC20)11635", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:52" + }, + { + "label": "era", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:57" + }, + { + "label": "stakes", + "offset": 0, + "slot": "260", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:61" + }, + { + "label": "totalStakes", + "offset": 0, + "slot": "261", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:62" + }, + { + "label": "stakeRSR", + "offset": 0, + "slot": "262", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:63" + }, + { + "label": "stakeRate", + "offset": 0, + "slot": "263", + "type": "t_uint192", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:64" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "264", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_mapping(t_address,t_uint256)))", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:69" + }, + { + "label": "draftEra", + "offset": 0, + "slot": "265", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:74" + }, + { + "label": "draftQueues", + "offset": 0, + "slot": "266", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_array(t_struct(CumulativeDraft)46080_storage)dyn_storage))", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:82" + }, + { + "label": "firstRemainingDraft", + "offset": 0, + "slot": "267", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:83" + }, + { + "label": "totalDrafts", + "offset": 0, + "slot": "268", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:84" + }, + { + "label": "draftRSR", + "offset": 0, + "slot": "269", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:85" + }, + { + "label": "draftRate", + "offset": 0, + "slot": "270", + "type": "t_uint192", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:86" + }, + { + "label": "_nonces", + "offset": 0, + "slot": "271", + "type": "t_mapping(t_address,t_struct(Counter)2739_storage)", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:124" + }, + { + "label": "_delegationNonces", + "offset": 0, + "slot": "272", + "type": "t_mapping(t_address,t_struct(Counter)2739_storage)", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:126" + }, + { + "label": "unstakingDelay", + "offset": 0, + "slot": "273", + "type": "t_uint48", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:136" + }, + { + "label": "rewardRatio", + "offset": 6, + "slot": "273", + "type": "t_uint192", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:137" + }, + { + "label": "payoutLastPaid", + "offset": 0, + "slot": "274", + "type": "t_uint48", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:148" + }, + { + "label": "rsrRewardsAtLastPayout", + "offset": 0, + "slot": "275", + "type": "t_uint256", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:151" + }, + { + "label": "leaked", + "offset": 0, + "slot": "276", + "type": "t_uint192", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:157" + }, + { + "label": "lastWithdrawRefresh", + "offset": 24, + "slot": "276", + "type": "t_uint48", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:158" + }, + { + "label": "withdrawalLeak", + "offset": 0, + "slot": "277", + "type": "t_uint192", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:159" + }, + { + "label": "__gap", + "offset": 0, + "slot": "278", + "type": "t_array(t_uint256)28_storage", + "contract": "StRSRP1", + "src": "contracts/p1/StRSR.sol:958" + }, + { + "label": "_delegates", + "offset": 0, + "slot": "306", + "type": "t_mapping(t_address,t_address)", + "contract": "StRSRP1Votes", + "src": "contracts/p1/StRSRVotes.sol:31" + }, + { + "label": "_eras", + "offset": 0, + "slot": "307", + "type": "t_array(t_struct(Checkpoint)48275_storage)dyn_storage", + "contract": "StRSRP1Votes", + "src": "contracts/p1/StRSRVotes.sol:34" + }, + { + "label": "_checkpoints", + "offset": 0, + "slot": "308", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_array(t_struct(Checkpoint)48275_storage)dyn_storage))", + "contract": "StRSRP1Votes", + "src": "contracts/p1/StRSRVotes.sol:38" + }, + { + "label": "_totalSupplyCheckpoints", + "offset": 0, + "slot": "309", + "type": "t_mapping(t_uint256,t_array(t_struct(Checkpoint)48275_storage)dyn_storage)", + "contract": "StRSRP1Votes", + "src": "contracts/p1/StRSRVotes.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "310", + "type": "t_array(t_uint256)46_storage", + "contract": "StRSRP1Votes", + "src": "contracts/p1/StRSRVotes.sol:243" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Checkpoint)48275_storage)dyn_storage": { + "label": "struct StRSRP1Votes.Checkpoint[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(CumulativeDraft)46080_storage)dyn_storage": { + "label": "struct StRSRP1.CumulativeDraft[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)28_storage": { + "label": "uint256[28]", + "numberOfBytes": "896" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IAssetRegistry)20716": { + "label": "contract IAssetRegistry", + "numberOfBytes": "20" + }, + "t_contract(IBackingManager)20778": { + "label": "contract IBackingManager", + "numberOfBytes": "20" + }, + "t_contract(IBasketHandler)21025": { + "label": "contract IBasketHandler", + "numberOfBytes": "20" + }, + "t_contract(IERC20)11635": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IMain)22420": { + "label": "contract IMain", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_address)": { + "label": "mapping(address => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_array(t_struct(Checkpoint)48275_storage)dyn_storage)": { + "label": "mapping(address => struct StRSRP1Votes.Checkpoint[])", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_array(t_struct(CumulativeDraft)46080_storage)dyn_storage)": { + "label": "mapping(address => struct StRSRP1.CumulativeDraft[])", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Counter)2739_storage)": { + "label": "mapping(address => struct CountersUpgradeable.Counter)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_struct(Checkpoint)48275_storage)dyn_storage)": { + "label": "mapping(uint256 => struct StRSRP1Votes.Checkpoint[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_array(t_struct(Checkpoint)48275_storage)dyn_storage))": { + "label": "mapping(uint256 => mapping(address => struct StRSRP1Votes.Checkpoint[]))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_array(t_struct(CumulativeDraft)46080_storage)dyn_storage))": { + "label": "mapping(uint256 => mapping(address => struct StRSRP1.CumulativeDraft[]))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_mapping(t_address,t_uint256)))": { + "label": "mapping(uint256 => mapping(address => mapping(address => uint256)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_uint256))": { + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Checkpoint)48275_storage": { + "label": "struct StRSRP1Votes.Checkpoint", + "members": [ + { + "label": "fromBlock", + "type": "t_uint48", + "offset": 0, + "slot": "0" + }, + { + "label": "val", + "type": "t_uint224", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Counter)2739_storage": { + "label": "struct CountersUpgradeable.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(CumulativeDraft)46080_storage": { + "label": "struct StRSRP1.CumulativeDraft", + "members": [ + { + "label": "drafts", + "type": "t_uint176", + "offset": 0, + "slot": "0" + }, + { + "label": "availableAt", + "type": "t_uint64", + "offset": 22, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint176": { + "label": "uint176", + "numberOfBytes": "22" + }, + "t_uint192": { + "label": "uint192", + "numberOfBytes": "24" + }, + "t_uint224": { + "label": "uint224", + "numberOfBytes": "28" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint48": { + "label": "uint48", + "numberOfBytes": "6" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/.solcover.js b/.solcover.js index df2f65f1d..c5f8686d5 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,17 +2,17 @@ module.exports = { skipFiles: [ 'mocks', 'test', - 'prod/test', + 'vendor', 'libraries/Fixed.sol', 'libraries/test', - 'p0/test', - 'p0/mocks', - 'p1/test', - 'p1/mocks', - 'p2/test', - 'p2/mocks', 'plugins/mocks', - 'plugins/aave', + 'plugins/assets/aave/vendor', + 'plugins/assets/ankr/vendor', + 'plugins/assets/compoundv3/vendor', + 'plugins/assets/curve/cvx/vendor', + 'plugins/assets/frax-eth/vendor', + 'plugins/assets/lido/vendor', + 'plugins/assets/rocket-eth/vendor', 'fuzz', ], configureYulOptimizer: true, diff --git a/.solhintignore b/.solhintignore index 01246d39d..db227ef74 100644 --- a/.solhintignore +++ b/.solhintignore @@ -18,4 +18,5 @@ contracts/libraries/Fixed.sol contracts/vendor contracts/plugins/assets/compound/vendor -contracts/plugins/assets/convex/vendor +contracts/plugins/assets/curve/crv +contracts/plugins/assets/curve/cvx diff --git a/CHANGELOG.md b/CHANGELOG.md index cb6d15e37..c8b49994a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Collateral / Asset plugins from 2.1.0 do not need to be upgraded #### Core Protocol Contracts +Bump solidity version to 0.8.19 + - `AssetRegistry` [+1 slot] Summary: Other component contracts need to know when refresh() was last called - Add last refresh timestamp tracking and expose via `lastRefresh()` getter diff --git a/README.md b/README.md index 9f1068837..ae48bf253 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ If you would like to contribute, you'll need to configure a secret in your fork Usage: `https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }}` +To get setup with tenderly, install the [tenderly cli](https://github.com/Tenderly/tenderly-cli). and login with `tenderly login --authentication-method access-key --access-key {your_access_key} --force`. + ## External Documentation [Video overview](https://youtu.be/341MhkOWsJE) diff --git a/common/configuration.ts b/common/configuration.ts index 1c8cdf1d7..6f14eff79 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -13,6 +13,7 @@ export interface ITokens { USDP?: string TUSD?: string BUSD?: string + sUSD?: string FRAX?: string MIM?: string eUSD?: string @@ -48,6 +49,9 @@ export interface ITokens { wstETH?: string rETH?: string cUSDCv3?: string + ONDO?: string + sDAI?: string + cbETH?: string } export interface IFeeds { @@ -55,11 +59,15 @@ export interface IFeeds { stETHUSD?: string } -export interface IPlugins { +export interface IPools { cvx3Pool?: string cvxeUSDFRAXBP?: string cvxTriCrypto?: string cvxMIM3Pool?: string + crv3Pool?: string + crveUSDFRAXBP?: string + crvTriCrypto?: string + crvMIM3Pool?: string } interface INetworkConfig { @@ -69,6 +77,7 @@ interface INetworkConfig { AAVE_LENDING_POOL?: string AAVE_INCENTIVES?: string AAVE_EMISSIONS_MGR?: string + AAVE_RESERVE_TREASURY?: string COMPTROLLER?: string FLUX_FINANCE_COMPTROLLER?: string GNOSIS_EASY_AUCTION?: string @@ -91,6 +100,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { BUSD: '0x4Fabb145d64652a948d72533023f6E7A623C7C53', USDP: '0x8E870D67F660D95d5be530380D0eC0bd388289E1', TUSD: '0x0000000000085d4780B73119b644AE5ecd22b376', + sUSD: '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', FRAX: '0x853d955aCEf822Db058eb8505911ED77F175b99e', MIM: '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3', eUSD: '0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F', @@ -126,6 +136,9 @@ export const networkConfig: { [key: string]: INetworkConfig } = { wstETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', rETH: '0xae78736Cd615f374D3085123A210448E74Fc6393', cUSDCv3: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + ONDO: '0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3', + sDAI: '0x83f20f44975d03b1b09e64809b757c47f942beea', + cbETH: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', @@ -137,6 +150,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { BUSD: '0x833D8Eb16D306ed1FbB5D7A2E019e106B960965A', USDP: '0x09023c0DA49Aaf8fc3fA3ADF34C6A7016D38D5e3', TUSD: '0xec746eCF986E2927Abd291a2A1716c940100f8Ba', + sUSD: '0xad35Bd71b9aFE6e4bDc266B345c198eaDEf9Ad94', FRAX: '0xB9E1E3A9feFf48998E45Fa90847ed4D467E8BcfD', MIM: '0x7A364e8770418566e3eb2001A96116E6138Eb32F', ETH: '0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419', @@ -149,10 +163,12 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0x86392dc19c0b719886221c78ab11eb8cf5c52812', // stETH/ETH stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH + cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH }, AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9', AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5', AAVE_EMISSIONS_MGR: '0xEE56e2B3D491590B5b31738cC34d5232F378a8D5', + AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c', FLUX_FINANCE_COMPTROLLER: '0x95Af143a021DF745bc78e845b54591C53a8B3A51', COMPTROLLER: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101', @@ -176,6 +192,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { BUSD: '0x4Fabb145d64652a948d72533023f6E7A623C7C53', USDP: '0x8E870D67F660D95d5be530380D0eC0bd388289E1', TUSD: '0x0000000000085d4780B73119b644AE5ecd22b376', + sUSD: '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', FRAX: '0x853d955aCEf822Db058eb8505911ED77F175b99e', MIM: '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3', eUSD: '0xA0d69E286B938e21CBf7E51D71F6A4c8918f482F', @@ -211,6 +228,9 @@ export const networkConfig: { [key: string]: INetworkConfig } = { wstETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', rETH: '0xae78736Cd615f374D3085123A210448E74Fc6393', cUSDCv3: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + ONDO: '0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3', + sDAI: '0x83f20f44975d03b1b09e64809b757c47f942beea', + cbETH: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', @@ -222,6 +242,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { BUSD: '0x833D8Eb16D306ed1FbB5D7A2E019e106B960965A', USDP: '0x09023c0DA49Aaf8fc3fA3ADF34C6A7016D38D5e3', TUSD: '0xec746eCF986E2927Abd291a2A1716c940100f8Ba', + sUSD: '0xad35Bd71b9aFE6e4bDc266B345c198eaDEf9Ad94', FRAX: '0xB9E1E3A9feFf48998E45Fa90847ed4D467E8BcfD', MIM: '0x7A364e8770418566e3eb2001A96116E6138Eb32F', ETH: '0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419', @@ -234,8 +255,10 @@ export const networkConfig: { [key: string]: INetworkConfig } = { stETHETH: '0x86392dc19c0b719886221c78ab11eb8cf5c52812', // stETH/ETH stETHUSD: '0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8', // stETH/USD rETH: '0x536218f9E9Eb48863970252233c8F271f554C2d0', // rETH/ETH + cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH }, AAVE_LENDING_POOL: '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9', + AAVE_RESERVE_TREASURY: '0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c', FLUX_FINANCE_COMPTROLLER: '0x95Af143a021DF745bc78e845b54591C53a8B3A51', COMPTROLLER: '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', GNOSIS_EASY_AUCTION: '0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101', @@ -268,7 +291,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = { fDAI: '0x7e1e077b289c0153b5ceAD9F264d66215341c9Ab', AAVE: '0xc47324262e1C7be67270Da717e1a0e7b0191c449', stkAAVE: '0x3Db8b170DA19c45B63B959789f20f397F22767D4', - COMP: '0x1b4449895037f25b102B28B45b8bD50c8C44Aca1', + COMP: '0xe16c7165c8fea64069802ae4c4c9c320783f2b6e', // canonical COMP WETH: '0xB5B58F0a853132EA8cB614cb17095dE87AF3E98b', WBTC: '0x528FdEd7CC39209ed67B4edA11937A9ABe1f6249', EURT: '0xD6da5A7ADE2a906d9992612752A339E3485dB508', @@ -278,9 +301,9 @@ export const networkConfig: { [key: string]: INetworkConfig } = { ankrETH: '0xE2b16e14dB6216e33082D5A8Be1Ef01DF7511bBb', frxETH: '0xe0E1d3c6f09DA01399e84699722B11308607BBfC', sfrxETH: '0x291ed25eB61fcc074156eE79c5Da87e5DA94198F', - stETH: '0x97F9d5ed17A0C99B279887caD5254d15fb1B619B', - wstETH: '0xd000a79BD2a07EB6D2e02ECAd73437De40E52d69', - rETH: '0x2304E98cD1E2F0fd3b4E30A1Bc6E9594dE2ea9b7', + stETH: '0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F', + wstETH: '0x6320cD32aA674d2898A68ec82e869385Fc5f7E2f', + rETH: '0x178E141a0E3b34152f73Ff610437A7bf9B83267A', cUSDCv3: '0x719fbae9e2Dcd525bCf060a8D5DBC6C9fE104A50', }, chainlinkFeeds: { @@ -300,10 +323,13 @@ export const networkConfig: { [key: string]: INetworkConfig } = { WBTC: '0xe52CE9436F2D4D4B744720aAEEfD9C6dbFC00b34', EURT: '0x68aA66BCde901c741C5EF07314875434E51E5D30', EUR: '0x12336777de46b9a6Edd7176E532810149C787bcD', + rETH: '0xeb1cDb6C2F18173eaC53fEd1DC03fe13286f86ec', + stETHUSD: '0x6dCCE86FFb3c1FC44Ded9a6E200eF12d0D4256a3', + stETHETH: '0x81ff01E93F86f41d3DFf66283Be2aD0a3C284604' }, AAVE_LENDING_POOL: '0x3e9E33B84C1cD9037be16AA45A0B296ae5F185AD', // mock GNOSIS_EASY_AUCTION: '0x1fbab40c338e2e7243da945820ba680c92ef8281', // canonical - COMPTROLLER: '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b', // canonical + COMPTROLLER: '0x627ea49279fd0de89186a58b8758ad02b6be2867', // canonical }, } @@ -422,6 +448,7 @@ export const MAX_MIN_TRADE_VOLUME = BigNumber.from(10).pow(29) export const MIN_THROTTLE_AMT_RATE = BigNumber.from(10).pow(18) export const MAX_THROTTLE_AMT_RATE = BigNumber.from(10).pow(48) export const MAX_THROTTLE_PCT_RATE = BigNumber.from(10).pow(18) +export const GNOSIS_MAX_TOKENS = BigNumber.from(7).mul(BigNumber.from(10).pow(28)) // Timestamps export const MAX_ORACLE_TIMEOUT = BigNumber.from(2).pow(48).sub(1) diff --git a/contracts/facade/DeployerRegistry.sol b/contracts/facade/DeployerRegistry.sol index 74fd82ad1..1e3344c89 100644 --- a/contracts/facade/DeployerRegistry.sol +++ b/contracts/facade/DeployerRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "../interfaces/IDeployerRegistry.sol"; diff --git a/contracts/facade/FacadeAct.sol b/contracts/facade/FacadeAct.sol index a0671dd27..283d9cb38 100644 --- a/contracts/facade/FacadeAct.sol +++ b/contracts/facade/FacadeAct.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/Multicall.sol"; @@ -14,6 +14,7 @@ import "../interfaces/IFacadeRead.sol"; * For use with ^3.0.0 RTokens. */ contract FacadeAct is IFacadeAct, Multicall { + using SafeERC20 for IERC20; using FixLib for uint192; function claimRewards(IRToken rToken) public { diff --git a/contracts/facade/FacadeRead.sol b/contracts/facade/FacadeRead.sol index 3f93878a8..29f076e40 100644 --- a/contracts/facade/FacadeRead.sol +++ b/contracts/facade/FacadeRead.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../plugins/trading/DutchTrade.sol"; diff --git a/contracts/facade/FacadeTest.sol b/contracts/facade/FacadeTest.sol index de71f7542..749a5bf42 100644 --- a/contracts/facade/FacadeTest.sol +++ b/contracts/facade/FacadeTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../interfaces/IAsset.sol"; diff --git a/contracts/facade/FacadeWrite.sol b/contracts/facade/FacadeWrite.sol index 134fa7c70..7d4a4c2c1 100644 --- a/contracts/facade/FacadeWrite.sol +++ b/contracts/facade/FacadeWrite.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../interfaces/IFacadeWrite.sol"; import "./lib/FacadeWriteLib.sol"; diff --git a/contracts/facade/lib/FacadeWriteLib.sol b/contracts/facade/lib/FacadeWriteLib.sol index 2671fe133..8ad05ba40 100644 --- a/contracts/facade/lib/FacadeWriteLib.sol +++ b/contracts/facade/lib/FacadeWriteLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../plugins/governance/Governance.sol"; diff --git a/contracts/interfaces/IAsset.sol b/contracts/interfaces/IAsset.sol index 824b9c403..bd796190a 100644 --- a/contracts/interfaces/IAsset.sol +++ b/contracts/interfaces/IAsset.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/interfaces/IAssetRegistry.sol b/contracts/interfaces/IAssetRegistry.sol index b5bdacf2f..caeaac2f3 100644 --- a/contracts/interfaces/IAssetRegistry.sol +++ b/contracts/interfaces/IAssetRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IAsset.sol"; diff --git a/contracts/interfaces/IBackingManager.sol b/contracts/interfaces/IBackingManager.sol index 982a79aa9..1aa4b9fb3 100644 --- a/contracts/interfaces/IBackingManager.sol +++ b/contracts/interfaces/IBackingManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IBroker.sol"; diff --git a/contracts/interfaces/IBasketHandler.sol b/contracts/interfaces/IBasketHandler.sol index 30a9c355d..f94455084 100644 --- a/contracts/interfaces/IBasketHandler.sol +++ b/contracts/interfaces/IBasketHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../libraries/Fixed.sol"; diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index 13a803109..c02fab919 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IAsset.sol"; import "./IComponent.sol"; diff --git a/contracts/interfaces/IComponent.sol b/contracts/interfaces/IComponent.sol index 90f227bd7..88d221e4b 100644 --- a/contracts/interfaces/IComponent.sol +++ b/contracts/interfaces/IComponent.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IMain.sol"; import "./IVersioned.sol"; diff --git a/contracts/interfaces/IDeployer.sol b/contracts/interfaces/IDeployer.sol index d338d1160..6eb344aa0 100644 --- a/contracts/interfaces/IDeployer.sol +++ b/contracts/interfaces/IDeployer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../libraries/Throttle.sol"; @@ -30,7 +30,7 @@ struct DeploymentParams { uint48 longFreeze; // {s} how long each freeze extension lasts // // === Rewards (Furnace + StRSR) === - uint192 rewardRatio; // the fraction of available revenues that are paid out each 12s period + uint192 rewardRatio; // the fraction of available revenues that are paid out each block period // // === StRSR === uint48 unstakingDelay; // {s} the "thawing time" of staked RSR before withdrawal diff --git a/contracts/interfaces/IDeployerRegistry.sol b/contracts/interfaces/IDeployerRegistry.sol index 85c9b1b63..25e0a33bd 100644 --- a/contracts/interfaces/IDeployerRegistry.sol +++ b/contracts/interfaces/IDeployerRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IDeployer.sol"; diff --git a/contracts/interfaces/IDistributor.sol b/contracts/interfaces/IDistributor.sol index 19e69cbdb..10606c6cb 100644 --- a/contracts/interfaces/IDistributor.sol +++ b/contracts/interfaces/IDistributor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IComponent.sol"; diff --git a/contracts/interfaces/IFacadeAct.sol b/contracts/interfaces/IFacadeAct.sol index 600071dae..b3c77eea0 100644 --- a/contracts/interfaces/IFacadeAct.sol +++ b/contracts/interfaces/IFacadeAct.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../interfaces/IBackingManager.sol"; +import "../interfaces/IStRSRVotes.sol"; import "../interfaces/IRevenueTrader.sol"; import "../interfaces/IRToken.sol"; diff --git a/contracts/interfaces/IFacadeRead.sol b/contracts/interfaces/IFacadeRead.sol index 3490690d6..61e2dbd52 100644 --- a/contracts/interfaces/IFacadeRead.sol +++ b/contracts/interfaces/IFacadeRead.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../p1/RToken.sol"; import "./IRToken.sol"; diff --git a/contracts/interfaces/IFacadeTest.sol b/contracts/interfaces/IFacadeTest.sol index cdcb3e121..6d2de1ece 100644 --- a/contracts/interfaces/IFacadeTest.sol +++ b/contracts/interfaces/IFacadeTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IRToken.sol"; import "./IStRSR.sol"; diff --git a/contracts/interfaces/IFacadeWrite.sol b/contracts/interfaces/IFacadeWrite.sol index 04d4bd7be..f56cd3294 100644 --- a/contracts/interfaces/IFacadeWrite.sol +++ b/contracts/interfaces/IFacadeWrite.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IDeployer.sol"; diff --git a/contracts/interfaces/IFurnace.sol b/contracts/interfaces/IFurnace.sol index 405459d66..25c754aac 100644 --- a/contracts/interfaces/IFurnace.sol +++ b/contracts/interfaces/IFurnace.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../libraries/Fixed.sol"; import "./IComponent.sol"; diff --git a/contracts/interfaces/IGnosis.sol b/contracts/interfaces/IGnosis.sol index 1c7d0b95c..4439de805 100644 --- a/contracts/interfaces/IGnosis.sol +++ b/contracts/interfaces/IGnosis.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/IMain.sol b/contracts/interfaces/IMain.sol index fbdb421f1..00bb261de 100644 --- a/contracts/interfaces/IMain.sol +++ b/contracts/interfaces/IMain.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -16,10 +16,6 @@ import "./IStRSR.sol"; import "./ITrading.sol"; import "./IVersioned.sol"; -// Warning, assumption: Chain should have blocktimes of 12s -// See docs/system-design.md for discussion of handling longer or shorter times -uint48 constant ONE_BLOCK = 12; //{s} - // === Auth roles === bytes32 constant OWNER = bytes32(bytes("OWNER")); diff --git a/contracts/interfaces/IRToken.sol b/contracts/interfaces/IRToken.sol index fe6246923..31bf6cc9f 100644 --- a/contracts/interfaces/IRToken.sol +++ b/contracts/interfaces/IRToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; // solhint-disable-next-line max-line-length diff --git a/contracts/interfaces/IRTokenOracle.sol b/contracts/interfaces/IRTokenOracle.sol new file mode 100644 index 000000000..e4f42dde3 --- /dev/null +++ b/contracts/interfaces/IRTokenOracle.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +// RToken Oracle Interface +interface IRTokenOracle { + struct CachedOracleData { + uint192 cachedPrice; // {UoA/tok} + uint256 cachedAtTime; // {s} + uint48 cachedAtNonce; // {basketNonce} + uint48 cachedTradesOpen; + uint256 cachedTradesNonce; // {tradeNonce} + } + + // @returns rTokenPrice {D18} {UoA/rTok} The price of the RToken, in UoA + function latestPrice() external returns (uint192 rTokenPrice, uint256 updatedAt); + + // Force recalculate the price of the RToken + function forceUpdatePrice() external; +} diff --git a/contracts/interfaces/IRevenueTrader.sol b/contracts/interfaces/IRevenueTrader.sol index 7f90f20b8..aaff95bc3 100644 --- a/contracts/interfaces/IRevenueTrader.sol +++ b/contracts/interfaces/IRevenueTrader.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IBroker.sol"; import "./IComponent.sol"; diff --git a/contracts/interfaces/IRewardable.sol b/contracts/interfaces/IRewardable.sol index 3749ed0f6..90563bad5 100644 --- a/contracts/interfaces/IRewardable.sol +++ b/contracts/interfaces/IRewardable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IComponent.sol"; diff --git a/contracts/interfaces/IStRSR.sol b/contracts/interfaces/IStRSR.sol index cf503404f..45b7db922 100644 --- a/contracts/interfaces/IStRSR.sol +++ b/contracts/interfaces/IStRSR.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; // solhint-disable-next-line max-line-length diff --git a/contracts/interfaces/IStRSRVotes.sol b/contracts/interfaces/IStRSRVotes.sol index 1f5968470..246ead7cd 100644 --- a/contracts/interfaces/IStRSRVotes.sol +++ b/contracts/interfaces/IStRSRVotes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; diff --git a/contracts/interfaces/ITrade.sol b/contracts/interfaces/ITrade.sol index 57ae23b50..d05e3028f 100644 --- a/contracts/interfaces/ITrade.sol +++ b/contracts/interfaces/ITrade.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "./IBroker.sol"; diff --git a/contracts/interfaces/ITrading.sol b/contracts/interfaces/ITrading.sol index fd3801855..4c40bce0e 100644 --- a/contracts/interfaces/ITrading.sol +++ b/contracts/interfaces/ITrading.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../libraries/Fixed.sol"; @@ -62,13 +62,8 @@ interface ITrading is IComponent, IRewardableComponent { /// @return The number of ongoing trades open function tradesOpen() external view returns (uint48); - /// Light wrapper around FixLib.mulDiv to support try-catch - function mulDiv( - uint192 x, - uint192 y, - uint192 z, - RoundingMode rounding - ) external pure returns (uint192); + /// @return The number of total trades ever opened + function tradesNonce() external view returns (uint256); } interface TestITrading is ITrading { diff --git a/contracts/interfaces/IVersioned.sol b/contracts/interfaces/IVersioned.sol index 0fc1df4ae..b7d98407c 100644 --- a/contracts/interfaces/IVersioned.sol +++ b/contracts/interfaces/IVersioned.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface IVersioned { function version() external view returns (string memory); diff --git a/contracts/libraries/Array.sol b/contracts/libraries/Array.sol index 2e8b75d62..5a46e97ff 100644 --- a/contracts/libraries/Array.sol +++ b/contracts/libraries/Array.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/libraries/Fixed.sol b/contracts/libraries/Fixed.sol index e22042719..de4e6c37b 100644 --- a/contracts/libraries/Fixed.sol +++ b/contracts/libraries/Fixed.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BlueOak-1.0.0 // solhint-disable func-name-mixedcase func-visibility -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; /// @title FixedPoint, a fixed-point arithmetic library defining the custom type uint192 /// @author Matt Elder and the Reserve Team @@ -532,6 +532,73 @@ library FixLib { return uint192(shiftDelta / FIX_ONE); // {D18} = {D36} / {D18} } } + + /// Divide two fixes, rounding up to FIX_MAX and down to 0 + /// @param a Numerator + /// @param b Denominator + function safeDiv( + uint192 a, + uint192 b, + RoundingMode rounding + ) internal pure returns (uint192) { + if (a == 0) return 0; + if (b == 0) return FIX_MAX; + + uint256 raw = _divrnd(FIX_ONE_256 * a, uint256(b), rounding); + if (raw >= FIX_MAX) return FIX_MAX; + return uint192(raw); // don't need _safeWrap + } + + /// Multiplies two fixes and divide by a third + /// @param a First to multiply + /// @param b Second to multiply + /// @param c Denominator + function safeMulDiv( + uint192 a, + uint192 b, + uint192 c, + RoundingMode rounding + ) internal pure returns (uint192 result) { + if (a == 0 || b == 0) return 0; + if (a == FIX_MAX || b == FIX_MAX || c == 0) return FIX_MAX; + + uint256 result_256; + unchecked { + (uint256 hi, uint256 lo) = fullMul(a, b); + if (hi >= c) return FIX_MAX; + uint256 mm = mulmod(a, b, c); + if (mm > lo) hi -= 1; + lo -= mm; + uint256 pow2 = c & (0 - c); + + uint256 c_256 = uint256(c); + // Warning: Should not access c below this line + + c_256 /= pow2; + lo /= pow2; + lo += hi * ((0 - pow2) / pow2 + 1); + uint256 r = 1; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + r *= 2 - c_256 * r; + result_256 = lo * r; + + // Apply rounding + if (rounding == CEIL) { + if (mm > 0) result_256 += 1; + } else if (rounding == ROUND) { + if (mm > ((c_256 - 1) / 2)) result_256 += 1; + } + } + + if (result_256 >= FIX_MAX) return FIX_MAX; + return uint192(result_256); + } } // ================ a couple pure-uint helpers================ diff --git a/contracts/libraries/Permit.sol b/contracts/libraries/Permit.sol index fe04b1f37..ecda68e89 100644 --- a/contracts/libraries/Permit.sol +++ b/contracts/libraries/Permit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/SignatureCheckerUpgradeable.sol"; diff --git a/contracts/libraries/String.sol b/contracts/libraries/String.sol index a48103f2f..53a473fe5 100644 --- a/contracts/libraries/String.sol +++ b/contracts/libraries/String.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; // From https://gist.github.com/ottodevs/c43d0a8b4b891ac2da675f825b1d1dbf library StringLib { diff --git a/contracts/libraries/Throttle.sol b/contracts/libraries/Throttle.sol index d43b89e39..258796be2 100644 --- a/contracts/libraries/Throttle.sol +++ b/contracts/libraries/Throttle.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./Fixed.sol"; diff --git a/contracts/libraries/test/ArrayCallerMock.sol b/contracts/libraries/test/ArrayCallerMock.sol index d9595c3e2..ab823990b 100644 --- a/contracts/libraries/test/ArrayCallerMock.sol +++ b/contracts/libraries/test/ArrayCallerMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; import "../Array.sol"; diff --git a/contracts/libraries/test/FixedCallerMock.sol b/contracts/libraries/test/FixedCallerMock.sol index f9cdf1dec..98701eb58 100644 --- a/contracts/libraries/test/FixedCallerMock.sol +++ b/contracts/libraries/test/FixedCallerMock.sol @@ -1,156 +1,282 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; import "../Fixed.sol"; // Simple mock for Fixed library. -// prettier-ignore contract FixedCallerMock { - function toFix_(uint256 x) public pure returns (uint192 ) { + function toFix_(uint256 x) public pure returns (uint192) { return toFix(x); } - function shiftl_toFix_(uint256 x, int8 d) public pure returns (uint192 ) { + + function shiftl_toFix_(uint256 x, int8 d) public pure returns (uint192) { return shiftl_toFix(x, d); } - function shiftl_toFix_Rnd(uint256 x, int8 d, RoundingMode rnd) public pure returns (uint192 ) { + + function shiftl_toFix_Rnd( + uint256 x, + int8 d, + RoundingMode rnd + ) public pure returns (uint192) { return shiftl_toFix(x, d, rnd); } - function divFix_(uint256 x, uint192 y) public pure returns (uint192 ) { + + function divFix_(uint256 x, uint192 y) public pure returns (uint192) { return divFix(x, y); } + function divuu_(uint256 x, uint256 y) public pure returns (uint256) { return divuu(x, y); } - function fixMin_(uint192 x, uint192 y) public pure returns (uint192 ) { + + function fixMin_(uint192 x, uint192 y) public pure returns (uint192) { return fixMin(x, y); } - function fixMax_(uint192 x, uint192 y) public pure returns (uint192 ) { + + function fixMax_(uint192 x, uint192 y) public pure returns (uint192) { return fixMax(x, y); } + function abs_(int256 x) public pure returns (uint256) { return abs(x); } - function divrnd_(uint256 n, uint256 d, RoundingMode rnd) public pure returns (uint256) { + + function divrnd_( + uint256 n, + uint256 d, + RoundingMode rnd + ) public pure returns (uint256) { return _divrnd(n, d, rnd); } - function toUint(uint192 x) public pure returns (uint256 ) { + + function toUint(uint192 x) public pure returns (uint256) { return FixLib.toUint(x); } - function toUintRnd(uint192 x, RoundingMode rnd) public pure returns (uint256 ) { + + function toUintRnd(uint192 x, RoundingMode rnd) public pure returns (uint256) { return FixLib.toUint(x, rnd); } - function shiftl(uint192 x, int8 decimals) public pure returns (uint192 ) { + + function shiftl(uint192 x, int8 decimals) public pure returns (uint192) { return FixLib.shiftl(x, decimals); } - function shiftlRnd(uint192 x, int8 decimals, RoundingMode rnd) public pure returns (uint192 ) { + + function shiftlRnd( + uint192 x, + int8 decimals, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.shiftl(x, decimals, rnd); } - function plus(uint192 x, uint192 y) public pure returns (uint192 ) { + + function plus(uint192 x, uint192 y) public pure returns (uint192) { return FixLib.plus(x, y); } - function plusu(uint192 x, uint256 y) public pure returns (uint192 ) { + + function plusu(uint192 x, uint256 y) public pure returns (uint192) { return FixLib.plusu(x, y); } - function minus(uint192 x, uint192 y) public pure returns (uint192 ) { + + function minus(uint192 x, uint192 y) public pure returns (uint192) { return FixLib.minus(x, y); } - function minusu(uint192 x, uint256 y) public pure returns (uint192 ) { + + function minusu(uint192 x, uint256 y) public pure returns (uint192) { return FixLib.minusu(x, y); } - function mul(uint192 x, uint192 y) public pure returns (uint192 ) { + + function mul(uint192 x, uint192 y) public pure returns (uint192) { return FixLib.mul(x, y); } - function mulRnd(uint192 x, uint192 y, RoundingMode rnd) public pure returns (uint192 ) { + + function mulRnd( + uint192 x, + uint192 y, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.mul(x, y, rnd); } - function mulu(uint192 x, uint256 y) public pure returns (uint192 ) { + + function mulu(uint192 x, uint256 y) public pure returns (uint192) { return FixLib.mulu(x, y); } - function div(uint192 x, uint192 y) public pure returns (uint192 ) { + + function div(uint192 x, uint192 y) public pure returns (uint192) { return FixLib.div(x, y); } - function divRnd(uint192 x, uint192 y, RoundingMode rnd) public pure returns (uint192 ) { + + function divRnd( + uint192 x, + uint192 y, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.div(x, y, rnd); } - function divu(uint192 x, uint256 y) public pure returns (uint192 ) { + + function divu(uint192 x, uint256 y) public pure returns (uint192) { return FixLib.divu(x, y); } - function divuRnd(uint192 x, uint256 y, RoundingMode rnd) public pure returns (uint192 ) { + + function divuRnd( + uint192 x, + uint256 y, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.divu(x, y, rnd); } - function powu(uint192 x, uint48 y) public pure returns (uint192 ) { + + function powu(uint192 x, uint48 y) public pure returns (uint192) { return FixLib.powu(x, y); } - function lt(uint192 x, uint192 y) public pure returns (bool) { + + function lt(uint192 x, uint192 y) public pure returns (bool) { return FixLib.lt(x, y); } - function lte(uint192 x, uint192 y) public pure returns (bool) { + + function lte(uint192 x, uint192 y) public pure returns (bool) { return FixLib.lte(x, y); } - function gt(uint192 x, uint192 y) public pure returns (bool) { + + function gt(uint192 x, uint192 y) public pure returns (bool) { return FixLib.gt(x, y); } - function gte(uint192 x, uint192 y) public pure returns (bool) { + + function gte(uint192 x, uint192 y) public pure returns (bool) { return FixLib.gte(x, y); } - function eq(uint192 x, uint192 y) public pure returns (bool) { + + function eq(uint192 x, uint192 y) public pure returns (bool) { return FixLib.eq(x, y); } - function neq(uint192 x, uint192 y) public pure returns (bool) { + + function neq(uint192 x, uint192 y) public pure returns (bool) { return FixLib.neq(x, y); } - function near(uint192 x, uint192 y, uint192 epsilon) public pure returns (bool) { + + function near( + uint192 x, + uint192 y, + uint192 epsilon + ) public pure returns (bool) { return FixLib.near(x, y, epsilon); } // ================ chained operations - function shiftl_toUint(uint192 x, int8 d) public pure returns (uint256) { + function shiftl_toUint(uint192 x, int8 d) public pure returns (uint256) { return FixLib.shiftl_toUint(x, d); } - function shiftl_toUintRnd(uint192 x, int8 d, RoundingMode rnd) public pure returns (uint256) { + + function shiftl_toUintRnd( + uint192 x, + int8 d, + RoundingMode rnd + ) public pure returns (uint256) { return FixLib.shiftl_toUint(x, d, rnd); } - function mulu_toUint(uint192 x, uint256 y) public pure returns (uint256) { + + function mulu_toUint(uint192 x, uint256 y) public pure returns (uint256) { return FixLib.mulu_toUint(x, y); } - function mulu_toUintRnd(uint192 x, uint256 y, RoundingMode rnd) public pure returns (uint256) { + + function mulu_toUintRnd( + uint192 x, + uint256 y, + RoundingMode rnd + ) public pure returns (uint256) { return FixLib.mulu_toUint(x, y, rnd); } - function mul_toUint(uint192 x, uint192 y) public pure returns (uint256) { + + function mul_toUint(uint192 x, uint192 y) public pure returns (uint256) { return FixLib.mul_toUint(x, y); } - function mul_toUintRnd(uint192 x, uint192 y, RoundingMode rnd) public pure returns (uint256) { + + function mul_toUintRnd( + uint192 x, + uint192 y, + RoundingMode rnd + ) public pure returns (uint256) { return FixLib.mul_toUint(x, y, rnd); } - function muluDivu(uint192 x, uint256 y, uint256 z) public pure returns (uint192 ) { + + function muluDivu( + uint192 x, + uint256 y, + uint256 z + ) public pure returns (uint192) { return FixLib.muluDivu(x, y, z); } - function muluDivuRnd(uint192 x, uint256 y, uint256 z, RoundingMode rnd) public pure returns (uint192 ) { + + function muluDivuRnd( + uint192 x, + uint256 y, + uint256 z, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.muluDivu(x, y, z, rnd); } - function mulDiv(uint192 x, uint192 y, uint192 z) public pure returns (uint192 ) { + + function mulDiv( + uint192 x, + uint192 y, + uint192 z + ) public pure returns (uint192) { return FixLib.mulDiv(x, y, z); } - function mulDivRnd(uint192 x, uint192 y, uint192 z, RoundingMode rnd) public pure returns (uint192 ) { + + function mulDivRnd( + uint192 x, + uint192 y, + uint192 z, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.mulDiv(x, y, z, rnd); } + // ============== safe* operations - function safeMul_(uint192 a, uint192 b, RoundingMode rnd) public pure returns (uint192) { + function safeMul( + uint192 a, + uint192 b, + RoundingMode rnd + ) public pure returns (uint192) { return FixLib.safeMul(a, b, rnd); } + function safeDiv( + uint192 x, + uint192 y, + RoundingMode rnd + ) public pure returns (uint192) { + return FixLib.safeDiv(x, y, rnd); + } + + function safeMulDiv( + uint192 x, + uint192 y, + uint192 z, + RoundingMode rnd + ) public pure returns (uint192) { + return FixLib.safeMulDiv(x, y, z, rnd); + } + // ================ wide muldiv operations - function mulDiv256_(uint256 x, uint256 y, uint256 z) public pure returns (uint256) { + function mulDiv256_( + uint256 x, + uint256 y, + uint256 z + ) public pure returns (uint256) { return mulDiv256(x, y, z); } - function mulDiv256Rnd_(uint256 x, uint256 y, uint256 z, RoundingMode rnd) - public pure returns (uint256) { + + function mulDiv256Rnd_( + uint256 x, + uint256 y, + uint256 z, + RoundingMode rnd + ) public pure returns (uint256) { return mulDiv256(x, y, z, rnd); } + function fullMul_(uint256 x, uint256 y) public pure returns (uint256 h, uint256 l) { return fullMul(x, y); } - - - } diff --git a/contracts/libraries/test/StringCallerMock.sol b/contracts/libraries/test/StringCallerMock.sol index a96fb1495..7b3e39bc0 100644 --- a/contracts/libraries/test/StringCallerMock.sol +++ b/contracts/libraries/test/StringCallerMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; import "../String.sol"; diff --git a/contracts/mixins/Auth.sol b/contracts/mixins/Auth.sol index ea0583b15..24cd81d70 100644 --- a/contracts/mixins/Auth.sol +++ b/contracts/mixins/Auth.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "../interfaces/IMain.sol"; diff --git a/contracts/mixins/ComponentRegistry.sol b/contracts/mixins/ComponentRegistry.sol index 6dfdefa82..d8136c627 100644 --- a/contracts/mixins/ComponentRegistry.sol +++ b/contracts/mixins/ComponentRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; diff --git a/contracts/mixins/NetworkConfigLib.sol b/contracts/mixins/NetworkConfigLib.sol new file mode 100644 index 000000000..b347bec48 --- /dev/null +++ b/contracts/mixins/NetworkConfigLib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +/** + * @title NetworkConfigLib + * @notice Provides network-specific configuration parameters + */ +library NetworkConfigLib { + error InvalidNetwork(); + + // Returns the blocktime based on the current network (e.g. 12s for Ethereum PoS) + // See docs/system-design.md for discussion of handling longer or shorter times + function blocktime() internal view returns (uint48) { + uint256 chainId = block.chainid; + // untestable: + // most of the branches will be shown as uncovered, because we only run coverage + // on local Ethereum PoS network (31337). Manual testing was performed. + if (chainId == 1 || chainId == 5 || chainId == 31337) { + return 12; // Ethereum PoS, Goerli, HH (tests) + } else if (chainId == 8453 || chainId == 84531) { + return 2; // Base, Base Goerli + } else { + revert InvalidNetwork(); + } + } +} diff --git a/contracts/mixins/Versioned.sol b/contracts/mixins/Versioned.sol index bd57e4bd0..54c5f75da 100644 --- a/contracts/mixins/Versioned.sol +++ b/contracts/mixins/Versioned.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../interfaces/IVersioned.sol"; diff --git a/contracts/p0/AssetRegistry.sol b/contracts/p0/AssetRegistry.sol index cd4b0aded..91b9bb1a2 100644 --- a/contracts/p0/AssetRegistry.sol +++ b/contracts/p0/AssetRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; diff --git a/contracts/p0/BackingManager.sol b/contracts/p0/BackingManager.sol index 04f910426..cd7c063ca 100644 --- a/contracts/p0/BackingManager.sol +++ b/contracts/p0/BackingManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -11,6 +11,7 @@ import "../interfaces/IBroker.sol"; import "../interfaces/IMain.sol"; import "../libraries/Array.sol"; import "../libraries/Fixed.sol"; +import "../mixins/NetworkConfigLib.sol"; /** * @title BackingManager @@ -23,11 +24,18 @@ contract BackingManagerP0 is TradingP0, IBackingManager { uint48 public constant MAX_TRADING_DELAY = 31536000; // {s} 1 year uint192 public constant MAX_BACKING_BUFFER = 1e18; // {%} + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable ONE_BLOCK; // {s} 1 block based on network + uint48 public tradingDelay; // {s} how long to wait until resuming trading after switching uint192 public backingBuffer; // {%} how much extra backing collateral to keep mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind + constructor() { + ONE_BLOCK = NetworkConfigLib.blocktime(); + } + function init( IMain main_, uint48 tradingDelay_, @@ -81,7 +89,6 @@ contract BackingManagerP0 is TradingP0, IBackingManager { main.furnace().melt(); // DoS prevention: unless caller is self, require 1 empty block between like-kind auctions - // Assumption: chain has <= 12s blocktimes require( _msgSender() == address(this) || tradeEnd[kind] + ONE_BLOCK < block.timestamp, "already rebalancing" diff --git a/contracts/p0/BasketHandler.sol b/contracts/p0/BasketHandler.sol index ea6ca7ffa..3fc647432 100644 --- a/contracts/p0/BasketHandler.sol +++ b/contracts/p0/BasketHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -402,7 +402,14 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { : reg.toAsset(basket.erc20s[i]).price(); low256 += qty.safeMul(lowP, RoundingMode.FLOOR); - high256 += qty.safeMul(highP, RoundingMode.CEIL); + + if (high256 < FIX_MAX) { + if (highP == FIX_MAX) { + high256 = FIX_MAX; + } else { + high256 += qty.safeMul(highP, RoundingMode.CEIL); + } + } } // safe downcast: FIX_MAX is type(uint192).max @@ -503,7 +510,8 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { if (refPerTok == 0) continue; // {tok} = {BU} * {ref/BU} / {ref/tok} - quantities[i] = safeMulDivFloor(amount, refAmtsAll[i], refPerTok).shiftl_toUint( + + quantities[i] = amount.safeMulDiv(refAmtsAll[i], refPerTok, FLOOR).shiftl_toUint( int8(asset.erc20Decimals()), FLOOR ); @@ -798,24 +806,4 @@ contract BasketHandlerP0 is ComponentP0, IBasketHandler { return false; } } - - // === Private === - - /// @return The floored result of FixLib.mulDiv - function safeMulDivFloor( - uint192 x, - uint192 y, - uint192 z - ) private view returns (uint192) { - try main.backingManager().mulDiv(x, y, z, FLOOR) returns (uint192 result) { - return result; - } catch Panic(uint256 errorCode) { - // 0x11: overflow - // 0x12: div-by-zero - assert(errorCode == 0x11 || errorCode == 0x12); - } catch (bytes memory reason) { - assert(keccak256(reason) == UIntOutofBoundsHash); - } - return FIX_MAX; - } } diff --git a/contracts/p0/Broker.sol b/contracts/p0/Broker.sol index fd4e8b9f7..033ef0bf9 100644 --- a/contracts/p0/Broker.sol +++ b/contracts/p0/Broker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -22,8 +22,8 @@ contract BrokerP0 is ComponentP0, IBroker { using SafeERC20 for IERC20Metadata; uint48 public constant MAX_AUCTION_LENGTH = 604800; // {s} max valid duration -1 week - uint48 public constant MIN_AUCTION_LENGTH = ONE_BLOCK * 2; // {s} min auction length - 2 blocks - // warning: blocktime <= 12s assumption + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable MIN_AUCTION_LENGTH; // {s} 2 blocks based on network // Added for interface compatibility with P1 ITrade public batchTradeImplementation; @@ -38,6 +38,10 @@ contract BrokerP0 is ComponentP0, IBroker { bool public disabled; + constructor() { + MIN_AUCTION_LENGTH = NetworkConfigLib.blocktime() * 2; + } + function init( IMain main_, IGnosis gnosis_, diff --git a/contracts/p0/Deployer.sol b/contracts/p0/Deployer.sol index 70689b8f9..2b089b224 100644 --- a/contracts/p0/Deployer.sol +++ b/contracts/p0/Deployer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../plugins/assets/Asset.sol"; diff --git a/contracts/p0/Distributor.sol b/contracts/p0/Distributor.sol index bea4059c0..877485598 100644 --- a/contracts/p0/Distributor.sol +++ b/contracts/p0/Distributor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/p0/Furnace.sol b/contracts/p0/Furnace.sol index a8e436b66..9301b3b0f 100644 --- a/contracts/p0/Furnace.sol +++ b/contracts/p0/Furnace.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../libraries/Fixed.sol"; import "../interfaces/IFurnace.sol"; import "./mixins/Component.sol"; +import "../mixins/NetworkConfigLib.sol"; /** * @title FurnaceP0 @@ -13,7 +14,8 @@ contract FurnaceP0 is ComponentP0, IFurnace { using FixLib for uint192; uint192 public constant MAX_RATIO = FIX_ONE; // {1} 100% - uint48 public constant PERIOD = ONE_BLOCK; // {s} 12 seconds; 1 block on PoS Ethereum + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable PERIOD; // {seconds} 1 block based on network uint192 public ratio; // {1} What fraction of balance to melt each PERIOD @@ -21,6 +23,10 @@ contract FurnaceP0 is ComponentP0, IFurnace { uint48 public lastPayout; // {seconds} The last time we did a payout uint256 public lastPayoutBal; // {qRTok} The balance of RToken at the last payout + constructor() { + PERIOD = NetworkConfigLib.blocktime(); + } + function init(IMain main_, uint192 ratio_) public initializer { __Component_init(main_); setRatio(ratio_); diff --git a/contracts/p0/Main.sol b/contracts/p0/Main.sol index 3a3374c0c..3681f2da5 100644 --- a/contracts/p0/Main.sol +++ b/contracts/p0/Main.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/p0/RToken.sol b/contracts/p0/RToken.sol index 390d99e61..2dc33334a 100644 --- a/contracts/p0/RToken.sol +++ b/contracts/p0/RToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; // solhint-disable-next-line max-line-length import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/p0/RevenueTrader.sol b/contracts/p0/RevenueTrader.sol index c46cf4482..d9a1ed6e4 100644 --- a/contracts/p0/RevenueTrader.sol +++ b/contracts/p0/RevenueTrader.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/p0/StRSR.sol b/contracts/p0/StRSR.sol index da9870a0a..f0e2f1e73 100644 --- a/contracts/p0/StRSR.sol +++ b/contracts/p0/StRSR.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/interfaces/IERC1271Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; @@ -16,6 +16,7 @@ import "../interfaces/IMain.sol"; import "../libraries/Fixed.sol"; import "../libraries/Permit.sol"; import "./mixins/Component.sol"; +import "../mixins/NetworkConfigLib.sol"; /* * @title StRSRP0 @@ -31,8 +32,10 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { using EnumerableSet for EnumerableSet.AddressSet; using FixLib for uint192; - uint48 public constant PERIOD = ONE_BLOCK; // {s} 12 seconds; 1 block on PoS Ethereum - uint48 public constant MIN_UNSTAKING_DELAY = PERIOD * 2; // {s} + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable PERIOD; // {s} 1 block based on network + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable MIN_UNSTAKING_DELAY; // {s} based on network uint48 public constant MAX_UNSTAKING_DELAY = 31536000; // {s} 1 year uint192 public constant MAX_REWARD_RATIO = 1e18; uint192 public constant MAX_WITHDRAWAL_LEAK = 3e17; // {1} 30% @@ -104,6 +107,11 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { uint192 public rewardRatio; uint192 public withdrawalLeak; // {1} gov param -- % RSR that can be withdrawn without refresh + constructor() { + PERIOD = NetworkConfigLib.blocktime(); + MIN_UNSTAKING_DELAY = PERIOD * 2; + } + function init( IMain main_, string memory name_, @@ -239,6 +247,7 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { // require(bh.isReady(), "basket not ready"); Withdrawal[] storage queue = withdrawals[account]; + if (endId == 0) return; require(endId <= queue.length, "index out-of-bounds"); // require(queue[endId - 1].availableAt <= block.timestamp, "withdrawal unavailable"); @@ -248,6 +257,9 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { while (start < endId && queue[start].rsrAmount == 0 && queue[start].stakeAmount == 0) start++; + // Return if nothing to process + if (start == endId) return; + // Accumulate and zero executable withdrawals uint256 total = 0; uint256 i = start; @@ -418,8 +430,8 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { address to, uint256 amount ) private { - require(from != address(0), "ERC20: transfer from the zero address"); - require(to != address(0), "ERC20: transfer to the zero address"); + require(from != address(0), "ERC20: transfer to or from the zero address"); + require(to != address(0), "ERC20: transfer to or from the zero address"); require(to != address(this), "StRSR transfer to self"); uint256 fromBalance = balances[from]; @@ -475,8 +487,8 @@ contract StRSRP0 is IStRSR, ComponentP0, EIP712Upgradeable { address spender, uint256 amount ) private { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); + require(owner != address(0), "ERC20: approve to or from the zero address"); + require(spender != address(0), "ERC20: approve to or from the zero address"); allowances[owner][spender] = amount; diff --git a/contracts/p0/mixins/Component.sol b/contracts/p0/mixins/Component.sol index cc484b509..09f5846b6 100644 --- a/contracts/p0/mixins/Component.sol +++ b/contracts/p0/mixins/Component.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; diff --git a/contracts/p0/mixins/Rewardable.sol b/contracts/p0/mixins/Rewardable.sol index 8d22f54d5..a2d0bcdbe 100644 --- a/contracts/p0/mixins/Rewardable.sol +++ b/contracts/p0/mixins/Rewardable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/Address.sol"; import "./Component.sol"; diff --git a/contracts/p0/mixins/Trading.sol b/contracts/p0/mixins/Trading.sol index 1d2e9f22e..871092986 100644 --- a/contracts/p0/mixins/Trading.sol +++ b/contracts/p0/mixins/Trading.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -26,6 +26,9 @@ abstract contract TradingP0 is RewardableP0, ITrading { uint192 public minTradeVolume; // {UoA} + // === 3.0.0 === + uint256 public tradesNonce; // to keep track of how many trades have been opened in total + // untestable: // `else` branch of `onlyInitializing` (ie. revert) is currently untestable. // This function is only called inside other `init` functions, each of which is wrapped @@ -68,6 +71,8 @@ abstract contract TradingP0 is RewardableP0, ITrading { trade = broker.openTrade(kind, req); trades[req.sell.erc20()] = trade; tradesOpen++; + tradesNonce++; + emit TradeStarted( trade, req.sell.erc20(), @@ -92,16 +97,4 @@ abstract contract TradingP0 is RewardableP0, ITrading { emit MinTradeVolumeSet(minTradeVolume, val); minTradeVolume = val; } - - // === FixLib Helper === - - /// Light wrapper around FixLib.mulDiv to support try-catch - function mulDiv( - uint192 x, - uint192 y, - uint192 z, - RoundingMode rounding - ) external pure returns (uint192) { - return x.mulDiv(y, z, rounding); - } } diff --git a/contracts/p0/mixins/TradingLib.sol b/contracts/p0/mixins/TradingLib.sol index 3de3f0f0d..271c6610f 100644 --- a/contracts/p0/mixins/TradingLib.sol +++ b/contracts/p0/mixins/TradingLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../interfaces/IAsset.sol"; @@ -58,11 +58,10 @@ library TradingLibP0 { // Calculate equivalent buyAmount within [0, FIX_MAX] // {buyTok} = {sellTok} * {1} * {UoA/sellTok} / {UoA/buyTok} - uint192 b = safeMulDivCeil( - ITrading(address(this)), - s.mul(FIX_ONE.minus(maxTradeSlippage)), - trade.sellPrice, // {UoA/sellTok} - trade.buyPrice // {UoA/buyTok} + uint192 b = s.mul(FIX_ONE.minus(maxTradeSlippage)).safeMulDiv( + trade.sellPrice, + trade.buyPrice, + CEIL ); // {*tok} => {q*Tok} @@ -317,11 +316,8 @@ library TradingLibP0 { } else { // surplus: add-in optimistic estimate of baskets purchaseable - // {BU} = {UoA/tok} * {tok} / {UoA/BU} - deltaTop += int256( - uint256(ctx.bm.safeMulDivCeil(high, bal - anchor, buPriceLow)) - ); // needs overflow protection: using high price of asset which can be FIX_MAX + deltaTop += int256(uint256(high.safeMulDiv(bal - anchor, buPriceLow, CEIL))); } } @@ -544,25 +540,6 @@ library TradingLibP0 { amt.shiftl_toUint(int8(asset.erc20Decimals())) > 1; } - /// @return The result of FixLib.mulDiv bounded from above by FIX_MAX in the case of overflow - function safeMulDivCeil( - ITrading trader, - uint192 x, - uint192 y, - uint192 z - ) internal pure returns (uint192) { - try trader.mulDiv(x, y, z, CEIL) returns (uint192 result) { - return result; - } catch Panic(uint256 errorCode) { - // 0x11: overflow - // 0x12: div-by-zero - assert(errorCode == 0x11 || errorCode == 0x12); - } catch (bytes memory reason) { - assert(keccak256(reason) == UIntOutofBoundsHash); - } - return FIX_MAX; - } - // === Private === /// Calculates the minTradeSize for an asset based on the given minTradeVolume and price diff --git a/contracts/p1/AssetRegistry.sol b/contracts/p1/AssetRegistry.sol index 481673eff..6ae765525 100644 --- a/contracts/p1/AssetRegistry.sol +++ b/contracts/p1/AssetRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; diff --git a/contracts/p1/BackingManager.sol b/contracts/p1/BackingManager.sol index 933d43e1f..bc0df7db2 100644 --- a/contracts/p1/BackingManager.sol +++ b/contracts/p1/BackingManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -10,6 +10,7 @@ import "../libraries/Array.sol"; import "../libraries/Fixed.sol"; import "./mixins/Trading.sol"; import "./mixins/RecollateralizationLib.sol"; +import "../mixins/NetworkConfigLib.sol"; /** * @title BackingManager @@ -21,6 +22,10 @@ contract BackingManagerP1 is TradingP1, IBackingManager { using FixLib for uint192; using SafeERC20 for IERC20; + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable ONE_BLOCK; // {s} 1 block based on network + // Cache of peer components IAssetRegistry private assetRegistry; IBasketHandler private basketHandler; @@ -43,6 +48,11 @@ contract BackingManagerP1 is TradingP1, IBackingManager { // ==== Invariants ==== // tradingDelay <= MAX_TRADING_DELAY and backingBuffer <= MAX_BACKING_BUFFER + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + ONE_BLOCK = NetworkConfigLib.blocktime(); + } + function init( IMain main_, uint48 tradingDelay_, @@ -107,7 +117,6 @@ contract BackingManagerP1 is TradingP1, IBackingManager { furnace.melt(); // DoS prevention: unless caller is self, require 1 empty block between like-kind auctions - // Assumption: chain has <= 12s blocktimes require( _msgSender() == address(this) || tradeEnd[kind] + ONE_BLOCK < block.timestamp, "already rebalancing" diff --git a/contracts/p1/BasketHandler.sol b/contracts/p1/BasketHandler.sol index 3f54f65c8..568d3d765 100644 --- a/contracts/p1/BasketHandler.sol +++ b/contracts/p1/BasketHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -17,6 +17,8 @@ import "./mixins/Component.sol"; * @title BasketHandler * @notice Handles the basket configuration, definition, and evolution over time. */ + +/// @custom:oz-upgrades-unsafe-allow external-library-linking contract BasketHandlerP1 is ComponentP1, IBasketHandler { using BasketLibP1 for Basket; using EnumerableMap for EnumerableMap.Bytes32ToUintMap; @@ -337,7 +339,14 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { : assetRegistry.toAsset(basket.erc20s[i]).price(); low256 += qty.safeMul(lowP, RoundingMode.FLOOR); - high256 += qty.safeMul(highP, RoundingMode.CEIL); + + if (high256 < FIX_MAX) { + if (highP == FIX_MAX) { + high256 = FIX_MAX; + } else { + high256 += qty.safeMul(highP, RoundingMode.CEIL); + } + } } // safe downcast: FIX_MAX is type(uint192).max @@ -401,6 +410,8 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { // Add-in refAmts contribution from historical basket for (uint256 j = 0; j < b.erc20s.length; ++j) { IERC20 erc20 = b.erc20s[j]; + // untestable: + // previous baskets erc20s do not contain the zero address if (address(erc20) == address(0)) continue; // Ugly search through erc20sAll @@ -438,11 +449,10 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { if (!asset.isCollateral()) continue; // skip token if no longer registered // {tok} = {BU} * {ref/BU} / {ref/tok} - quantities[i] = safeMulDivFloor( - amount, - refAmtsAll[i], - ICollateral(address(asset)).refPerTok() - ).shiftl_toUint(int8(asset.erc20Decimals()), FLOOR); + quantities[i] = amount + .safeMulDiv(refAmtsAll[i], ICollateral(address(asset)).refPerTok(), FLOOR) + .shiftl_toUint(int8(asset.erc20Decimals()), FLOOR); + // marginally more penalizing than its sibling calculation that uses _quantity() // because does not intermediately CEIL as part of the division } catch (bytes memory errData) { @@ -531,12 +541,6 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { IERC20[] calldata newERC20s, uint192[] calldata newTargetAmts ) private { - // Empty _targetAmts mapping - while (_targetAmts.length() > 0) { - (bytes32 key, ) = _targetAmts.at(0); - _targetAmts.remove(key); - } - // Populate _targetAmts mapping with old basket config uint256 len = config.erc20s.length; for (uint256 i = 0; i < len; ++i) { @@ -598,11 +602,10 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { if (!asset.isCollateral()) continue; // skip token if no longer registered // {tok} = {BU} * {ref/BU} / {ref/tok} - quantities[i] = safeMulDivFloor( - FIX_ONE, - b.refAmts[erc20s[i]], - ICollateral(address(asset)).refPerTok() - ).shiftl_toUint(int8(asset.erc20Decimals()), FLOOR); + quantities[i] = b + .refAmts[erc20s[i]] + .safeDiv(ICollateral(address(asset)).refPerTok(), FLOOR) + .shiftl_toUint(int8(asset.erc20Decimals()), FLOOR); } catch (bytes memory errData) { // untested: // OOG pattern tested in other contracts, cost to test here is high @@ -654,26 +657,6 @@ contract BasketHandlerP1 is ComponentP1, IBasketHandler { max = backup.max; } - // === Private === - - /// @return The floored result of FixLib.mulDiv - function safeMulDivFloor( - uint192 x, - uint192 y, - uint192 z - ) private view returns (uint192) { - try backingManager.mulDiv(x, y, z, FLOOR) returns (uint192 result) { - return result; - } catch Panic(uint256 errorCode) { - // 0x11: overflow - // 0x12: div-by-zero - assert(errorCode == 0x11 || errorCode == 0x12); - } catch (bytes memory reason) { - assert(keccak256(reason) == UIntOutofBoundsHash); - } - return FIX_MAX; - } - // ==== Storage Gap ==== /** diff --git a/contracts/p1/Broker.sol b/contracts/p1/Broker.sol index 242d28358..666c604b4 100644 --- a/contracts/p1/Broker.sol +++ b/contracts/p1/Broker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -9,6 +9,7 @@ import "../interfaces/IMain.sol"; import "../interfaces/ITrade.sol"; import "../libraries/Fixed.sol"; import "./mixins/Component.sol"; +import "../mixins/NetworkConfigLib.sol"; import "../plugins/trading/DutchTrade.sol"; import "../plugins/trading/GnosisTrade.sol"; @@ -23,8 +24,9 @@ contract BrokerP1 is ComponentP1, IBroker { using Clones for address; uint48 public constant MAX_AUCTION_LENGTH = 604800; // {s} max valid duration - 1 week - uint48 public constant MIN_AUCTION_LENGTH = ONE_BLOCK * 2; // {s} min auction length - 2 blocks - // warning: blocktime <= 12s assumption + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable MIN_AUCTION_LENGTH; // {s} 2 blocks based on network IBackingManager private backingManager; IRevenueTrader private rsrTrader; @@ -59,6 +61,11 @@ contract BrokerP1 is ComponentP1, IBroker { // ==== Invariant ==== // (trades[addr] == true) iff this contract has created an ITrade clone at addr + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + MIN_AUCTION_LENGTH = NetworkConfigLib.blocktime() * 2; + } + // effects: initial parameters are set function init( IMain main_, diff --git a/contracts/p1/Deployer.sol b/contracts/p1/Deployer.sol index 2077fc414..9dd23363a 100644 --- a/contracts/p1/Deployer.sol +++ b/contracts/p1/Deployer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/proxy/Clones.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; diff --git a/contracts/p1/Distributor.sol b/contracts/p1/Distributor.sol index 21b973fce..734c702d2 100644 --- a/contracts/p1/Distributor.sol +++ b/contracts/p1/Distributor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/p1/Furnace.sol b/contracts/p1/Furnace.sol index 1596942cf..a261f217c 100644 --- a/contracts/p1/Furnace.sol +++ b/contracts/p1/Furnace.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../libraries/Fixed.sol"; import "../interfaces/IFurnace.sol"; import "./mixins/Component.sol"; +import "../mixins/NetworkConfigLib.sol"; /** * @title FurnaceP1 @@ -13,7 +14,9 @@ contract FurnaceP1 is ComponentP1, IFurnace { using FixLib for uint192; uint192 public constant MAX_RATIO = FIX_ONE; // {1} 100% - uint48 public constant PERIOD = ONE_BLOCK; // {s} 12 seconds; 1 block on PoS Ethereum + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable PERIOD; // {seconds} 1 block based on network IRToken private rToken; @@ -24,6 +27,11 @@ contract FurnaceP1 is ComponentP1, IFurnace { uint48 public lastPayout; // {seconds} The last time we did a payout uint256 public lastPayoutBal; // {qRTok} The balance of RToken at the last payout + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() ComponentP1() { + PERIOD = NetworkConfigLib.blocktime(); + } + // ==== Invariants ==== // ratio <= MAX_RATIO = 1e18 // lastPayout was the timestamp of the end of the last period we paid out diff --git a/contracts/p1/Main.sol b/contracts/p1/Main.sol index 7bd551151..e4922caa9 100644 --- a/contracts/p1/Main.sol +++ b/contracts/p1/Main.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; diff --git a/contracts/p1/RToken.sol b/contracts/p1/RToken.sol index 3119702d6..75cbcc3b6 100644 --- a/contracts/p1/RToken.sol +++ b/contracts/p1/RToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; // solhint-disable-next-line max-line-length import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; diff --git a/contracts/p1/RevenueTrader.sol b/contracts/p1/RevenueTrader.sol index 849b06ebd..d588b7e5f 100644 --- a/contracts/p1/RevenueTrader.sol +++ b/contracts/p1/RevenueTrader.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/p1/StRSR.sol b/contracts/p1/StRSR.sol index 8301a0bae..99cbe10d4 100644 --- a/contracts/p1/StRSR.sol +++ b/contracts/p1/StRSR.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/interfaces/IERC1271Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -13,6 +13,7 @@ import "../interfaces/IMain.sol"; import "../libraries/Fixed.sol"; import "../libraries/Permit.sol"; import "./mixins/Component.sol"; +import "../mixins/NetworkConfigLib.sol"; /* * @title StRSRP1 @@ -34,8 +35,12 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab using CountersUpgradeable for CountersUpgradeable.Counter; using SafeERC20Upgradeable for IERC20Upgradeable; - uint48 public constant PERIOD = ONE_BLOCK; // {s} 12 seconds; 1 block on PoS Ethereum - uint48 public constant MIN_UNSTAKING_DELAY = PERIOD * 2; // {s} + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable PERIOD; // {s} 1 block based on network + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable MIN_UNSTAKING_DELAY; // {s} based on network uint48 public constant MAX_UNSTAKING_DELAY = 31536000; // {s} 1 year uint192 public constant MAX_REWARD_RATIO = FIX_ONE; // {1} 100% @@ -160,6 +165,12 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // ====================== + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() ComponentP1() { + PERIOD = NetworkConfigLib.blocktime(); + MIN_UNSTAKING_DELAY = PERIOD * 2; + } + // init() can only be called once (initializer) // ==== Financial State: // effects: @@ -355,6 +366,8 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab // Cancelling unstake does not require checking if the unstaking was available // require(queue[endId - 1].availableAt <= block.timestamp, "withdrawal unavailable"); + // untestable: + // firstId will never be zero, due to previous checks against endId uint192 oldDrafts = firstId > 0 ? queue[firstId - 1].drafts : 0; uint192 draftAmount = queue[endId - 1].drafts - oldDrafts; @@ -787,8 +800,10 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab address to, uint256 amount ) internal { - require(from != address(0), "ERC20: transfer from the zero address"); - require(to != address(0), "ERC20: transfer to the zero address"); + require( + from != address(0) && to != address(0), + "ERC20: transfer to or from the zero address" + ); mapping(address => uint256) storage eraStakes = stakes[era]; uint256 fromBalance = eraStakes[from]; @@ -844,8 +859,10 @@ abstract contract StRSRP1 is Initializable, ComponentP1, IStRSR, EIP712Upgradeab address spender, uint256 amount ) internal { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); + require( + owner != address(0) && spender != address(0), + "ERC20: approve to or from the zero address" + ); _allowances[era][owner][spender] = amount; emit Approval(owner, spender, amount); diff --git a/contracts/p1/StRSRVotes.sol b/contracts/p1/StRSRVotes.sol index 4ba528bd7..2251ac1ff 100644 --- a/contracts/p1/StRSRVotes.sol +++ b/contracts/p1/StRSRVotes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; diff --git a/contracts/p1/mixins/BasketLib.sol b/contracts/p1/mixins/BasketLib.sol index bc52d1c6a..0ade4b65e 100644 --- a/contracts/p1/mixins/BasketLib.sol +++ b/contracts/p1/mixins/BasketLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; diff --git a/contracts/p1/mixins/Component.sol b/contracts/p1/mixins/Component.sol index ccdf81700..913379cb8 100644 --- a/contracts/p1/mixins/Component.sol +++ b/contracts/p1/mixins/Component.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; diff --git a/contracts/p1/mixins/RecollateralizationLib.sol b/contracts/p1/mixins/RecollateralizationLib.sol index 73281db5c..a4162df47 100644 --- a/contracts/p1/mixins/RecollateralizationLib.sol +++ b/contracts/p1/mixins/RecollateralizationLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../interfaces/IAsset.sol"; @@ -212,11 +212,8 @@ library RecollateralizationLibP1 { } else { // surplus: add-in optimistic estimate of baskets purchaseable - // {BU} = {UoA/tok} * {tok} / {UoA/BU} - deltaTop += int256( - uint256(ctx.bm.safeMulDivCeil(high, bal - anchor, buPriceLow)) - ); - // needs overflow protection: using high price of asset which can be FIX_MAX + // {BU} = {UoA/tok} * {tok} / {UoA/BU} + deltaTop += int256(uint256(high.safeMulDiv(bal - anchor, buPriceLow, CEIL))); } } diff --git a/contracts/p1/mixins/RewardableLib.sol b/contracts/p1/mixins/RewardableLib.sol index 6490adf0f..58b34987b 100644 --- a/contracts/p1/mixins/RewardableLib.sol +++ b/contracts/p1/mixins/RewardableLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/p1/mixins/TradeLib.sol b/contracts/p1/mixins/TradeLib.sol index 0751f1df4..22f612ef8 100644 --- a/contracts/p1/mixins/TradeLib.sol +++ b/contracts/p1/mixins/TradeLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../interfaces/IAsset.sol"; @@ -61,11 +61,10 @@ library TradeLib { // Calculate equivalent buyAmount within [0, FIX_MAX] // {buyTok} = {sellTok} * {1} * {UoA/sellTok} / {UoA/buyTok} - uint192 b = safeMulDivCeil( - ITrading(address(this)), - s.mul(FIX_ONE.minus(maxTradeSlippage)), - trade.sellPrice, // {UoA/sellTok} - trade.buyPrice // {UoA/buyTok} + uint192 b = s.mul(FIX_ONE.minus(maxTradeSlippage)).safeMulDiv( + trade.sellPrice, + trade.buyPrice, + CEIL ); // {*tok} => {q*Tok} @@ -149,28 +148,6 @@ library TradeLib { amt.shiftl_toUint(int8(asset.erc20Decimals())) > 1; } - /// @return The result of FixLib.mulDiv bounded from above by FIX_MAX in the case of overflow - function safeMulDivCeil( - ITrading trader, - uint192 x, - uint192 y, - uint192 z - ) internal pure returns (uint192) { - try trader.mulDiv(x, y, z, CEIL) returns (uint192 result) { - return result; - } catch Panic(uint256 errorCode) { - // 0x11: overflow - // 0x12: div-by-zero - // untestable: - // Overflow is protected against and checked for in FixLib.mulDiv() - // Div-by-zero is NOT protected against, but no caller will ever use 0 for z - assert(errorCode == 0x11 || errorCode == 0x12); - } catch (bytes memory reason) { - assert(keccak256(reason) == UIntOutofBoundsHash); - } - return FIX_MAX; - } - // === Private === /// Calculates the minTradeSize for an asset based on the given minTradeVolume and price diff --git a/contracts/p1/mixins/Trading.sol b/contracts/p1/mixins/Trading.sol index 258c7cd34..9a84b1d72 100644 --- a/contracts/p1/mixins/Trading.sol +++ b/contracts/p1/mixins/Trading.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -32,9 +32,11 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl // === Governance param === uint192 public maxTradeSlippage; // {%} - uint192 public minTradeVolume; // {UoA} + // === 3.0.0 === + uint256 public tradesNonce; // to keep track of how many trades have been opened in total + // ==== Invariants ==== // tradesOpen = len(values(trades)) // trades[sell] != 0 iff trade[sell] has been opened and not yet settled @@ -111,7 +113,6 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl // trades' = trades.set(req.sell, tradeID) // tradesOpen' = tradesOpen + 1 function tryTrade(TradeKind kind, TradeRequest memory req) internal returns (ITrade trade) { - /* */ IERC20 sell = req.sell.erc20(); assert(address(trades[sell]) == address(0)); @@ -121,6 +122,7 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl trade = broker.openTrade(kind, req); trades[sell] = trade; tradesOpen++; + tradesNonce++; emit TradeStarted(trade, sell, req.buy.erc20(), req.sellAmount, req.minBuyAmount); } @@ -141,22 +143,10 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl minTradeVolume = val; } - // === FixLib Helper === - - /// Light wrapper around FixLib.mulDiv to support try-catch - function mulDiv( - uint192 x, - uint192 y, - uint192 z, - RoundingMode round - ) external pure returns (uint192) { - return x.mulDiv(y, z, round); - } - /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[46] private __gap; + uint256[45] private __gap; } diff --git a/contracts/plugins/assets/AppreciatingFiatCollateral.sol b/contracts/plugins/assets/AppreciatingFiatCollateral.sol index a8b620c03..3722b5644 100644 --- a/contracts/plugins/assets/AppreciatingFiatCollateral.sol +++ b/contracts/plugins/assets/AppreciatingFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../interfaces/IAsset.sol"; diff --git a/contracts/plugins/assets/Asset.sol b/contracts/plugins/assets/Asset.sol index 64d2073d7..b35c7941e 100644 --- a/contracts/plugins/assets/Asset.sol +++ b/contracts/plugins/assets/Asset.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/EURFiatCollateral.sol b/contracts/plugins/assets/EURFiatCollateral.sol index a37bdf01b..ae6dcfc3c 100644 --- a/contracts/plugins/assets/EURFiatCollateral.sol +++ b/contracts/plugins/assets/EURFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/FiatCollateral.sol b/contracts/plugins/assets/FiatCollateral.sol index 3aa03bdcb..d4d141281 100644 --- a/contracts/plugins/assets/FiatCollateral.sol +++ b/contracts/plugins/assets/FiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../interfaces/IAsset.sol"; diff --git a/contracts/plugins/assets/NonFiatCollateral.sol b/contracts/plugins/assets/NonFiatCollateral.sol index 7cf9f7bf4..2e6b3c531 100644 --- a/contracts/plugins/assets/NonFiatCollateral.sol +++ b/contracts/plugins/assets/NonFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/OracleLib.sol b/contracts/plugins/assets/OracleLib.sol index b6bbea745..e15605f88 100644 --- a/contracts/plugins/assets/OracleLib.sol +++ b/contracts/plugins/assets/OracleLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/RTokenAsset.sol b/contracts/plugins/assets/RTokenAsset.sol index 87645cc6d..fd8c78fa2 100644 --- a/contracts/plugins/assets/RTokenAsset.sol +++ b/contracts/plugins/assets/RTokenAsset.sol @@ -1,19 +1,24 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../p1/mixins/RecollateralizationLib.sol"; import "../../interfaces/IMain.sol"; import "../../interfaces/IRToken.sol"; +import "../../interfaces/IRTokenOracle.sol"; import "./Asset.sol"; import "./VersionedAsset.sol"; +uint256 constant ORACLE_TIMEOUT = 15 minutes; + /// Once an RToken gets large enough to get a price feed, replacing this asset with /// a simpler one will do wonders for gas usage -contract RTokenAsset is IAsset, VersionedAsset { +// @dev This RTokenAsset is ONLY compatible with Protocol ^3.0.0 +contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { using FixLib for uint192; using OracleLib for AggregatorV3Interface; // Component addresses are not mutable in protocol, so it's safe to cache these + IMain public immutable main; IBasketHandler public immutable basketHandler; IAssetRegistry public immutable assetRegistry; IBackingManager public immutable backingManager; @@ -24,12 +29,15 @@ contract RTokenAsset is IAsset, VersionedAsset { uint192 public immutable maxTradeVolume; // {UoA} + // Oracle State + CachedOracleData public cachedOracleData; + /// @param maxTradeVolume_ {UoA} The max trade volume, in UoA constructor(IRToken erc20_, uint192 maxTradeVolume_) { require(address(erc20_) != address(0), "missing erc20"); require(maxTradeVolume_ > 0, "invalid max trade volume"); - IMain main = erc20_.main(); + main = erc20_.main(); basketHandler = main.basketHandler(); assetRegistry = main.assetRegistry(); backingManager = main.backingManager(); @@ -59,12 +67,15 @@ contract RTokenAsset is IAsset, VersionedAsset { // {UoA/tok} = {BU} * {UoA/BU} / {tok} low = range.bottom.mulDiv(lowBUPrice, supply, FLOOR); high = range.top.mulDiv(highBUPrice, supply, CEIL); + assert(low <= high); // not obviously true } // solhint-disable no-empty-blocks function refresh() public virtual override { // No need to save lastPrice; can piggyback off the backing collateral's lotPrice() + + cachedOracleData.cachedAtTime = 0; // force oracle refresh } // solhint-enable no-empty-blocks @@ -128,8 +139,41 @@ contract RTokenAsset is IAsset, VersionedAsset { // solhint-enable no-empty-blocks + function forceUpdatePrice() external { + _updateCachedPrice(); + } + + function latestPrice() external returns (uint192 rTokenPrice, uint256 updatedAt) { + // Situations that require an update, from most common to least common. + if ( + cachedOracleData.cachedAtTime + ORACLE_TIMEOUT <= block.timestamp || // Cache Timeout + cachedOracleData.cachedAtNonce != basketHandler.nonce() || // Basket nonce was updated + cachedOracleData.cachedTradesNonce != backingManager.tradesNonce() || // New trades + cachedOracleData.cachedTradesOpen != backingManager.tradesOpen() // ..or settled + ) { + _updateCachedPrice(); + } + + return (cachedOracleData.cachedPrice, cachedOracleData.cachedAtTime); + } + // ==== Private ==== + // Update Oracle Data + function _updateCachedPrice() internal { + (uint192 low, uint192 high) = price(); + + require(low != 0 && high != FIX_MAX, "invalid price"); + + cachedOracleData = CachedOracleData( + (low + high) / 2, + block.timestamp, + basketHandler.nonce(), + backingManager.tradesOpen(), + backingManager.tradesNonce() + ); + } + /// Computationally expensive basketRange calculation; used in price() & lotPrice() function basketRange() private view returns (BasketRange memory range) { BasketRange memory basketsHeld = basketHandler.basketsHeldBy(address(backingManager)); @@ -145,7 +189,6 @@ contract RTokenAsset is IAsset, VersionedAsset { // the absence of an external price feed. Any RToken that gets reasonably big // should switch over to an asset with a price feed. - IMain main = backingManager.main(); TradingContext memory ctx; ctx.basketsHeld = basketsHeld; diff --git a/contracts/plugins/assets/SelfReferentialCollateral.sol b/contracts/plugins/assets/SelfReferentialCollateral.sol index eaed125e7..fb3eb2b92 100644 --- a/contracts/plugins/assets/SelfReferentialCollateral.sol +++ b/contracts/plugins/assets/SelfReferentialCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/VersionedAsset.sol b/contracts/plugins/assets/VersionedAsset.sol index 1b92bac32..f3fc5e30a 100644 --- a/contracts/plugins/assets/VersionedAsset.sol +++ b/contracts/plugins/assets/VersionedAsset.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../interfaces/IVersioned.sol"; diff --git a/contracts/plugins/assets/aave/ATokenFiatCollateral.sol b/contracts/plugins/assets/aave/ATokenFiatCollateral.sol index e9ed7267f..f6e98c267 100644 --- a/contracts/plugins/assets/aave/ATokenFiatCollateral.sol +++ b/contracts/plugins/assets/aave/ATokenFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/aave/vendor/IStaticATokenLM.sol b/contracts/plugins/assets/aave/IStaticATokenLM.sol similarity index 99% rename from contracts/plugins/assets/aave/vendor/IStaticATokenLM.sol rename to contracts/plugins/assets/aave/IStaticATokenLM.sol index 534771782..c559bcf54 100644 --- a/contracts/plugins/assets/aave/vendor/IStaticATokenLM.sol +++ b/contracts/plugins/assets/aave/IStaticATokenLM.sol @@ -4,7 +4,7 @@ pragma experimental ABIEncoderV2; import { IERC20 } from "@aave/protocol-v2/contracts/dependencies/openzeppelin/contracts/IERC20.sol"; import { ILendingPool } from "@aave/protocol-v2/contracts/interfaces/ILendingPool.sol"; -import { IAaveIncentivesController } from "./IAaveIncentivesController.sol"; +import { IAaveIncentivesController } from "./vendor/IAaveIncentivesController.sol"; interface IStaticATokenLM is IERC20 { struct SignatureParams { diff --git a/contracts/plugins/assets/aave/vendor/StaticATokenErrors.sol b/contracts/plugins/assets/aave/StaticATokenErrors.sol similarity index 100% rename from contracts/plugins/assets/aave/vendor/StaticATokenErrors.sol rename to contracts/plugins/assets/aave/StaticATokenErrors.sol diff --git a/contracts/plugins/assets/aave/vendor/StaticATokenLM.sol b/contracts/plugins/assets/aave/StaticATokenLM.sol similarity index 93% rename from contracts/plugins/assets/aave/vendor/StaticATokenLM.sol rename to contracts/plugins/assets/aave/StaticATokenLM.sol index d22344d07..b24a3c14b 100644 --- a/contracts/plugins/assets/aave/vendor/StaticATokenLM.sol +++ b/contracts/plugins/assets/aave/StaticATokenLM.sol @@ -6,18 +6,18 @@ import { ILendingPool } from "@aave/protocol-v2/contracts/interfaces/ILendingPoo import { IERC20 } from "@aave/protocol-v2/contracts/dependencies/openzeppelin/contracts/IERC20.sol"; import { IERC20Detailed } from "@aave/protocol-v2/contracts/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; -import { IAToken } from "./IAToken.sol"; +import { IAToken } from "./vendor/IAToken.sol"; import { IStaticATokenLM } from "./IStaticATokenLM.sol"; -import { IAaveIncentivesController } from "./IAaveIncentivesController.sol"; +import { IAaveIncentivesController } from "./vendor/IAaveIncentivesController.sol"; import { StaticATokenErrors } from "./StaticATokenErrors.sol"; -import { ERC20 } from "./ERC20.sol"; -import { ReentrancyGuard } from "./ReentrancyGuard.sol"; +import { ERC20 } from "./vendor/ERC20.sol"; +import { ReentrancyGuard } from "./vendor/ReentrancyGuard.sol"; import { SafeERC20 } from "@aave/protocol-v2/contracts/dependencies/openzeppelin/contracts/SafeERC20.sol"; import { WadRayMath } from "@aave/protocol-v2/contracts/protocol/libraries/math/WadRayMath.sol"; -import { RayMathNoRounding } from "./RayMathNoRounding.sol"; +import { RayMathNoRounding } from "./vendor/RayMathNoRounding.sol"; import { SafeMath } from "@aave/protocol-v2/contracts/dependencies/openzeppelin/contracts/SafeMath.sol"; /** @@ -106,6 +106,8 @@ contract StaticATokenLM is } ///@inheritdoc IStaticATokenLM + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function deposit( address recipient, uint256 amount, @@ -116,6 +118,8 @@ contract StaticATokenLM is } ///@inheritdoc IStaticATokenLM + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function withdraw( address recipient, uint256 amount, @@ -125,6 +129,8 @@ contract StaticATokenLM is } ///@inheritdoc IStaticATokenLM + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function withdrawDynamicAmount( address recipient, uint256 amount, @@ -162,6 +168,8 @@ contract StaticATokenLM is } ///@inheritdoc IStaticATokenLM + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function metaDeposit( address depositor, address recipient, @@ -202,6 +210,8 @@ contract StaticATokenLM is } ///@inheritdoc IStaticATokenLM + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function metaWithdraw( address owner, address recipient, @@ -436,6 +446,8 @@ contract StaticATokenLM is } ///@inheritdoc IStaticATokenLM + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function collectAndUpdateRewards() external override nonReentrant { _collectAndUpdateRewards(); } @@ -469,6 +481,8 @@ contract StaticATokenLM is } } + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function claimRewardsOnBehalf( address onBehalfOf, address receiver, @@ -485,6 +499,8 @@ contract StaticATokenLM is _claimRewardsOnBehalf(onBehalfOf, receiver, forceUpdate); } + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function claimRewards(address receiver, bool forceUpdate) external override nonReentrant { if (address(INCENTIVES_CONTROLLER) == address(0)) { return; @@ -492,6 +508,8 @@ contract StaticATokenLM is _claimRewardsOnBehalf(msg.sender, receiver, forceUpdate); } + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function claimRewardsToSelf(bool forceUpdate) external override nonReentrant { if (address(INCENTIVES_CONTROLLER) == address(0)) { return; @@ -499,6 +517,8 @@ contract StaticATokenLM is _claimRewardsOnBehalf(msg.sender, msg.sender, forceUpdate); } + // untested: + // nonReentrant line is assumed to be working. cost/benefit of direct testing is high function claimRewards() external virtual nonReentrant { if (address(INCENTIVES_CONTROLLER) == address(0)) { return; diff --git a/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol b/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol index 01cdb42dd..251686c30 100644 --- a/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol +++ b/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/math/Math.sol"; import "../../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/ankr/vendor/IAnkrETH.sol b/contracts/plugins/assets/ankr/vendor/IAnkrETH.sol index 800ea59a1..660c8ae58 100644 --- a/contracts/plugins/assets/ankr/vendor/IAnkrETH.sol +++ b/contracts/plugins/assets/ankr/vendor/IAnkrETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/cbeth/CBETHCollateral.sol b/contracts/plugins/assets/cbeth/CBETHCollateral.sol new file mode 100644 index 000000000..40ee822e3 --- /dev/null +++ b/contracts/plugins/assets/cbeth/CBETHCollateral.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { _safeWrap } from "../../../libraries/Fixed.sol"; +import "../AppreciatingFiatCollateral.sol"; + +interface CBEth is IERC20Metadata { + function mint(address account, uint256 amount) external returns (bool); + + function updateExchangeRate(uint256 exchangeRate) external; + + function configureMinter(address minter, uint256 minterAllowedAmount) external returns (bool); + + function exchangeRate() external view returns (uint256 _exchangeRate); +} + +contract CBEthCollateral is AppreciatingFiatCollateral { + using OracleLib for AggregatorV3Interface; + using FixLib for uint192; + + CBEth public immutable token; + AggregatorV3Interface public immutable refPerTokChainlinkFeed; + uint48 public immutable refPerTokChainlinkTimeout; + + /// @param config.chainlinkFeed {UoA/ref} price of DAI in USD terms + constructor( + CollateralConfig memory config, + uint192 revenueHiding, + AggregatorV3Interface _refPerTokChainlinkFeed, + uint48 _refPerTokChainlinkTimeout + ) AppreciatingFiatCollateral(config, revenueHiding) { + token = CBEth(address(config.erc20)); + + refPerTokChainlinkFeed = _refPerTokChainlinkFeed; + refPerTokChainlinkTimeout = _refPerTokChainlinkTimeout; + } + + /// 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} The actual price observed in the peg + function tryPrice() + external + view + override + returns ( + uint192 low, + uint192 high, + uint192 pegPrice + ) + { + // {UoA/tok} = {UoA/ref} * {ref/tok} + uint192 p = chainlinkFeed.price(oracleTimeout).mul( + refPerTokChainlinkFeed.price(refPerTokChainlinkTimeout) + ); + uint192 err = p.mul(oracleError, CEIL); + + high = p + err; + low = p - err; + // assert(low <= high); obviously true just by inspection + + pegPrice = targetPerRef(); // {target/ref} ETH/ETH is always 1 + } + + /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens + function _underlyingRefPerTok() internal view override returns (uint192) { + return _safeWrap(token.exchangeRate()); + } +} diff --git a/contracts/plugins/assets/cbeth/README.md b/contracts/plugins/assets/cbeth/README.md new file mode 100644 index 000000000..fe74735ca --- /dev/null +++ b/contracts/plugins/assets/cbeth/README.md @@ -0,0 +1,19 @@ +# CBETH Collateral Plugin + +## Summary + +This plugin allows `CBETH` holders to use their tokens as collateral in the Reserve Protocol. + +## Implementation + +### Units + +| tok | ref | target | UoA | +| ----- | --- | ------ | --- | +| cbeth | ETH | ETH | ETH | + +### Functions + +#### refPerTok {ref/tok} + +`return _safeWrap(token.exchange_rate());` diff --git a/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol index 2ce5b6a23..9688f437d 100644 --- a/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../../libraries/Fixed.sol"; import "../AppreciatingFiatCollateral.sol"; import "../../../interfaces/IRewardable.sol"; +import "../erc20/RewardableERC20Wrapper.sol"; import "./ICToken.sol"; -import "../../../vendor/oz/IERC4626.sol"; /** * @title CTokenFiatCollateral @@ -22,14 +22,16 @@ contract CTokenFiatCollateral is AppreciatingFiatCollateral { uint8 public immutable referenceERC20Decimals; - ICToken public immutable cToken; + ICToken public immutable cToken; // gas-optimization: access underlying cToken directly + /// @param config.erc20 Should be a CTokenWrapper /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide constructor(CollateralConfig memory config, uint192 revenueHiding) AppreciatingFiatCollateral(config, revenueHiding) { - cToken = ICToken(address(IERC4626(address(config.erc20)).asset())); + cToken = ICToken(address(RewardableERC20Wrapper(address(config.erc20)).underlying())); referenceERC20Decimals = IERC20Metadata(cToken.underlying()).decimals(); + require(referenceERC20Decimals > 0, "referenceERC20Decimals missing"); } /// Refresh exchange rates and update default status. diff --git a/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol index 54b381355..f0a44584b 100644 --- a/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../libraries/Fixed.sol"; import "../OracleLib.sol"; diff --git a/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol b/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol index d3329f4ad..f4b8adf30 100644 --- a/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol +++ b/contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; +import "../erc20/RewardableERC20Wrapper.sol"; import "../AppreciatingFiatCollateral.sol"; import "./ICToken.sol"; @@ -17,6 +18,8 @@ contract CTokenSelfReferentialCollateral is AppreciatingFiatCollateral { uint8 public immutable referenceERC20Decimals; + ICToken public immutable cToken; // gas-optimization: access underlying cToken directly + /// @param config.chainlinkFeed Feed units: {UoA/ref} /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide /// @param referenceERC20Decimals_ The number of decimals in the reference token @@ -27,6 +30,7 @@ contract CTokenSelfReferentialCollateral is AppreciatingFiatCollateral { ) AppreciatingFiatCollateral(config, revenueHiding) { require(config.defaultThreshold == 0, "default threshold not supported"); require(referenceERC20Decimals_ > 0, "referenceERC20Decimals missing"); + cToken = ICToken(address(RewardableERC20Wrapper(address(config.erc20)).underlying())); referenceERC20Decimals = referenceERC20Decimals_; } @@ -59,8 +63,8 @@ contract CTokenSelfReferentialCollateral is AppreciatingFiatCollateral { /// @custom:interaction RCEI function refresh() public virtual override { // == Refresh == - // Update the Compound Protocol - ICToken(address(erc20)).exchangeRateCurrent(); + // Update the Compound Protocol -- access cToken directly + cToken.exchangeRateCurrent(); // Violation of calling super first! Composition broken! Intentional! super.refresh(); // already handles all necessary default checks @@ -68,7 +72,7 @@ contract CTokenSelfReferentialCollateral is AppreciatingFiatCollateral { /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens function _underlyingRefPerTok() internal view override returns (uint192) { - uint256 rate = ICToken(address(erc20)).exchangeRateStored(); + uint256 rate = cToken.exchangeRateStored(); int8 shiftLeft = 8 - int8(referenceERC20Decimals) - 18; return shiftl_toFix(rate, shiftLeft); } diff --git a/contracts/plugins/assets/compoundv2/CTokenVault.sol b/contracts/plugins/assets/compoundv2/CTokenVault.sol deleted file mode 100644 index 8d8c8501a..000000000 --- a/contracts/plugins/assets/compoundv2/CTokenVault.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity ^0.8.17; - -import "../vaults/RewardableERC20Vault.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "./ICToken.sol"; - -contract CTokenVault is RewardableERC20Vault { - using SafeERC20 for ERC20; - - IComptroller public immutable comptroller; - - constructor( - ERC20 _asset, - string memory _name, - string memory _symbol, - IComptroller _comptroller - ) RewardableERC20Vault(_asset, _name, _symbol, ERC20(_comptroller.getCompAddress())) { - comptroller = _comptroller; - } - - function _claimAssetRewards() internal virtual override { - comptroller.claimComp(address(this)); - } - - function exchangeRateCurrent() external returns (uint256) { - return ICToken(asset()).exchangeRateCurrent(); - } - - function exchangeRateStored() external view returns (uint256) { - return ICToken(asset()).exchangeRateStored(); - } -} diff --git a/contracts/plugins/assets/compoundv2/CTokenWrapper.sol b/contracts/plugins/assets/compoundv2/CTokenWrapper.sol new file mode 100644 index 000000000..534de316a --- /dev/null +++ b/contracts/plugins/assets/compoundv2/CTokenWrapper.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../erc20/RewardableERC20Wrapper.sol"; +import "./ICToken.sol"; + +contract CTokenWrapper is RewardableERC20Wrapper { + using SafeERC20 for ERC20; + + IComptroller public immutable comptroller; + + constructor( + ERC20 _underlying, + string memory _name, + string memory _symbol, + IComptroller _comptroller + ) RewardableERC20Wrapper(_underlying, _name, _symbol, ERC20(_comptroller.getCompAddress())) { + comptroller = _comptroller; + } + + /// === Exchange rate pass-throughs === + + // While these are included in the wrapper, it should probably not be used directly + // by the collateral plugin for gas optimization reasons + + function exchangeRateCurrent() external returns (uint256) { + return ICToken(address(underlying)).exchangeRateCurrent(); + } + + function exchangeRateStored() external view returns (uint256) { + return ICToken(address(underlying)).exchangeRateStored(); + } + + // === Overrides === + + function _claimAssetRewards() internal virtual override { + comptroller.claimComp(address(this)); + } + + // No overrides of _deposit()/_withdraw() necessary: no staking required +} diff --git a/contracts/plugins/assets/compoundv2/ICToken.sol b/contracts/plugins/assets/compoundv2/ICToken.sol index a68a8c6ec..384ba8498 100644 --- a/contracts/plugins/assets/compoundv2/ICToken.sol +++ b/contracts/plugins/assets/compoundv2/ICToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol index ea923e9dc..a4d9e8f62 100644 --- a/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol +++ b/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -104,6 +104,8 @@ contract CTokenV3Collateral is AppreciatingFiatCollateral { lastSave = uint48(block.timestamp); } else { // must be unpriced + // untested: + // validated in other plugins, cost to test here is high assert(low == 0); } diff --git a/contracts/plugins/assets/compoundv3/CometHelpers.sol b/contracts/plugins/assets/compoundv3/CometHelpers.sol index e8fb6fc04..bc67617cb 100644 --- a/contracts/plugins/assets/compoundv3/CometHelpers.sol +++ b/contracts/plugins/assets/compoundv3/CometHelpers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; contract CometHelpers { uint64 internal constant BASE_INDEX_SCALE = 1e15; @@ -12,15 +12,12 @@ contract CometHelpers { error NegativeNumber(); function safe64(uint256 n) internal pure returns (uint64) { + // untested: + // comet code, overflow is hard to cover if (n > type(uint64).max) revert InvalidUInt64(); return uint64(n); } - function signed256(uint256 n) internal pure returns (int256) { - if (n > uint256(type(int256).max)) revert InvalidInt256(); - return int256(n); - } - function presentValueSupply(uint64 baseSupplyIndex_, uint104 principalValue_) internal pure @@ -38,15 +35,12 @@ contract CometHelpers { } function safe104(uint256 n) internal pure returns (uint104) { + // untested: + // comet code, overflow is hard to cover if (n > type(uint104).max) revert InvalidUInt104(); return uint104(n); } - function unsigned256(int256 n) internal pure returns (uint256) { - if (n < 0) revert NegativeNumber(); - return uint256(n); - } - /** * @dev Multiply a number by a factor */ diff --git a/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol b/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol index bb6930c33..923432218 100644 --- a/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol +++ b/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./vendor/CometInterface.sol"; @@ -161,6 +161,8 @@ contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers { // occasionally comet will withdraw 1-10 wei more than we asked for. // this is ok because 9 times out of 10 we are rounding in favor of the wrapper. // safe because we have already capped the comet withdraw amount to src underlying bal. + // untested: + // difficult to trigger, depends on comet rules regarding rounding if (srcBalPre <= burnAmt) burnAmt = srcBalPre; accrueAccountRewards(src); diff --git a/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol b/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol index 67a676b3b..f1514ec8e 100644 --- a/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol +++ b/contracts/plugins/assets/compoundv3/ICusdcV3Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/compoundv3/IWrappedERC20.sol b/contracts/plugins/assets/compoundv3/IWrappedERC20.sol index 8b0ea06af..b9e08fca1 100644 --- a/contracts/plugins/assets/compoundv3/IWrappedERC20.sol +++ b/contracts/plugins/assets/compoundv3/IWrappedERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/compoundv3/WrappedERC20.sol b/contracts/plugins/assets/compoundv3/WrappedERC20.sol index a2e2b8170..b3287711d 100644 --- a/contracts/plugins/assets/compoundv3/WrappedERC20.sol +++ b/contracts/plugins/assets/compoundv3/WrappedERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./IWrappedERC20.sol"; @@ -225,9 +225,13 @@ abstract contract WrappedERC20 is IWrappedERC20 { * - `account` must have at least `amount` tokens. */ function _burn(address account, uint256 amount) internal virtual { + // untestable: + // previously validated, account will not be address(0) if (account == address(0)) revert ZeroAddress(); uint256 accountBalance = _balances[account]; + // untestable: + // ammount previously capped to the account balance if (amount > accountBalance) revert ExceedsBalance(amount); unchecked { _balances[account] = accountBalance - amount; diff --git a/contracts/plugins/assets/compoundv3/vendor/CometCore.sol b/contracts/plugins/assets/compoundv3/vendor/CometCore.sol index ad5d8bff3..3626c0f3c 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometCore.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometCore.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./CometStorage.sol"; diff --git a/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol b/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol index d42d48d96..a144d6911 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometExtInterface.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.17; +pragma solidity 0.8.19; struct TotalsBasic { uint64 baseSupplyIndex; diff --git a/contracts/plugins/assets/compoundv3/vendor/CometExtMock.sol b/contracts/plugins/assets/compoundv3/vendor/CometExtMock.sol index ba4e7fa5d..cc182262b 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometExtMock.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometExtMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./CometCore.sol"; diff --git a/contracts/plugins/assets/compoundv3/vendor/CometInterface.sol b/contracts/plugins/assets/compoundv3/vendor/CometInterface.sol index 3c955966a..b68bd0813 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometInterface.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometInterface.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./CometMainInterface.sol"; import "./CometExtInterface.sol"; diff --git a/contracts/plugins/assets/compoundv3/vendor/CometMainInterface.sol b/contracts/plugins/assets/compoundv3/vendor/CometMainInterface.sol index 6e1f18eb8..247bc7738 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometMainInterface.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometMainInterface.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.17; +pragma solidity 0.8.19; struct AssetInfo { uint8 offset; diff --git a/contracts/plugins/assets/compoundv3/vendor/CometStorage.sol b/contracts/plugins/assets/compoundv3/vendor/CometStorage.sol index 9564ca89b..946b8983f 100644 --- a/contracts/plugins/assets/compoundv3/vendor/CometStorage.sol +++ b/contracts/plugins/assets/compoundv3/vendor/CometStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.17; +pragma solidity 0.8.19; /** * @title Compound's Comet Configuration Interface diff --git a/contracts/plugins/assets/compoundv3/vendor/IComet.sol b/contracts/plugins/assets/compoundv3/vendor/IComet.sol index 5a70d2dde..44fffae2d 100644 --- a/contracts/plugins/assets/compoundv3/vendor/IComet.sol +++ b/contracts/plugins/assets/compoundv3/vendor/IComet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface IComet { function getReserves() external view returns (int256); diff --git a/contracts/plugins/assets/compoundv3/vendor/ICometConfigurator.sol b/contracts/plugins/assets/compoundv3/vendor/ICometConfigurator.sol index 05361a854..d92675a38 100644 --- a/contracts/plugins/assets/compoundv3/vendor/ICometConfigurator.sol +++ b/contracts/plugins/assets/compoundv3/vendor/ICometConfigurator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface ICometConfigurator { struct Configuration { diff --git a/contracts/plugins/assets/compoundv3/vendor/ICometProxyAdmin.sol b/contracts/plugins/assets/compoundv3/vendor/ICometProxyAdmin.sol index e96ee181b..bb778143f 100644 --- a/contracts/plugins/assets/compoundv3/vendor/ICometProxyAdmin.sol +++ b/contracts/plugins/assets/compoundv3/vendor/ICometProxyAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/contracts/plugins/assets/compoundv3/vendor/ICometRewards.sol b/contracts/plugins/assets/compoundv3/vendor/ICometRewards.sol index f40b3b491..5d64950be 100644 --- a/contracts/plugins/assets/compoundv3/vendor/ICometRewards.sol +++ b/contracts/plugins/assets/compoundv3/vendor/ICometRewards.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface ICometRewards { struct RewardConfig { diff --git a/contracts/plugins/assets/convex/CvxStableCollateral.sol b/contracts/plugins/assets/curve/CurveStableCollateral.sol similarity index 87% rename from contracts/plugins/assets/convex/CvxStableCollateral.sol rename to contracts/plugins/assets/curve/CurveStableCollateral.sol index 77123e274..4a2b0d35a 100644 --- a/contracts/plugins/assets/convex/CvxStableCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableCollateral.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; @@ -7,25 +7,26 @@ import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "contracts/interfaces/IAsset.sol"; import "contracts/libraries/Fixed.sol"; import "contracts/plugins/assets/AppreciatingFiatCollateral.sol"; -import "./vendor/IConvexStakingWrapper.sol"; -import "./PoolTokens.sol"; +import "../curve/PoolTokens.sol"; /** - * @title CvxStableCollateral - * This plugin contract is fully general to any number of tokens in a plain stable pool, - * with between 1 and 2 oracles per each token. Stable means only like-kind pools. + * @title CurveStableCollateral + * This plugin contract is fully general to any number of (fiat) tokens in a Curve stable pool, + * whether this LP token ends up staked in Curve, Convex, Frax, or somewhere else. + * Each token in the pool can have between 1 and 2 oracles per each token. + * Stable means only like-kind pools. * - * tok = ConvexStakingWrapper(cvxStablePlainPool) - * ref = cvxStablePlainPool pool invariant + * tok = ConvexStakingWrapper(stablePlainPool) + * ref = stablePlainPool pool invariant * tar = USD * UoA = USD */ -contract CvxStableCollateral is AppreciatingFiatCollateral, PoolTokens { +contract CurveStableCollateral is AppreciatingFiatCollateral, PoolTokens { using OracleLib for AggregatorV3Interface; using FixLib for uint192; /// @dev config Unused members: chainlinkFeed, oracleError, oracleTimeout - /// @dev config.erc20 should be a IConvexStakingWrapper + /// @dev config.erc20 should be a RewardableERC20 constructor( CollateralConfig memory config, uint192 revenueHiding, @@ -108,6 +109,8 @@ contract CvxStableCollateral is AppreciatingFiatCollateral, PoolTokens { lastSave = uint48(block.timestamp); } else { // must be unpriced + // untested: + // validated in other plugins, cost to test here is high assert(low == 0); } @@ -156,6 +159,8 @@ contract CvxStableCollateral is AppreciatingFiatCollateral, PoolTokens { if (mid < pegBottom || mid > pegTop) return true; } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data + // untested: + // pattern validated in other plugins, cost to test is high if (errData.length == 0) revert(); // solhint-disable-line reason-string return true; } diff --git a/contracts/plugins/assets/convex/CvxStableMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol similarity index 92% rename from contracts/plugins/assets/convex/CvxStableMetapoolCollateral.sol rename to contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol index 55cc54c18..450cf1f70 100644 --- a/contracts/plugins/assets/convex/CvxStableMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol @@ -1,7 +1,7 @@ -// SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; -import "./CvxStableCollateral.sol"; +import "./CurveStableCollateral.sol"; // solhint-disable no-empty-blocks interface ICurveMetaPool is ICurvePool, IERC20Metadata { @@ -9,8 +9,8 @@ interface ICurveMetaPool is ICurvePool, IERC20Metadata { } /** - * @title CvxStableMetapoolCollateral - * This plugin contract is intended for 2-token stable metapools that + * @title CurveStableMetapoolCollateral + * This plugin contract is intended for 2-fiattoken stable metapools that * DO NOT involve RTokens, such as LUSD-fraxBP or MIM-3CRV. * * tok = ConvexStakingWrapper(PairedUSDToken/USDBasePool) @@ -18,7 +18,7 @@ interface ICurveMetaPool is ICurvePool, IERC20Metadata { * tar = USD * UoA = USD */ -contract CvxStableMetapoolCollateral is CvxStableCollateral { +contract CurveStableMetapoolCollateral is CurveStableCollateral { using OracleLib for AggregatorV3Interface; using FixLib for uint192; @@ -32,14 +32,14 @@ contract CvxStableMetapoolCollateral is CvxStableCollateral { /// @param config.chainlinkFeed Feed units: {UoA/pairedTok} /// @dev config.chainlinkFeed/oracleError/oracleTimeout should be set for paired token - /// @dev config.erc20 should be a IConvexStakingWrapper + /// @dev config.erc20 should be a RewardableERC20 constructor( CollateralConfig memory config, uint192 revenueHiding, PTConfiguration memory ptConfig, ICurveMetaPool metapoolToken_, uint192 pairedTokenDefaultThreshold_ - ) CvxStableCollateral(config, revenueHiding, ptConfig) { + ) CurveStableCollateral(config, revenueHiding, ptConfig) { require(address(metapoolToken_) != address(0), "metapoolToken address is zero"); require( pairedTokenDefaultThreshold_ > 0 && pairedTokenDefaultThreshold_ < FIX_ONE, @@ -129,6 +129,8 @@ contract CvxStableMetapoolCollateral is CvxStableCollateral { if (mid < pairedTokenPegBottom || mid > pairedTokenPegTop) return true; } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data + // untested: + // pattern validated in other plugins, cost to test is high if (errData.length == 0) revert(); // solhint-disable-line reason-string return true; } diff --git a/contracts/plugins/assets/convex/CvxStableRTokenMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol similarity index 74% rename from contracts/plugins/assets/convex/CvxStableRTokenMetapoolCollateral.sol rename to contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol index 3c34abeb3..005a7911b 100644 --- a/contracts/plugins/assets/convex/CvxStableRTokenMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol @@ -1,26 +1,26 @@ -// SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; -import "./CvxStableMetapoolCollateral.sol"; +import "./CurveStableMetapoolCollateral.sol"; /** - * @title CvxStableRTokenMetapoolCollateral - * This plugin contract is intended for 2-token stable metapools that + * @title CurveStableRTokenMetapoolCollateral + * This plugin contract is intended for 2-fiattoken stable metapools that * involve RTokens, such as eUSD-fraxBP. * - * tok = ConvexStakingWrapper(cvxPairedUSDRToken/USDBasePool) + * tok = ConvexStakingWrapper(pairedUSDRToken/USDBasePool) * ref = PairedUSDRToken/USDBasePool pool invariant * tar = USD * UoA = USD */ -contract CvxStableRTokenMetapoolCollateral is CvxStableMetapoolCollateral { +contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral { using FixLib for uint192; IAssetRegistry internal immutable pairedAssetRegistry; // AssetRegistry of pairedToken /// @param config.chainlinkFeed Feed units: {UoA/pairedTok} /// @dev config.chainlinkFeed/oracleError/oracleTimeout are unused; set chainlinkFeed to 0x1 - /// @dev config.erc20 should be a IConvexStakingWrapper + /// @dev config.erc20 should be a RewardableERC20 constructor( CollateralConfig memory config, uint192 revenueHiding, @@ -28,7 +28,7 @@ contract CvxStableRTokenMetapoolCollateral is CvxStableMetapoolCollateral { ICurveMetaPool metapoolToken_, uint192 pairedTokenDefaultThreshold_ ) - CvxStableMetapoolCollateral( + CurveStableMetapoolCollateral( config, revenueHiding, ptConfig, diff --git a/contracts/plugins/assets/convex/CvxVolatileCollateral.sol b/contracts/plugins/assets/curve/CurveVolatileCollateral.sol similarity index 75% rename from contracts/plugins/assets/convex/CvxVolatileCollateral.sol rename to contracts/plugins/assets/curve/CurveVolatileCollateral.sol index 831f5fa3a..4846f4fa2 100644 --- a/contracts/plugins/assets/convex/CvxVolatileCollateral.sol +++ b/contracts/plugins/assets/curve/CurveVolatileCollateral.sol @@ -1,18 +1,19 @@ -// SPDX-License-Identifier: ISC -pragma solidity 0.8.17; -import "./CvxStableCollateral.sol"; +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "./CurveStableCollateral.sol"; /** - * @title CvxVolatileCollateral - * This plugin contract extends CvxCurveStableCollateral to work for + * @title CurveVolatileCollateral + * This plugin contract extends CrvCurveStableCollateral to work for * volatile pools like TriCrypto. * - * tok = ConvexStakingWrapper(cvxVolatilePlainPool) - * ref = cvxVolatilePlainPool pool invariant - * tar = cvxVolatilePlainPool pool invariant + * tok = ConvexStakingWrapper(volatilePlainPool) + * ref = volatilePlainPool pool invariant + * tar = volatilePlainPool pool invariant * UoA = USD */ -contract CvxVolatileCollateral is CvxStableCollateral { +contract CurveVolatileCollateral is CurveStableCollateral { using FixLib for uint192; // this isn't saved by our parent classes, but we'll need to track it @@ -23,7 +24,7 @@ contract CvxVolatileCollateral is CvxStableCollateral { CollateralConfig memory config, uint192 revenueHiding, PTConfiguration memory ptConfig - ) CvxStableCollateral(config, revenueHiding, ptConfig) { + ) CurveStableCollateral(config, revenueHiding, ptConfig) { _defaultThreshold = config.defaultThreshold; } @@ -44,6 +45,8 @@ contract CvxVolatileCollateral is CvxStableCollateral { valSum += vals[i]; } catch (bytes memory errData) { // see: docs/solidity-style.md#Catching-Empty-Data + // untested: + // pattern validated in other plugins, cost to test is high if (errData.length == 0) revert(); // solhint-disable-line reason-string return true; } diff --git a/contracts/plugins/assets/convex/PoolTokens.sol b/contracts/plugins/assets/curve/PoolTokens.sol similarity index 96% rename from contracts/plugins/assets/convex/PoolTokens.sol rename to contracts/plugins/assets/curve/PoolTokens.sol index 4e196ddfb..bc5367dcd 100644 --- a/contracts/plugins/assets/convex/PoolTokens.sol +++ b/contracts/plugins/assets/curve/PoolTokens.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -32,9 +32,7 @@ interface ICurvePool { ) external; } -// solhint-enable func-name-mixedcase - -/// Supports CvxCurve non-meta pools for up to 4 tokens +/// Supports Curve base pools for up to 4 tokens contract PoolTokens { using OracleLib for AggregatorV3Interface; using FixLib for uint192; @@ -44,7 +42,7 @@ contract PoolTokens { enum CurvePoolType { Plain, - Lending, + Lending, // not supported in this version Metapool // not supported via this class. parent class handles metapool math } @@ -126,10 +124,8 @@ contract PoolTokens { for (uint8 i = 0; i < nTokens; ++i) { if (config.poolType == CurvePoolType.Plain) { tokens[i] = IERC20Metadata(curvePool.coins(i)); - } else if (config.poolType == CurvePoolType.Lending) { - tokens[i] = IERC20Metadata(curvePool.underlying_coins(i)); } else { - revert("Use MetaPoolTokens class"); + revert("invalid poolType"); } } @@ -146,6 +142,8 @@ contract PoolTokens { // token0 bool more = config.feeds[0].length > 0; + // untestable: + // more will always be true based on previous feeds validations _t0feed0 = more ? config.feeds[0][0] : AggregatorV3Interface(address(0)); _t0timeout0 = more && config.oracleTimeouts[0].length > 0 ? config.oracleTimeouts[0][0] : 0; _t0error0 = more && config.oracleErrors[0].length > 0 ? config.oracleErrors[0][0] : 0; @@ -166,6 +164,8 @@ contract PoolTokens { } // token1 + // untestable: + // more will always be true based on previous feeds validations more = config.feeds[1].length > 0; _t1feed0 = more ? config.feeds[1][0] : AggregatorV3Interface(address(0)); _t1timeout0 = more && config.oracleTimeouts[1].length > 0 ? config.oracleTimeouts[1][0] : 0; @@ -307,6 +307,8 @@ contract PoolTokens { // === Private === function getToken(uint8 index) private view returns (IERC20Metadata) { + // untestable: + // getToken is always called with a valid index if (index >= nTokens) revert WrongIndex(nTokens - 1); if (index == 0) return token0; if (index == 1) return token1; diff --git a/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol b/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol new file mode 100644 index 000000000..4d712ac72 --- /dev/null +++ b/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../../erc20/RewardableERC20Wrapper.sol"; + +interface IMinter { + /// Mint CRV to msg.sender based on their prorata share of the provided gauge + function mint(address gaugeAddr) external; +} + +interface ILiquidityGauge { + function deposit(uint256 _value) external; + + /// @param _value LP token amount + function withdraw(uint256 _value) external; +} + +contract CurveGaugeWrapper is RewardableERC20Wrapper { + using SafeERC20 for IERC20; + + IMinter public constant MINTER = IMinter(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0); + + ILiquidityGauge public immutable gauge; + + /// @param _lpToken The curve LP token, transferrable + constructor( + ERC20 _lpToken, + string memory _name, + string memory _symbol, + ERC20 _crv, + ILiquidityGauge _gauge + ) RewardableERC20Wrapper(_lpToken, _name, _symbol, _crv) { + gauge = _gauge; + } + + // deposit a curve token + function _afterDeposit(uint256 _amount, address) internal override { + underlying.approve(address(gauge), _amount); + gauge.deposit(_amount); + } + + // withdraw to curve token + function _beforeWithdraw(uint256 _amount, address) internal override { + gauge.withdraw(_amount); + } + + function _claimAssetRewards() internal virtual override { + MINTER.mint(address(gauge)); + } +} diff --git a/contracts/plugins/assets/convex/README.md b/contracts/plugins/assets/curve/cvx/README.md similarity index 100% rename from contracts/plugins/assets/convex/README.md rename to contracts/plugins/assets/curve/cvx/README.md diff --git a/contracts/plugins/assets/convex/vendor/ConvexInterfaces.sol b/contracts/plugins/assets/curve/cvx/vendor/ConvexInterfaces.sol similarity index 99% rename from contracts/plugins/assets/convex/vendor/ConvexInterfaces.sol rename to contracts/plugins/assets/curve/cvx/vendor/ConvexInterfaces.sol index a6404c304..0a8c00e15 100644 --- a/contracts/plugins/assets/convex/vendor/ConvexInterfaces.sol +++ b/contracts/plugins/assets/curve/cvx/vendor/ConvexInterfaces.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface ICurveGauge { function deposit(uint256) external; diff --git a/contracts/plugins/assets/convex/vendor/ConvexStakingWrapper.sol b/contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol similarity index 100% rename from contracts/plugins/assets/convex/vendor/ConvexStakingWrapper.sol rename to contracts/plugins/assets/curve/cvx/vendor/ConvexStakingWrapper.sol diff --git a/contracts/plugins/assets/convex/vendor/CvxMining.sol b/contracts/plugins/assets/curve/cvx/vendor/CvxMining.sol similarity index 100% rename from contracts/plugins/assets/convex/vendor/CvxMining.sol rename to contracts/plugins/assets/curve/cvx/vendor/CvxMining.sol diff --git a/contracts/plugins/assets/convex/vendor/IConvexStakingWrapper.sol b/contracts/plugins/assets/curve/cvx/vendor/IConvexStakingWrapper.sol similarity index 90% rename from contracts/plugins/assets/convex/vendor/IConvexStakingWrapper.sol rename to contracts/plugins/assets/curve/cvx/vendor/IConvexStakingWrapper.sol index e177c86b5..970aaf3f7 100644 --- a/contracts/plugins/assets/convex/vendor/IConvexStakingWrapper.sol +++ b/contracts/plugins/assets/curve/cvx/vendor/IConvexStakingWrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface IConvexStakingWrapper { function crv() external returns (address); diff --git a/contracts/plugins/assets/convex/vendor/IRewardStaking.sol b/contracts/plugins/assets/curve/cvx/vendor/IRewardStaking.sol similarity index 100% rename from contracts/plugins/assets/convex/vendor/IRewardStaking.sol rename to contracts/plugins/assets/curve/cvx/vendor/IRewardStaking.sol diff --git a/contracts/plugins/assets/convex/vendor/StableSwap3Pool.vy b/contracts/plugins/assets/curve/cvx/vendor/StableSwap3Pool.vy similarity index 100% rename from contracts/plugins/assets/convex/vendor/StableSwap3Pool.vy rename to contracts/plugins/assets/curve/cvx/vendor/StableSwap3Pool.vy diff --git a/contracts/plugins/assets/dsr/README.md b/contracts/plugins/assets/dsr/README.md new file mode 100644 index 000000000..6adf9b64d --- /dev/null +++ b/contracts/plugins/assets/dsr/README.md @@ -0,0 +1,27 @@ +# SDAI DSR Collateral Plugin + +## Summary + +This plugin allows `sDAI` holders to use their tokens as collateral in the Reserve Protocol. + +sDAI is an unowned, immutable, ERC4626-wrapper around the Dai savings rate. + +`sDAI` will accrue the same amount of DAI as it would if it were deposited directly into the DSR. + +Since it is ERC4626, the redeemable DAI amount can be gotten by dividing `sDAI.totalAssets()` by `sDAI.totalSupply()`. However, the same rate can be read out more directly by calling `pot.chi()`, for the MakerDAO pot. There is a mutation required before either of these values can be read. + +`sDAI` contract: + +## Implementation + +### Units + +| tok | ref | target | UoA | +| ---- | --- | ------ | --- | +| sDAI | DAI | USD | USD | + +### Functions + +#### refPerTok {ref/tok} + +`return shiftl_toFix(pot.chi(), -27);` diff --git a/contracts/plugins/assets/dsr/SDaiCollateral.sol b/contracts/plugins/assets/dsr/SDaiCollateral.sol new file mode 100644 index 000000000..8e7643575 --- /dev/null +++ b/contracts/plugins/assets/dsr/SDaiCollateral.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "../../../libraries/Fixed.sol"; +import "../AppreciatingFiatCollateral.sol"; + +/// MakerDAO Pot +interface IPot { + function rho() external returns (uint256); + + function drip() external returns (uint256); + + /// {ray} + function chi() external view returns (uint256); +} + +/** + * @title SDAI Collateral + * @notice Collateral plugin for the DSR wrapper sDAI + * tok = SDAI (transferrable DSR-locked DAI) + * ref = DAI + * tar = USD + * UoA = USD + */ +contract SDaiCollateral is AppreciatingFiatCollateral { + using OracleLib for AggregatorV3Interface; + using FixLib for uint192; + + IPot public immutable pot; + + /// @param config.chainlinkFeed {UoA/ref} price of DAI in USD terms + constructor( + CollateralConfig memory config, + uint192 revenueHiding, + IPot _pot + ) AppreciatingFiatCollateral(config, revenueHiding) { + pot = _pot; + } + + /// Refresh exchange rates and update default status. + /// @custom:interaction RCEI + function refresh() public virtual override { + // == Refresh == + // Update the DSR contract + + if (block.timestamp > pot.rho()) pot.drip(); + + // Intentional and correct for the super call to be last! + super.refresh(); // already handles all necessary default checks + } + + /// @return {ref/tok} Actual quantity of whole reference units per whole collateral tokens + function _underlyingRefPerTok() internal view override returns (uint192) { + return shiftl_toFix(pot.chi(), -27); + } +} diff --git a/contracts/plugins/assets/vaults/RewardableERC20Vault.sol b/contracts/plugins/assets/erc20/RewardableERC20.sol similarity index 75% rename from contracts/plugins/assets/vaults/RewardableERC20Vault.sol rename to contracts/plugins/assets/erc20/RewardableERC20.sol index c9140be70..90d1846e6 100644 --- a/contracts/plugins/assets/vaults/RewardableERC20Vault.sol +++ b/contracts/plugins/assets/erc20/RewardableERC20.sol @@ -1,37 +1,33 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; -import "../../../interfaces/IRewardable.sol"; -import "../../../vendor/oz/ERC4626.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../../../interfaces/IRewardable.sol"; /** - * @title RewardableERC20Vault - * @notice A transferrable vault token wrapping an inner ERC20 that earns rewards. - * Holding the vault token for a period of time earns the holder the right to - * their prorata share of the global rewards earned during that time. - * @dev To inherit, override _claimAssetRewards() + * @title RewardableERC20 + * @notice An abstract class that can be extended to create rewardable wrapper + * @dev To inherit: + * - override _claimAssetRewards() + * - call ERC20 constructor elsewhere during construction */ -abstract contract RewardableERC20Vault is IRewardable, ERC4626 { - using SafeERC20 for ERC20; +abstract contract RewardableERC20 is IRewardable, ERC20 { + using SafeERC20 for IERC20; uint256 public immutable one; // {qShare/share} - ERC20 public immutable rewardToken; + IERC20 public immutable rewardToken; uint256 public rewardsPerShare; // {qRewards/share} mapping(address => uint256) public lastRewardsPerShare; // {qRewards/share} mapping(address => uint256) public accumulatedRewards; // {qRewards} mapping(address => uint256) public claimedRewards; // {qRewards} - constructor( - ERC20 _asset, - string memory _name, - string memory _symbol, - ERC20 _rewardToken - ) ERC4626(_asset, _name, _symbol) { + /// @dev Extending class must ensure ERC20 constructor is called + constructor(IERC20 _rewardToken, uint8 _decimals) { rewardToken = _rewardToken; - one = 10**decimals(); + one = 10**_decimals; // set via pass-in to prevent inheritance issues } function claimRewards() external { @@ -67,8 +63,6 @@ abstract contract RewardableERC20Vault is IRewardable, ERC4626 { } } - function _claimAssetRewards() internal virtual; - function _claimAccountRewards(address account) internal { uint256 claimableRewards = accumulatedRewards[account] - claimedRewards[account]; emit RewardsClaimed(IERC20(address(rewardToken)), claimableRewards); @@ -87,7 +81,7 @@ abstract contract RewardableERC20Vault is IRewardable, ERC4626 { _syncAccount(to); } - function _decimalsOffset() internal view virtual override returns (uint8) { - return 9; - } + /// === Must override === + + function _claimAssetRewards() internal virtual; } diff --git a/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol b/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol new file mode 100644 index 000000000..285582a02 --- /dev/null +++ b/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "./RewardableERC20.sol"; + +/** + * @title RewardableERC20Wrapper + * @notice A transferrable ERC20 wrapper token wrapping an inner position that earns rewards. + * @dev To inherit: + * - override _claimAssetRewards() + * - consider overriding _afterDeposit() and _beforeWithdraw() + */ +abstract contract RewardableERC20Wrapper is RewardableERC20 { + using SafeERC20 for IERC20; + + IERC20 public immutable underlying; + + uint8 private immutable underlyingDecimals; + + event Deposited(address indexed _user, address indexed _account, uint256 _amount); + event Withdrawn(address indexed _user, address indexed _account, uint256 _amount); + + /// @dev Extending class must ensure ERC20 constructor is called + constructor( + IERC20Metadata _underlying, + string memory _name, + string memory _symbol, + IERC20 _rewardToken + ) ERC20(_name, _symbol) RewardableERC20(_rewardToken, _underlying.decimals()) { + underlying = _underlying; + underlyingDecimals = _underlying.decimals(); + } + + function decimals() public view virtual override returns (uint8) { + return underlyingDecimals; + } + + /// Deposit the underlying token and optionally take an action such as staking in a gauge + function deposit(uint256 _amount, address _to) external virtual { + if (_amount > 0) { + _mint(_to, _amount); // does balance checkpointing + underlying.safeTransferFrom(msg.sender, address(this), _amount); + _afterDeposit(_amount, _to); + } + emit Deposited(msg.sender, _to, _amount); + } + + /// Withdraw the underlying token and optionally take an action such as staking in a gauge + function withdraw(uint256 _amount, address _to) external virtual { + if (_amount > 0) { + _burn(msg.sender, _amount); // does balance checkpointing + _beforeWithdraw(_amount, _to); + underlying.safeTransfer(_to, _amount); + } + + emit Withdrawn(msg.sender, _to, _amount); + } + + /// === Must override === + + // function _claimAssetRewards() internal virtual; + + /// === May override === + // solhint-disable no-empty-blocks + + /// Any steps that should be taken after deposit, such as staking in a gauge + function _afterDeposit(uint256 _amount, address to) internal virtual {} + + /// Any steps that should be taken before withdraw, such as unstaking from a gauge + function _beforeWithdraw(uint256 _amount, address to) internal virtual {} +} diff --git a/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol b/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol new file mode 100644 index 000000000..284f717c2 --- /dev/null +++ b/contracts/plugins/assets/erc20/RewardableERC4626Vault.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../../../interfaces/IRewardable.sol"; +import "../../../vendor/oz/ERC4626.sol"; +import "./RewardableERC20.sol"; + +/** + * @title RewardableERC4626Vault + * @notice A transferrable ERC4626 vault wrapping an inner position that earns rewards. + * Holding the vault token for a period of time earns the holder the right to + * their prorata share of the global rewards earned during that time. + * @dev To inherit: + * - override _claimAssetRewards() + * - consider overriding _afterDeposit() and _beforeWithdraw() + */ +abstract contract RewardableERC4626Vault is ERC4626, RewardableERC20 { + // solhint-disable no-empty-blocks + constructor( + IERC20Metadata _asset, + string memory _name, + string memory _symbol, + ERC20 _rewardToken + ) + ERC4626(_asset, _name, _symbol) + RewardableERC20(_rewardToken, _asset.decimals() + _decimalsOffset()) + {} + + // solhint-enable no-empty-blocks + + function decimals() public view virtual override(ERC4626, ERC20) returns (uint8) { + return ERC4626.decimals(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(RewardableERC20, ERC20) { + RewardableERC20._beforeTokenTransfer(from, to, amount); + } + + function _decimalsOffset() internal view virtual override returns (uint8) { + return 9; + } + + /// === Must override === + + // function _claimAssetRewards() internal virtual; +} diff --git a/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol b/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol index ee50f90eb..4697ec0da 100644 --- a/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol +++ b/contracts/plugins/assets/frax-eth/SFraxEthCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/math/Math.sol"; import "../../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/frax-eth/vendor/IfrxEthMinter.sol b/contracts/plugins/assets/frax-eth/vendor/IfrxEthMinter.sol index 815e8cb5d..b8a71ad33 100644 --- a/contracts/plugins/assets/frax-eth/vendor/IfrxEthMinter.sol +++ b/contracts/plugins/assets/frax-eth/vendor/IfrxEthMinter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; interface IfrxEthMinter { function submitAndDeposit(address recipient) external payable returns (uint256 shares); diff --git a/contracts/plugins/assets/frax-eth/vendor/IsfrxEth.sol b/contracts/plugins/assets/frax-eth/vendor/IsfrxEth.sol index 70514a7ee..fe904d019 100644 --- a/contracts/plugins/assets/frax-eth/vendor/IsfrxEth.sol +++ b/contracts/plugins/assets/frax-eth/vendor/IsfrxEth.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol b/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol index 575f94d2b..783896f2c 100644 --- a/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol +++ b/contracts/plugins/assets/lido/LidoStakedEthCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/math/Math.sol"; import "../../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/lido/vendor/ISTETH.sol b/contracts/plugins/assets/lido/vendor/ISTETH.sol index bb49ff6e8..9a1b41642 100644 --- a/contracts/plugins/assets/lido/vendor/ISTETH.sol +++ b/contracts/plugins/assets/lido/vendor/ISTETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/plugins/assets/lido/vendor/IWSTETH.sol b/contracts/plugins/assets/lido/vendor/IWSTETH.sol index 7d282bac6..4f5446df5 100644 --- a/contracts/plugins/assets/lido/vendor/IWSTETH.sol +++ b/contracts/plugins/assets/lido/vendor/IWSTETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/rocket-eth/RethCollateral.sol b/contracts/plugins/assets/rocket-eth/RethCollateral.sol index 6c8708271..42888b93d 100644 --- a/contracts/plugins/assets/rocket-eth/RethCollateral.sol +++ b/contracts/plugins/assets/rocket-eth/RethCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/math/Math.sol"; import "../../../libraries/Fixed.sol"; diff --git a/contracts/plugins/assets/rocket-eth/vendor/IReth.sol b/contracts/plugins/assets/rocket-eth/vendor/IReth.sol index ae64bb07d..8983c3b31 100644 --- a/contracts/plugins/assets/rocket-eth/vendor/IReth.sol +++ b/contracts/plugins/assets/rocket-eth/vendor/IReth.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/assets/rocket-eth/vendor/IRocketNetworkBalances.sol b/contracts/plugins/assets/rocket-eth/vendor/IRocketNetworkBalances.sol index c131dd9d9..117fb7979 100644 --- a/contracts/plugins/assets/rocket-eth/vendor/IRocketNetworkBalances.sol +++ b/contracts/plugins/assets/rocket-eth/vendor/IRocketNetworkBalances.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface IRocketNetworkBalances { function getTotalETHBalance() external view returns (uint256); diff --git a/contracts/plugins/assets/rocket-eth/vendor/IRocketStorage.sol b/contracts/plugins/assets/rocket-eth/vendor/IRocketStorage.sol index a4b88fa5f..4ae67598e 100644 --- a/contracts/plugins/assets/rocket-eth/vendor/IRocketStorage.sol +++ b/contracts/plugins/assets/rocket-eth/vendor/IRocketStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; interface IRocketStorage { function setUint(bytes32 _key, uint256 _value) external; diff --git a/contracts/plugins/governance/Governance.sol b/contracts/plugins/governance/Governance.sol index 2a4af73ae..c20978fa0 100644 --- a/contracts/plugins/governance/Governance.sol +++ b/contracts/plugins/governance/Governance.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/governance/Governor.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; diff --git a/contracts/plugins/mocks/ATokenMock.sol b/contracts/plugins/mocks/ATokenMock.sol index c852d1e3a..5459fbaba 100644 --- a/contracts/plugins/mocks/ATokenMock.sol +++ b/contracts/plugins/mocks/ATokenMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/aave/ATokenFiatCollateral.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/mocks/ATokenNoController.sol b/contracts/plugins/mocks/ATokenNoController.sol new file mode 100644 index 000000000..b9f94e9b5 --- /dev/null +++ b/contracts/plugins/mocks/ATokenNoController.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.6.12; + +import "@aave/protocol-v2/contracts/protocol/tokenization/AToken.sol"; +import "../assets/aave/vendor/IAaveIncentivesController.sol"; + +contract ATokenNoController is AToken { + constructor( + ILendingPool pool, + address underlyingAssetAddress, + address reserveTreasuryAddress, + string memory tokenName, + string memory tokenSymbol, + address incentivesController + ) + public + AToken( + pool, + underlyingAssetAddress, + reserveTreasuryAddress, + tokenName, + tokenSymbol, + incentivesController + ) + {} + + function getIncentivesController() external pure returns (IAaveIncentivesController) { + return IAaveIncentivesController(address(0)); + } +} diff --git a/contracts/plugins/mocks/AaveLendingPoolMock.sol b/contracts/plugins/mocks/AaveLendingPoolMock.sol index 3ee3218bf..8c1a969d0 100644 --- a/contracts/plugins/mocks/AaveLendingPoolMock.sol +++ b/contracts/plugins/mocks/AaveLendingPoolMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; // See: https://github.com/aave/protocol-v2/tree/master/contracts/interfaces interface IAaveLendingPool { diff --git a/contracts/plugins/mocks/BackingMgrBackCompatible.sol b/contracts/plugins/mocks/BackingMgrBackCompatible.sol new file mode 100644 index 000000000..9fe7767cc --- /dev/null +++ b/contracts/plugins/mocks/BackingMgrBackCompatible.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../../p1/BackingManager.sol"; + +interface IBackingManagerComp { + function manageTokens(IERC20[] memory erc20s) external; +} + +// BackingManager compatible with version 2 +contract BackingMgrCompatibleV2 is BackingManagerP1, IBackingManagerComp { + function manageTokens(IERC20[] calldata erc20s) external notTradingPausedOrFrozen { + // Mirror V3 logic (only the section relevant to tests) + if (erc20s.length == 0) { + this.rebalance(TradeKind.DUTCH_AUCTION); + } else { + this.forwardRevenue(erc20s); + } + } + + function version() public pure virtual override(Versioned, IVersioned) returns (string memory) { + return "2.1.0"; + } +} + +// BackingManager compatible with version 1 +contract BackingMgrCompatibleV1 is BackingMgrCompatibleV2 { + function version() public pure override(BackingMgrCompatibleV2) returns (string memory) { + return "1.0.0"; + } +} + +// BackingManager with invalid version +contract BackingMgrInvalidVersion is BackingMgrCompatibleV2 { + function version() public pure override(BackingMgrCompatibleV2) returns (string memory) { + return "0.0.0"; + } +} diff --git a/contracts/plugins/mocks/BadCollateralPlugin.sol b/contracts/plugins/mocks/BadCollateralPlugin.sol index a0564ca26..2af22a129 100644 --- a/contracts/plugins/mocks/BadCollateralPlugin.sol +++ b/contracts/plugins/mocks/BadCollateralPlugin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/aave/ATokenFiatCollateral.sol"; diff --git a/contracts/plugins/mocks/BadERC20.sol b/contracts/plugins/mocks/BadERC20.sol index 3af7fff78..99c7791e8 100644 --- a/contracts/plugins/mocks/BadERC20.sol +++ b/contracts/plugins/mocks/BadERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/Address.sol"; import "../../libraries/Fixed.sol"; @@ -8,18 +8,24 @@ import "./ERC20Mock.sol"; contract BadERC20 is ERC20Mock { using Address for address; using FixLib for uint192; + uint8 private _decimals; uint192 public transferFee; // {1} - bool public revertDecimals; mapping(address => bool) public censored; - constructor(string memory name, string memory symbol) ERC20Mock(name, symbol) {} + constructor(string memory name, string memory symbol) ERC20Mock(name, symbol) { + _decimals = 18; + } function setTransferFee(uint192 newFee) external { transferFee = newFee; } + function setDecimals(uint8 newVal) external { + _decimals = newVal; + } + function setRevertDecimals(bool newVal) external { revertDecimals = newVal; } @@ -33,7 +39,7 @@ contract BadERC20 is ERC20Mock { // Make an external staticcall to this address, for a function that does not exist if (revertDecimals) address(this).functionStaticCall(data, "No Decimals"); - return 18; + return _decimals; } function transfer(address to, uint256 amount) public virtual override returns (bool) { diff --git a/contracts/plugins/mocks/CTokenMock.sol b/contracts/plugins/mocks/CTokenMock.sol index da13680ec..d02401dee 100644 --- a/contracts/plugins/mocks/CTokenMock.sol +++ b/contracts/plugins/mocks/CTokenMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/mocks/CTokenVaultMock2.sol b/contracts/plugins/mocks/CTokenWrapperMock2.sol similarity index 72% rename from contracts/plugins/mocks/CTokenVaultMock2.sol rename to contracts/plugins/mocks/CTokenWrapperMock2.sol index d303f1440..7ada69cb2 100644 --- a/contracts/plugins/mocks/CTokenVaultMock2.sol +++ b/contracts/plugins/mocks/CTokenWrapperMock2.sol @@ -1,14 +1,14 @@ // 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 "../assets/compoundv2/CTokenVault.sol"; +import "../assets/compoundv2/CTokenWrapper.sol"; import "../assets/compoundv2/ICToken.sol"; import "./CTokenMock.sol"; -contract CTokenVaultMock is ERC20Mock, IRewardable { +contract CTokenWrapperMock is ERC20Mock, IRewardable { ERC20Mock public comp; - CTokenMock public asset; + CTokenMock public underlying; IComptroller public comptroller; bool public revertClaimRewards; @@ -20,25 +20,21 @@ contract CTokenVaultMock is ERC20Mock, IRewardable { ERC20Mock _comp, IComptroller _comptroller ) ERC20Mock(_name, _symbol) { - asset = new CTokenMock("cToken Mock", "cMOCK", _underlyingToken); + underlying = new CTokenMock("cToken Mock", "cMOCK", _underlyingToken); comp = _comp; comptroller = _comptroller; } - // function mint(uint256 amount, address recipient) external { - // _mint(recipient, amount); - // } - function decimals() public pure override returns (uint8) { return 8; } function exchangeRateCurrent() external returns (uint256) { - return asset.exchangeRateCurrent(); + return underlying.exchangeRateCurrent(); } function exchangeRateStored() external view returns (uint256) { - return asset.exchangeRateStored(); + return underlying.exchangeRateStored(); } function claimRewards() external { @@ -51,7 +47,7 @@ contract CTokenVaultMock is ERC20Mock, IRewardable { } function setExchangeRate(uint192 fiatcoinRedemptionRate) external { - asset.setExchangeRate(fiatcoinRedemptionRate); + underlying.setExchangeRate(fiatcoinRedemptionRate); } function setRevertClaimRewards(bool newVal) external { diff --git a/contracts/plugins/mocks/CometMock.sol b/contracts/plugins/mocks/CometMock.sol index 5f8d3ab88..16556c948 100644 --- a/contracts/plugins/mocks/CometMock.sol +++ b/contracts/plugins/mocks/CometMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.17; +pragma solidity 0.8.19; // prettier-ignore contract CometMock { diff --git a/contracts/plugins/mocks/ComptrollerMock.sol b/contracts/plugins/mocks/ComptrollerMock.sol index 29d508371..8e0d12850 100644 --- a/contracts/plugins/mocks/ComptrollerMock.sol +++ b/contracts/plugins/mocks/ComptrollerMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/compoundv2/ICToken.sol"; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/CurveMetapoolMock.sol b/contracts/plugins/mocks/CurveMetapoolMock.sol index 6c8adcb06..70ee17a3f 100644 --- a/contracts/plugins/mocks/CurveMetapoolMock.sol +++ b/contracts/plugins/mocks/CurveMetapoolMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./CurvePoolMock.sol"; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/CurvePoolMock.sol b/contracts/plugins/mocks/CurvePoolMock.sol index 4ce731422..cd33b7ba1 100644 --- a/contracts/plugins/mocks/CurvePoolMock.sol +++ b/contracts/plugins/mocks/CurvePoolMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: ISC -pragma solidity 0.8.17; +pragma solidity 0.8.19; -import "../../plugins/assets/convex/PoolTokens.sol"; +import "../../plugins/assets/curve/PoolTokens.sol"; contract CurvePoolMock is ICurvePool { uint256[] internal _balances; @@ -38,3 +38,52 @@ contract CurvePoolMock is ICurvePool { uint256 ) external {} } + +interface ICurvePoolVariantInt { + // For Curve Plain Pools and V2 Metapools + function coins(int128) external view returns (address); + + // Only exists in Curve Lending Pools + function underlying_coins(int128) external view returns (address); + + // Uses int128 as index + function balances(int128) external view returns (uint256); + + function get_virtual_price() external view returns (uint256); +} + +// Required for some Curve Pools that use int128 as index +contract CurvePoolMockVariantInt is ICurvePoolVariantInt { + uint256[] internal _balances; + address[] internal _coins; + address[] internal _underlying_coins; + uint256 public get_virtual_price; + + constructor(uint256[] memory initialBalances, address[] memory initialCoins) { + _balances = initialBalances; + _coins = initialCoins; + } + + function setBalances(uint256[] memory newBalances) external { + _balances = newBalances; + } + + function balances(int128 index) external view returns (uint256) { + uint256 newIndex = uint256(abs(index)); + return _balances[newIndex]; + } + + function coins(int128 index) external view returns (address) { + uint256 newIndex = uint256(abs(index)); + return _coins[newIndex]; + } + + function underlying_coins(int128 index) external view returns (address) { + uint256 newIndex = uint256(abs(index)); + return _underlying_coins[newIndex]; + } + + function setVirtualPrice(uint256 newPrice) external { + get_virtual_price = newPrice; + } +} diff --git a/contracts/plugins/mocks/CusdcV3WrapperMock.sol b/contracts/plugins/mocks/CusdcV3WrapperMock.sol index c23f92cea..844c9ef78 100644 --- a/contracts/plugins/mocks/CusdcV3WrapperMock.sol +++ b/contracts/plugins/mocks/CusdcV3WrapperMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/compoundv3/CusdcV3Wrapper.sol"; import "../assets/compoundv3/ICusdcV3Wrapper.sol"; diff --git a/contracts/plugins/mocks/EACAggregatorProxyMock.sol b/contracts/plugins/mocks/EACAggregatorProxyMock.sol index 2b553b10a..82b2b52d4 100644 --- a/contracts/plugins/mocks/EACAggregatorProxyMock.sol +++ b/contracts/plugins/mocks/EACAggregatorProxyMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity 0.8.19; /** * @title The Owned contract diff --git a/contracts/plugins/mocks/ERC1271Mock.sol b/contracts/plugins/mocks/ERC1271Mock.sol index 330f24832..ccc982ed1 100644 --- a/contracts/plugins/mocks/ERC1271Mock.sol +++ b/contracts/plugins/mocks/ERC1271Mock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/Address.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/mocks/ERC20Mock.sol b/contracts/plugins/mocks/ERC20Mock.sol index 5d12efbeb..f374070f5 100644 --- a/contracts/plugins/mocks/ERC20Mock.sol +++ b/contracts/plugins/mocks/ERC20Mock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/plugins/mocks/ERC20MockDecimals.sol b/contracts/plugins/mocks/ERC20MockDecimals.sol index b036d7527..d0449c1bf 100644 --- a/contracts/plugins/mocks/ERC20MockDecimals.sol +++ b/contracts/plugins/mocks/ERC20MockDecimals.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/ERC20MockRewarding.sol b/contracts/plugins/mocks/ERC20MockRewarding.sol index 7a2fc5459..69b0aff23 100644 --- a/contracts/plugins/mocks/ERC20MockRewarding.sol +++ b/contracts/plugins/mocks/ERC20MockRewarding.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./ERC20MockDecimals.sol"; diff --git a/contracts/plugins/mocks/GasGuzzlingFiatCollateral.sol b/contracts/plugins/mocks/GasGuzzlingFiatCollateral.sol index 7c91a76b2..ae3b82283 100644 --- a/contracts/plugins/mocks/GasGuzzlingFiatCollateral.sol +++ b/contracts/plugins/mocks/GasGuzzlingFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/FiatCollateral.sol"; diff --git a/contracts/plugins/mocks/GnosisMock.sol b/contracts/plugins/mocks/GnosisMock.sol index 2dc02b859..3d5490188 100644 --- a/contracts/plugins/mocks/GnosisMock.sol +++ b/contracts/plugins/mocks/GnosisMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/plugins/mocks/GnosisMockReentrant.sol b/contracts/plugins/mocks/GnosisMockReentrant.sol index 303dae4aa..52d2382af 100644 --- a/contracts/plugins/mocks/GnosisMockReentrant.sol +++ b/contracts/plugins/mocks/GnosisMockReentrant.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/plugins/mocks/InvalidATokenFiatCollateralMock.sol b/contracts/plugins/mocks/InvalidATokenFiatCollateralMock.sol index e349804c3..a1f8bdbb3 100644 --- a/contracts/plugins/mocks/InvalidATokenFiatCollateralMock.sol +++ b/contracts/plugins/mocks/InvalidATokenFiatCollateralMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/aave/ATokenFiatCollateral.sol"; diff --git a/contracts/plugins/mocks/InvalidBrokerMock.sol b/contracts/plugins/mocks/InvalidBrokerMock.sol index 7bbe3b728..67e4ae98c 100644 --- a/contracts/plugins/mocks/InvalidBrokerMock.sol +++ b/contracts/plugins/mocks/InvalidBrokerMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/mocks/InvalidFiatCollateral.sol b/contracts/plugins/mocks/InvalidFiatCollateral.sol index 1233d06d3..343737bd3 100644 --- a/contracts/plugins/mocks/InvalidFiatCollateral.sol +++ b/contracts/plugins/mocks/InvalidFiatCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/FiatCollateral.sol"; diff --git a/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol b/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol index 7506e491d..3ad165f9a 100644 --- a/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol +++ b/contracts/plugins/mocks/InvalidRefPerTokCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../libraries/Fixed.sol"; diff --git a/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol b/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol new file mode 100644 index 000000000..b19793ffa --- /dev/null +++ b/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../../interfaces/IMain.sol"; +import "../../interfaces/IAssetRegistry.sol"; +import "../../p1/mixins/Trading.sol"; +import "../../p1/mixins/TradeLib.sol"; + +/// Trader Component that reverts on manageToken +contract RevenueTraderP1InvalidReverts is TradingP1, IRevenueTrader { + using FixLib for uint192; + using SafeERC20 for IERC20; + + // Immutable after init() + IERC20 public tokenToBuy; + IAssetRegistry private assetRegistry; + IDistributor private distributor; + IBackingManager private backingManager; + IFurnace private furnace; + IRToken private rToken; + + function init( + IMain main_, + IERC20 tokenToBuy_, + uint192 maxTradeSlippage_, + uint192 minTradeVolume_ + ) external initializer { + require(address(tokenToBuy_) != address(0), "invalid token address"); + __Component_init(main_); + __Trading_init(main_, maxTradeSlippage_, minTradeVolume_); + tokenToBuy = tokenToBuy_; + cacheComponents(); + } + + /// Distribute tokenToBuy to its destinations + function distributeTokenToBuy() public { + uint256 bal = tokenToBuy.balanceOf(address(this)); + tokenToBuy.safeApprove(address(main.distributor()), 0); + tokenToBuy.safeApprove(address(main.distributor()), bal); + main.distributor().distribute(tokenToBuy, bal); + } + + /// Processes a single token; unpermissioned + /// Reverts for testing purposes + function manageToken(IERC20, TradeKind) external notTradingPausedOrFrozen { + rToken = rToken; // silence warning + revert(); + } + + function cacheComponents() public { + assetRegistry = main.assetRegistry(); + distributor = main.distributor(); + backingManager = main.backingManager(); + furnace = main.furnace(); + rToken = main.rToken(); + } +} diff --git a/contracts/plugins/mocks/MakerPotMock.sol b/contracts/plugins/mocks/MakerPotMock.sol new file mode 100644 index 000000000..1e59ac158 --- /dev/null +++ b/contracts/plugins/mocks/MakerPotMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../assets/dsr/SDaiCollateral.sol"; + +contract PotMock is IPot { + IPot public immutable pot; + + uint256 public chi; // {ray} + + constructor(IPot _pot) { + pot = _pot; + chi = pot.chi(); + } + + function setChi(uint256 newChi) external { + chi = newChi; + } + + function drip() external returns (uint256) { + return pot.drip(); + } + + function rho() external returns (uint256) { + return pot.rho(); + } +} diff --git a/contracts/plugins/mocks/MockableCollateral.sol b/contracts/plugins/mocks/MockableCollateral.sol index f887cef68..33cc5ab2f 100644 --- a/contracts/plugins/mocks/MockableCollateral.sol +++ b/contracts/plugins/mocks/MockableCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/aave/ATokenFiatCollateral.sol"; diff --git a/contracts/plugins/mocks/NontrivialPegCollateral.sol b/contracts/plugins/mocks/NontrivialPegCollateral.sol index 66ffd0bd7..f86816d06 100644 --- a/contracts/plugins/mocks/NontrivialPegCollateral.sol +++ b/contracts/plugins/mocks/NontrivialPegCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../assets/FiatCollateral.sol"; diff --git a/contracts/plugins/mocks/RTokenCollateral.sol b/contracts/plugins/mocks/RTokenCollateral.sol index 7c0c356c5..0e5cca0ec 100644 --- a/contracts/plugins/mocks/RTokenCollateral.sol +++ b/contracts/plugins/mocks/RTokenCollateral.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../interfaces/IMain.sol"; diff --git a/contracts/plugins/mocks/RevenueTraderBackComp.sol b/contracts/plugins/mocks/RevenueTraderBackComp.sol new file mode 100644 index 000000000..f7f8a0855 --- /dev/null +++ b/contracts/plugins/mocks/RevenueTraderBackComp.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../../p1/RevenueTrader.sol"; + +interface IRevenueTraderComp { + function manageToken(IERC20 sell) external; +} + +// RevenueTrader compatible with version 2 +contract RevenueTraderCompatibleV2 is RevenueTraderP1, IRevenueTraderComp { + function manageToken(IERC20 sell) external notTradingPausedOrFrozen { + // Mirror V3 logic (only the section relevant to tests) + this.manageToken(sell, TradeKind.DUTCH_AUCTION); + } + + function version() public pure virtual override(Versioned, IVersioned) returns (string memory) { + return "2.1.0"; + } +} + +// RevenueTrader compatible with version 1 +contract RevenueTraderCompatibleV1 is RevenueTraderCompatibleV2 { + function version() public pure override(RevenueTraderCompatibleV2) returns (string memory) { + return "1.0.0"; + } +} + +// RevenueTrader with invalid version +contract RevenueTraderInvalidVersion is RevenueTraderCompatibleV2 { + function version() public pure override(RevenueTraderCompatibleV2) returns (string memory) { + return "0.0.0"; + } +} diff --git a/contracts/plugins/mocks/RewardableERC20WrapperTest.sol b/contracts/plugins/mocks/RewardableERC20WrapperTest.sol new file mode 100644 index 000000000..a0629bc11 --- /dev/null +++ b/contracts/plugins/mocks/RewardableERC20WrapperTest.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import "../assets/erc20/RewardableERC20Wrapper.sol"; +import "./ERC20MockRewarding.sol"; + +contract RewardableERC20WrapperTest is RewardableERC20Wrapper { + constructor( + ERC20 _asset, + string memory _name, + string memory _symbol, + ERC20 _rewardToken + ) RewardableERC20Wrapper(_asset, _name, _symbol, _rewardToken) {} + + function _claimAssetRewards() internal virtual override { + ERC20MockRewarding(address(underlying)).claim(); + } +} diff --git a/contracts/plugins/mocks/RewardableERC20VaultTest.sol b/contracts/plugins/mocks/RewardableERC4626VaultTest.sol similarity index 60% rename from contracts/plugins/mocks/RewardableERC20VaultTest.sol rename to contracts/plugins/mocks/RewardableERC4626VaultTest.sol index 394fe2631..24f7b690a 100644 --- a/contracts/plugins/mocks/RewardableERC20VaultTest.sol +++ b/contracts/plugins/mocks/RewardableERC4626VaultTest.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; -import "../assets/vaults/RewardableERC20Vault.sol"; +import "../assets/erc20/RewardableERC4626Vault.sol"; import "./ERC20MockRewarding.sol"; -contract RewardableERC20VaultTest is RewardableERC20Vault { +contract RewardableERC4626VaultTest is RewardableERC4626Vault { constructor( ERC20 _asset, string memory _name, string memory _symbol, ERC20 _rewardToken - ) RewardableERC20Vault(_asset, _name, _symbol, _rewardToken) {} + ) RewardableERC4626Vault(_asset, _name, _symbol, _rewardToken) {} function _claimAssetRewards() internal virtual override { ERC20MockRewarding(asset()).claim(); diff --git a/contracts/plugins/mocks/SelfdestructTransferMock.sol b/contracts/plugins/mocks/SelfdestructTransferMock.sol index 94953222d..b6820aabc 100644 --- a/contracts/plugins/mocks/SelfdestructTransferMock.sol +++ b/contracts/plugins/mocks/SelfdestructTransferMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; contract SelfdestructTransfer { function destroyAndTransfer(address payable to) external payable { diff --git a/contracts/plugins/mocks/SfraxEthMock.sol b/contracts/plugins/mocks/SfraxEthMock.sol index b59d9ee0a..bde9e9e45 100644 --- a/contracts/plugins/mocks/SfraxEthMock.sol +++ b/contracts/plugins/mocks/SfraxEthMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/USDCMock.sol b/contracts/plugins/mocks/USDCMock.sol index 2ead8acb6..b1096a200 100644 --- a/contracts/plugins/mocks/USDCMock.sol +++ b/contracts/plugins/mocks/USDCMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/UnpricedPlugins.sol b/contracts/plugins/mocks/UnpricedPlugins.sol index 55dc369e1..f3d580876 100644 --- a/contracts/plugins/mocks/UnpricedPlugins.sol +++ b/contracts/plugins/mocks/UnpricedPlugins.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/plugins/mocks/WBTCMock.sol b/contracts/plugins/mocks/WBTCMock.sol index b3781a589..0d5bf09c1 100644 --- a/contracts/plugins/mocks/WBTCMock.sol +++ b/contracts/plugins/mocks/WBTCMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/WETH.sol b/contracts/plugins/mocks/WETH.sol index d3d5a0467..89e04c6ff 100644 --- a/contracts/plugins/mocks/WETH.sol +++ b/contracts/plugins/mocks/WETH.sol @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -pragma solidity 0.8.17; +pragma solidity 0.8.19; /// https://github.com/gnosis/canonical-weth at commit 0dd1ea3e295eef916d0c6223ec63141137d22d67 @@ -42,9 +42,9 @@ contract WETH9 { function withdraw(uint256 wad) public { require(balanceOf[msg.sender] >= wad); balanceOf[msg.sender] -= wad; - (bool success, ) = address(msg.sender).call{value: wad}(""); + (bool success, ) = address(msg.sender).call{ value: wad }(""); if (!success) { - revert("transfer failed"); + revert("transfer failed"); } emit Withdrawal(msg.sender, wad); } diff --git a/contracts/plugins/mocks/ZeroDecimalMock.sol b/contracts/plugins/mocks/ZeroDecimalMock.sol index d36654762..70cb2a4c5 100644 --- a/contracts/plugins/mocks/ZeroDecimalMock.sol +++ b/contracts/plugins/mocks/ZeroDecimalMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "./ERC20Mock.sol"; diff --git a/contracts/plugins/mocks/upgrades/AssetRegistryV2.sol b/contracts/plugins/mocks/upgrades/AssetRegistryV2.sol index 6de8e2900..652498ae3 100644 --- a/contracts/plugins/mocks/upgrades/AssetRegistryV2.sol +++ b/contracts/plugins/mocks/upgrades/AssetRegistryV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/AssetRegistry.sol"; diff --git a/contracts/plugins/mocks/upgrades/BackingManagerV2.sol b/contracts/plugins/mocks/upgrades/BackingManagerV2.sol index 7c14bb95a..9724bf541 100644 --- a/contracts/plugins/mocks/upgrades/BackingManagerV2.sol +++ b/contracts/plugins/mocks/upgrades/BackingManagerV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/BackingManager.sol"; diff --git a/contracts/plugins/mocks/upgrades/BasketHandlerV2.sol b/contracts/plugins/mocks/upgrades/BasketHandlerV2.sol index 7f63750dd..905eaab0d 100644 --- a/contracts/plugins/mocks/upgrades/BasketHandlerV2.sol +++ b/contracts/plugins/mocks/upgrades/BasketHandlerV2.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/BasketHandler.sol"; +/// @custom:oz-upgrades-unsafe-allow external-library-linking contract BasketHandlerP1V2 is BasketHandlerP1 { uint256 public newValue; diff --git a/contracts/plugins/mocks/upgrades/BrokerV2.sol b/contracts/plugins/mocks/upgrades/BrokerV2.sol index 231c8787b..fa6c04313 100644 --- a/contracts/plugins/mocks/upgrades/BrokerV2.sol +++ b/contracts/plugins/mocks/upgrades/BrokerV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/Broker.sol"; diff --git a/contracts/plugins/mocks/upgrades/DistributorV2.sol b/contracts/plugins/mocks/upgrades/DistributorV2.sol index 9c43361da..e6bcdd7fb 100644 --- a/contracts/plugins/mocks/upgrades/DistributorV2.sol +++ b/contracts/plugins/mocks/upgrades/DistributorV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/Distributor.sol"; diff --git a/contracts/plugins/mocks/upgrades/FurnaceV2.sol b/contracts/plugins/mocks/upgrades/FurnaceV2.sol index a596b16f0..145d0b4a2 100644 --- a/contracts/plugins/mocks/upgrades/FurnaceV2.sol +++ b/contracts/plugins/mocks/upgrades/FurnaceV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/Furnace.sol"; diff --git a/contracts/plugins/mocks/upgrades/MainV2.sol b/contracts/plugins/mocks/upgrades/MainV2.sol index 3ed96ce4f..afdcd1ff8 100644 --- a/contracts/plugins/mocks/upgrades/MainV2.sol +++ b/contracts/plugins/mocks/upgrades/MainV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/Main.sol"; diff --git a/contracts/plugins/mocks/upgrades/RTokenV2.sol b/contracts/plugins/mocks/upgrades/RTokenV2.sol index 01a6f3e46..0e6bae3e1 100644 --- a/contracts/plugins/mocks/upgrades/RTokenV2.sol +++ b/contracts/plugins/mocks/upgrades/RTokenV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/RToken.sol"; diff --git a/contracts/plugins/mocks/upgrades/RevenueTraderV2.sol b/contracts/plugins/mocks/upgrades/RevenueTraderV2.sol index 40bfd4786..f7c62d828 100644 --- a/contracts/plugins/mocks/upgrades/RevenueTraderV2.sol +++ b/contracts/plugins/mocks/upgrades/RevenueTraderV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/RevenueTrader.sol"; diff --git a/contracts/plugins/mocks/upgrades/StRSRV2.sol b/contracts/plugins/mocks/upgrades/StRSRV2.sol index c774cf60d..e8209f68e 100644 --- a/contracts/plugins/mocks/upgrades/StRSRV2.sol +++ b/contracts/plugins/mocks/upgrades/StRSRV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "../../../p1/StRSRVotes.sol"; diff --git a/contracts/plugins/trading/DutchTrade.sol b/contracts/plugins/trading/DutchTrade.sol index 3260d05c3..6c8f90fc7 100644 --- a/contracts/plugins/trading/DutchTrade.sol +++ b/contracts/plugins/trading/DutchTrade.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../libraries/Fixed.sol"; import "../../interfaces/IAsset.sol"; import "../../interfaces/ITrade.sol"; +import "../../mixins/NetworkConfigLib.sol"; uint192 constant FORTY_PERCENT = 4e17; // {1} 0.4 uint192 constant SIXTY_PERCENT = 6e17; // {1} 0.6 @@ -44,6 +45,9 @@ contract DutchTrade is ITrade { TradeKind public constant KIND = TradeKind.DUTCH_AUCTION; + // solhint-disable-next-line var-name-mixedcase + uint48 public immutable ONE_BLOCK; // {s} 1 block based on network + TradeStatus public status; // reentrancy protection ITrading public origin; // the address that initialized the contract @@ -54,7 +58,7 @@ contract DutchTrade is ITrade { uint192 public sellAmount; // {sellTok} // The auction runs from [startTime, endTime], inclusive - uint48 public startTime; // {s} when the dutch auction begins (12s after init()) + uint48 public startTime; // {s} when the dutch auction begins (one block after init()) uint48 public endTime; // {s} when the dutch auction ends if no bids are received // highPrice is always 1000x the middlePrice, so we don't need to track it explicitly @@ -90,6 +94,10 @@ contract DutchTrade is ITrade { return sellAmount.mul(price, CEIL).shiftl_toUint(int8(buy.decimals()), CEIL); } + constructor() { + ONE_BLOCK = NetworkConfigLib.blocktime(); + } + // === External === /// @param origin_ The Trader that originated the trade diff --git a/contracts/plugins/trading/GnosisTrade.sol b/contracts/plugins/trading/GnosisTrade.sol index 56bc5c559..eb93b62a9 100644 --- a/contracts/plugins/trading/GnosisTrade.sol +++ b/contracts/plugins/trading/GnosisTrade.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BlueOak-1.0.0 -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; diff --git a/contracts/vendor/ERC20PermitUpgradeable.sol b/contracts/vendor/ERC20PermitUpgradeable.sol index c1f471c1e..b31022a77 100644 --- a/contracts/vendor/ERC20PermitUpgradeable.sol +++ b/contracts/vendor/ERC20PermitUpgradeable.sol @@ -3,7 +3,7 @@ // The only modification that has been made is in the body of the `permit` function at line 83, /// where we failover to SignatureChecker in order to handle approvals for smart contracts. -pragma solidity 0.8.17; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-IERC20PermitUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; diff --git a/docs/dev-env.md b/docs/dev-env.md index 4fa9d6b91..1744e0e24 100644 --- a/docs/dev-env.md +++ b/docs/dev-env.md @@ -19,6 +19,7 @@ These instructions assume you already have standard installations of `node`, `np ## Setup +### Basic Dependencies Set up yarn and hardhat, needed for compiling and running tests: ```bash @@ -39,15 +40,25 @@ yarn prepare cp .env.example .env ``` +### Tenderly +If you are going to use a Tenderly network, do the following: +1. Install the [tenderly cli](https://github.com/Tenderly/tenderly-cli) +2. Login +```bash +tenderly login --authentication-method access-key --access-key {your_access_key} --force +``` +3. Configure the `TENDERLY_RPC_URL` in your `.env` file + +### Slither You should also setup `slither`. The [Trail of Bits tools][tob-suite] require solc-select. Check [the installation instructions](https://github.com/crytic/solc-select) to ensure you have all prerequisites. Then: ```bash # Install solc-select and slither pip3 install solc-select slither-analyzer -# Install and use solc version 0.8.17 -solc-select install 0.8.17 -solc-select use 0.8.17 +# Install and use solc version 0.8.19 +solc-select install 0.8.19 +solc-select use 0.8.19 # Double-check that your slither version is at least 0.8.3! hash -r && slither --version diff --git a/docs/solidity-style.md b/docs/solidity-style.md index 4c8be4c18..37309aeef 100644 --- a/docs/solidity-style.md +++ b/docs/solidity-style.md @@ -48,7 +48,7 @@ We're using 192 bits instead of the full 256 bits because it makes typical multi Initial versions of this code were written using the custom type `Fix` everywhere, and `Fixed` contained the line `type Fix is int192`. We found later that: - We had essentially no need for negative `Fix` values, so spending a storage bit on sign, and juggling the possibility of negative values, cost extra gas and harmed the clarity of our code. -- While `solc 0.8.17` allows custom types without any issue, practically all of the other tools we want to use on our Solidity source -- `slither`, `prettier`, `solhint` -- would fail when encountering substantial code using a custom type. +- While `solc 0.8.19` allows custom types without any issue, practically all of the other tools we want to use on our Solidity source -- `slither`, `prettier`, `solhint` -- would fail when encountering substantial code using a custom type. Reintroducing this custom type should be mostly mechanicanizable, but now that P1 contains a handful of hotspot optimizations that do raw arithmetic internally to eliminate Fixlib calls, it won't be trivial to do so. Still, if and when those tools achieve adequate support for custom types, we will probably do this conversion ourselves, if only to ensure that conversions between the Fix and integer interpretations of uints are carefully type-checked. diff --git a/hardhat.config.ts b/hardhat.config.ts index 0989a4ffa..08cf7e3f7 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,6 +8,7 @@ import '@typechain/hardhat' import 'hardhat-contract-sizer' import 'hardhat-gas-reporter' import 'solidity-coverage' +import * as tenderly from '@tenderly/hardhat-tenderly' import { useEnv } from '#/utils/env' import { HardhatUserConfig } from 'hardhat/types' @@ -16,6 +17,8 @@ import forkBlockNumber from '#/test/integration/fork-block-numbers' // eslint-disable-next-line node/no-missing-require require('#/tasks') +tenderly.setup() + const MAINNET_RPC_URL = useEnv(['MAINNET_RPC_URL', 'ALCHEMY_MAINNET_RPC_URL']) const TENDERLY_RPC_URL = useEnv('TENDERLY_RPC_URL') const GOERLI_RPC_URL = useEnv('GOERLI_RPC_URL') @@ -80,13 +83,13 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: '0.8.17', + version: '0.8.19', settings, }, { version: '0.6.12', settings, - } + }, ], overrides: { 'contracts/plugins/assets/convex/vendor/ConvexStakingWrapper.sol': { @@ -94,7 +97,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, runs: 1 } }, // contract over-size }, 'contracts/facade/FacadeRead.sol': { - version: '0.8.17', + version: '0.8.19', settings: { optimizer: { enabled: true, runs: 1 } }, // contract over-size }, }, @@ -121,6 +124,12 @@ const config: HardhatUserConfig = { etherscan: { apiKey: useEnv('ETHERSCAN_API_KEY'), }, + tenderly: { + // see https://github.com/Tenderly/hardhat-tenderly/tree/master/packages/tenderly-hardhat for details + username: 'Reserveslug', // org name + project: 'testnet', // project name + privateVerification: false, // must be false to verify contracts on a testnet or devnet + }, } if (useEnv('ONLY_FAST')) { diff --git a/package.json b/package.json index 1294fb5a5..731d7178d 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,16 @@ "deploy:run": "hardhat run scripts/deploy.ts", "deploy:confirm": "hardhat run scripts/confirm.ts", "deploy:verify_etherscan": "hardhat run scripts/verify_etherscan.ts", - "test:extreme": "EXTREME=1 PROTO_IMPL=1 npx hardhat test test/{Furnace,RToken,ZTradingExtremes,ZZStRSR}.test.ts", + "test:extreme": "EXTREME=1 PROTO_IMPL=1 npx hardhat test test/{Furnace,RTokenExtremes,ZTradingExtremes,ZZStRSR}.test.ts", "test:extreme:integration": "FORK=1 EXTREME=1 PROTO_IMPL=1 npx hardhat test test/integration/**/*.test.ts", "test:unit": "yarn test:plugins && yarn test:p0 && yarn test:p1", "test:fast": "bash tools/fast-test.sh", "test:p0": "PROTO_IMPL=0 hardhat test test/*.test.ts", "test:p1": "PROTO_IMPL=1 hardhat test test/*.test.ts", - "test:plugins": "hardhat test test/{libraries,plugins}/*.test.ts --parallel", + "test:plugins": "hardhat test test/{libraries,plugins}/*.test.ts", "test:plugins:integration": "PROTO_IMPL=1 FORK=1 hardhat test test/plugins/individual-collateral/**/*.test.ts", "test:integration": "PROTO_IMPL=1 FORK=1 hardhat test test/integration/**/*.test.ts", - "test:scenario": "PROTO_IMPL=1 hardhat test test/scenario/*.test.ts --parallel", + "test:scenario": "PROTO_IMPL=1 hardhat test test/scenario/*.test.ts", "test:gas": "yarn test:gas:protocol && yarn test:gas:integration", "test:gas:protocol": "REPORT_GAS=1 PROTO_IMPL=1 hardhat test test/{libraries,plugins,scenario}/*.test.ts test/*.test.ts", "test:gas:integration": "FORK=1 REPORT_GAS=1 PROTO_IMPL=1 hardhat test test/integration/**/*.test.ts", @@ -59,6 +59,7 @@ "@openzeppelin/contracts": "~4.7.3", "@openzeppelin/contracts-upgradeable": "~4.7.3", "@openzeppelin/hardhat-upgrades": "^1.23.0", + "@tenderly/hardhat-tenderly": "^1.7.7", "@typechain/ethers-v5": "^7.2.0", "@typechain/hardhat": "^2.3.1", "@types/chai": "^4.3.0", diff --git a/scripts/addresses/5-RTKN-tmp-deployments.json b/scripts/addresses/5-RTKN-tmp-deployments.json new file mode 100644 index 000000000..1e94d56ff --- /dev/null +++ b/scripts/addresses/5-RTKN-tmp-deployments.json @@ -0,0 +1,35 @@ +{ + "prerequisites": { + "RSR": "0xB58b5530332D2E9e15bfd1f2525E6fD84e830307", + "RSR_FEED": "0x905084691C2c7505b5FC63229621621b616bbbFe", + "GNOSIS_EASY_AUCTION": "0x1fbab40c338e2e7243da945820ba680c92ef8281" + }, + "tradingLib": "0xde1075de2e665d8B37Ae9941fa132574a0E6FcC3", + "cvxMiningLib": "0x073F98792ef4c00bB5f11B1F64f13cB25Cde0d8D", + "facadeRead": "0x62bf08e255706f3855821B2C25007a731D585E59", + "facadeAct": "0x7Bcb39F6d2A902aF8adFe384Ec6D84ABE66D2065", + "facadeWriteLib": "0xF6FB14EDD2c6FA038C3D19bC04238793369e6d6B", + "basketLib": "0x1c21E28F6cd7C4Be734cb60f9c6451484803924d", + "facadeWrite": "0x264Fb85EF99cb2026de73ef0f6f74AFd6335a006", + "deployer": "0x7bdAbdA24406A293f230690Ad5305173d266B7d6", + "rsrAsset": "0x5DeCeA2E6146058EB0FBBd770245284Ce756E068", + "implementations": { + "main": "0x15395aCCbF8c6b28671fe41624D599624709a2D6", + "trading": { + "gnosisTrade": "0xD971Fd59e90E836eCF2b8adE76374102025084A1", + "dutchTrade": "0x0EEa20c426EcE7D3dA5b73946bb1626697aA7c59" + }, + "components": { + "assetRegistry": "0xe981820A4Dd0d5168beb73F57E6dc827420D066C", + "backingManager": "0xbe7B053E820c5FBe70a0f075DA0C931aD8816e4F", + "basketHandler": "0xA7eCF508CdF5a88ae93b899DE4fcACcB43112Ce8", + "broker": "0xF4aB456F9FBA39b91F46c54185C218E4c32B014e", + "distributor": "0xb120c3429900DDF665b34882d7685e39BB01897B", + "furnace": "0xa570BF93FC51406809dBf52aB898913541C91C20", + "rsrTrader": "0x4c5FA27d1785d37B7Ac0942121f95370e61ff6a4", + "rTokenTrader": "0x4c5FA27d1785d37B7Ac0942121f95370e61ff6a4", + "rToken": "0xd0cb758e918ac6973a2959343ECa4F333d8d25B1", + "stRSR": "0xeC12e8412a7AE4598d754f4016D487c269719856" + } + } +} \ No newline at end of file diff --git a/scripts/addresses/5-tmp-assets-collateral.json b/scripts/addresses/5-tmp-assets-collateral.json new file mode 100644 index 000000000..a60fdaf59 --- /dev/null +++ b/scripts/addresses/5-tmp-assets-collateral.json @@ -0,0 +1,56 @@ +{ + "assets": { + "stkAAVE": "0x7B025f359eB0490bB9bC52B755B8A45AC40676B9", + "COMP": "0x882BBbD5dd09DD77c15f89fE8B50fE48b7765835" + }, + "collateral": { + "DAI": "0x18Be705B263f79884c60Ffb9E8c8C266F755BC7E", + "USDC": "0x70AE8A09E298e146eC6A21248fA295026B705892", + "USDT": "0x8Ce9bE066cD946c32e9fC524D6202F361FDBc5Fd", + "USDP": "0x668137e1d86AECE8dd9F53a8c9F7299445C44911", + "TUSD": "0x1aAF257Cc251557404939F8D67eA1f07849BF02B", + "BUSD": "0x225Af68a32059395DAE3C73DDc4F73fdD4de6769", + "aDAI": "0x9a0915952Abe643f1207A54eb73Ec05dD4854238", + "aUSDC": "0x6C5576bf258FEa59A603D1fdAddEB50B22Eecdb1", + "aUSDT": "0x44b955165E01A6d3a3566CDd522596CC49e64366", + "aBUSD": "0x35341C7768cd8F5081802BFe5aa17f4f54695387", + "aUSDP": "0xA74B16e291965E8b98Aa0929988a679D74c81A7D", + "cDAI": "0xc771dA3be582d6a2CB133d3F4937dEBf190Ede0A", + "cUSDC": "0x7622D205b88e5fB7030E5D717ade7202592B31ab", + "cUSDT": "0xf37adF141BD754e9C9E645de88bB28B5e4a6Db96", + "cUSDP": "0xEB74EC1d4C1DAB412D5d6674F6833FD19d3118Ce", + "cWBTC": "0xa43a653E10fB098579f3E5ccAF32EB5722aB9c11", + "cETH": "0x97f25E9A946ecCD2864C95E26888aA6ADa66CA82", + "WBTC": "0xDB5b8cead52f77De0f6B5255f73F348AAf2CBb8D", + "WETH": "0xe7Cb991c70DEa9E7054aD16cDEdFe39134d95bC9", + "EURT": "0x9B8B71c644f97dcD53C8910133134924d1ece945", + "rETH": "0x4E6f2A55FcC83027c4AF86a93028730DCB2FD739", + "wstETH": "0xD5A780C6cB1c826d9831d7D52b66463df917F245" + }, + "erc20s": { + "stkAAVE": "0x3Db8b170DA19c45B63B959789f20f397F22767D4", + "COMP": "0x1b4449895037f25b102B28B45b8bD50c8C44Aca1", + "DAI": "0x4E35fAA0c4e6BA16534aa28DE0e40f7b702642D3", + "USDC": "0x9276fC221399d81a848E9d543a6FAA5e741E40A7", + "USDT": "0xAE64954A904da3fD9D71945980A849B8A9F755d7", + "USDP": "0x5d3E908ff0649F01d51d1513132736e96477C15d", + "TUSD": "0x56e938BC973fB23aCd7f043Fc11b61b1Ae3DDcC5", + "BUSD": "0x66FE0f43D9f201474A54a3857c77599DEBbD38F4", + "aDAI": "0x4e326FB29FBdDE0a7333A216c452E20999f9440B", + "aUSDC": "0xa29cc69De0F52eE1880616eB9a7Bd566a1bbc071", + "aUSDT": "0x510A90e2195c64d703E5E0959086cd1b7F9109ca", + "aBUSD": "0xD860Bb21773085a664c5C603B72518E1Cb8873Fc", + "aUSDP": "0x0774dF07205a5E9261771b19afa62B6e757f7eF8", + "cDAI": "0x00eD6351d8d3014609c10a68389c1dF474B66262", + "cUSDC": "0x4dd96ebadc4899aA815620eB38EdF2D7e28a1011", + "cUSDT": "0x30B17D09f5E79BB9F21Db2ec63115415AD4C9069", + "cUSDP": "0x75b3E91D55FDBC3A1FBAAE839B4f6B778287f50D", + "cWBTC": "0xCfD19C2791A444ea46016eBdB27143753C864D8A", + "cETH": "0xad6A7850Ac386Aa2139d1e3B6A960cD4952AcFb7", + "WBTC": "0x528FdEd7CC39209ed67B4edA11937A9ABe1f6249", + "WETH": "0xB5B58F0a853132EA8cB614cb17095dE87AF3E98b", + "EURT": "0xD6da5A7ADE2a906d9992612752A339E3485dB508", + "rETH": "0x178E141a0E3b34152f73Ff610437A7bf9B83267A", + "wstETH": "0x6320cD32aA674d2898A68ec82e869385Fc5f7E2f" + } +} \ No newline at end of file diff --git a/scripts/addresses/5-tmp-deployments.json b/scripts/addresses/5-tmp-deployments.json index eeafe862b..9e37b1f97 100644 --- a/scripts/addresses/5-tmp-deployments.json +++ b/scripts/addresses/5-tmp-deployments.json @@ -4,32 +4,32 @@ "RSR_FEED": "0x905084691C2c7505b5FC63229621621b616bbbFe", "GNOSIS_EASY_AUCTION": "0x1fbab40c338e2e7243da945820ba680c92ef8281" }, - "tradingLib": "0x3BECE5EC596331033726E5C6C188c313Ff4E3fE5", - "cvxMiningLib": "0x890FAa00C16EAD6AA76F18A1A7fe9C40838F9122", - "facadeRead": "0xe8461dB45A7430AA7aB40346E68821284980FdFD", - "facadeAct": "0x103BAFaB86f37C407f2C4813ee343020b66b062f", - "facadeWriteLib": "0x1bc463270b8d3D797F59Fe639eDF5ae130f35FF3", - "basketLib": "0x5A4f2FfC4aD066152B344Ceb2fc2275275b1a9C7", - "facadeWrite": "0xdEBe74dc2A415e00bE8B4b9d1e6e0007153D006a", - "deployer": "0x14c443d8BdbE9A65F3a23FA4e199d8741D5B38Fa", - "rsrAsset": "0xC87CDFFD680D57BF50De4C364BF4277B8A90098E", + "tradingLib": "0xde1075de2e665d8B37Ae9941fa132574a0E6FcC3", + "cvxMiningLib": "0x073F98792ef4c00bB5f11B1F64f13cB25Cde0d8D", + "facadeRead": "0x62bf08e255706f3855821B2C25007a731D585E59", + "facadeAct": "0x7Bcb39F6d2A902aF8adFe384Ec6D84ABE66D2065", + "facadeWriteLib": "0xF6FB14EDD2c6FA038C3D19bC04238793369e6d6B", + "basketLib": "0x1c21E28F6cd7C4Be734cb60f9c6451484803924d", + "facadeWrite": "0x264Fb85EF99cb2026de73ef0f6f74AFd6335a006", + "deployer": "0x7bdAbdA24406A293f230690Ad5305173d266B7d6", + "rsrAsset": "0x5DeCeA2E6146058EB0FBBd770245284Ce756E068", "implementations": { - "main": "0x63be601cDE1121C987B885cc319b44e0a9d707a2", + "main": "0x15395aCCbF8c6b28671fe41624D599624709a2D6", "trading": { - "gnosisTrade": "0x9FF9c353136e86EFe02ADD177E7c9769f8a5A77F", - "dutchTrade": "0xc9291eF2f81dBc9B412381aBe83b28954220565E" + "gnosisTrade": "0xD971Fd59e90E836eCF2b8adE76374102025084A1", + "dutchTrade": "0x0EEa20c426EcE7D3dA5b73946bb1626697aA7c59" }, "components": { - "assetRegistry": "0xCBE084C44e7A2223F76362Dcc4EbDacA5Fb1cbA7", - "backingManager": "0xAdfB9BCdA981136c83076a52Ef8fE4D8B2b520e7", - "basketHandler": "0xC9c37FC53682207844B058026024853A9C0b8c7B", - "broker": "0x8Af118a89c5023Bb2B03C70f70c8B396aE71963D", - "distributor": "0xD31eEc6679Dd18D5D42A92F32f01Ed98d4e91941", - "furnace": "0xFdb9F465C56933ab91f341C966DB517f975de5c1", - "rsrTrader": "0x18a26902126154437322fe01fBa04A36b093906f", - "rTokenTrader": "0x18a26902126154437322fe01fBa04A36b093906f", - "rToken": "0x0240E29Be6cBbB178543fF27EA4AaC8F8b870b44", - "stRSR": "0x27F672aAf061cb0b2640a4DFCCBd799cD1a7309A" + "assetRegistry": "0xe981820A4Dd0d5168beb73F57E6dc827420D066C", + "backingManager": "0xbe7B053E820c5FBe70a0f075DA0C931aD8816e4F", + "basketHandler": "0xA7eCF508CdF5a88ae93b899DE4fcACcB43112Ce8", + "broker": "0xF4aB456F9FBA39b91F46c54185C218E4c32B014e", + "distributor": "0xb120c3429900DDF665b34882d7685e39BB01897B", + "furnace": "0xa570BF93FC51406809dBf52aB898913541C91C20", + "rsrTrader": "0x4c5FA27d1785d37B7Ac0942121f95370e61ff6a4", + "rTokenTrader": "0x4c5FA27d1785d37B7Ac0942121f95370e61ff6a4", + "rToken": "0xd0cb758e918ac6973a2959343ECa4F333d8d25B1", + "stRSR": "0xeC12e8412a7AE4598d754f4016D487c269719856" } } -} \ No newline at end of file +} diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 978d6cd9b..39739b1c5 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -45,9 +45,15 @@ async function main() { 'phase2-assets/collaterals/deploy_flux_finance_collateral.ts', 'phase2-assets/collaterals/deploy_ctokenv3_usdc_collateral.ts', 'phase2-assets/collaterals/deploy_convex_stable_plugin.ts', - 'phase2-assets/collaterals/deploy_convex_volatile_plugin.ts', + // 'phase2-assets/collaterals/deploy_convex_volatile_plugin.ts', // tricrypto on hold 'phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts', 'phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts', + 'phase2-assets/collaterals/deploy_curve_stable_plugin.ts', + // 'phase2-assets/collaterals/deploy_curve_volatile_plugin.ts', // tricrypto on hold + 'phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts', + 'phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts', + 'phase2-assets/collaterals/deploy_dsr_sdai.ts', + 'phase2-assets/collaterals/deploy_cbeth_collateral.ts', // =============================================== // These phase3 scripts will not deploy functional RTokens or Governance. They deploy bricked // versions that are used for verification only. Further deployment is left up to the Register. diff --git a/scripts/deployment/common.ts b/scripts/deployment/common.ts index 6e2a6b4f2..a67b13191 100644 --- a/scripts/deployment/common.ts +++ b/scripts/deployment/common.ts @@ -1,5 +1,5 @@ import fs from 'fs' -import { ITokens, IComponents, IImplementations, IPlugins } from '../../common/configuration' +import { ITokens, IComponents, IImplementations, IPools } from '../../common/configuration' // This file is intended to have minimal imports, so that it can be used from tasks if necessary @@ -25,8 +25,8 @@ export interface IDeployments { export interface IAssetCollDeployments { assets: ITokens - collateral: ITokens & IPlugins - erc20s: ITokens & IPlugins + collateral: ITokens & IPools + erc20s: ITokens & IPools } export interface IRTokenDeployments { diff --git a/scripts/deployment/phase1-common/2_deploy_implementations.ts b/scripts/deployment/phase1-common/2_deploy_implementations.ts index f023e1f78..eff5eac9f 100644 --- a/scripts/deployment/phase1-common/2_deploy_implementations.ts +++ b/scripts/deployment/phase1-common/2_deploy_implementations.ts @@ -105,7 +105,7 @@ async function main() { if (!upgrade) { mainImplAddr = await upgrades.deployImplementation(MainImplFactory, { kind: 'uups', - }) + }) as string } else { mainImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.main, @@ -113,7 +113,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } mainImpl = await ethers.getContractAt('MainP1', mainImplAddr) @@ -158,7 +158,7 @@ async function main() { if (!upgrade) { assetRegImplAddr = await upgrades.deployImplementation(AssetRegImplFactory, { kind: 'uups', - }) + }) as string } else { assetRegImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.assetRegistry, @@ -166,7 +166,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } assetRegImpl = await ethers.getContractAt('AssetRegistryP1', assetRegImplAddr) @@ -191,7 +191,7 @@ async function main() { backingMgrImplAddr = await upgrades.deployImplementation(BackingMgrImplFactory, { kind: 'uups', unsafeAllow: ['external-library-linking', 'delegatecall'], - }) + }) as string } else { backingMgrImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.backingManager, @@ -200,7 +200,7 @@ async function main() { kind: 'uups', unsafeAllow: ['external-library-linking', 'delegatecall'], } - ) + ) as string } backingMgrImpl = ( @@ -225,7 +225,7 @@ async function main() { bskHndlrImplAddr = await upgrades.deployImplementation(BskHandlerImplFactory, { kind: 'uups', unsafeAllow: ['external-library-linking'], - }) + }) as string } else { bskHndlrImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.basketHandler, @@ -234,7 +234,7 @@ async function main() { kind: 'uups', unsafeAllow: ['external-library-linking'], } - ) + ) as string } bskHndlrImpl = await ethers.getContractAt('BasketHandlerP1', bskHndlrImplAddr) @@ -254,7 +254,7 @@ async function main() { if (!upgrade) { brokerImplAddr = await upgrades.deployImplementation(BrokerImplFactory, { kind: 'uups', - }) + }) as string } else { brokerImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.broker, @@ -262,7 +262,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } brokerImpl = await ethers.getContractAt('BrokerP1', brokerImplAddr) @@ -282,7 +282,7 @@ async function main() { if (!upgrade) { distribImplAddr = await upgrades.deployImplementation(DistribImplFactory, { kind: 'uups', - }) + }) as string } else { distribImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.distributor, @@ -290,7 +290,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } distribImpl = await ethers.getContractAt('DistributorP1', distribImplAddr) @@ -310,7 +310,7 @@ async function main() { if (!upgrade) { furnaceImplAddr = await upgrades.deployImplementation(FurnaceImplFactory, { kind: 'uups', - }) + }) as string } else { furnaceImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.furnace, @@ -318,7 +318,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } furnaceImpl = await ethers.getContractAt('FurnaceP1', furnaceImplAddr) @@ -341,7 +341,7 @@ async function main() { rsrTraderImplAddr = await upgrades.deployImplementation(RevTraderImplFactory, { kind: 'uups', unsafeAllow: ['delegatecall'], - }) + }) as string rTokenTraderImplAddr = rsrTraderImplAddr // Both equal in initial deployment } else { // RSR Trader @@ -352,7 +352,7 @@ async function main() { kind: 'uups', unsafeAllow: ['delegatecall'], } - ) + ) as string // If Traders have different implementations, upgrade separately if ( @@ -367,7 +367,7 @@ async function main() { kind: 'uups', unsafeAllow: ['delegatecall'], } - ) + ) as string } else { // Both use the same implementation rTokenTraderImplAddr = rsrTraderImplAddr @@ -402,7 +402,7 @@ async function main() { if (!upgrade) { rTokenImplAddr = await upgrades.deployImplementation(RTokenImplFactory, { kind: 'uups', - }) + }) as string } else { rTokenImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.rToken, @@ -410,7 +410,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } rTokenImpl = await ethers.getContractAt('RTokenP1', rTokenImplAddr) @@ -431,7 +431,7 @@ async function main() { if (!upgrade) { stRSRImplAddr = await upgrades.deployImplementation(StRSRImplFactory, { kind: 'uups', - }) + }) as string } else { stRSRImplAddr = await upgrades.prepareUpgrade( prevDeployments.implementations.components.stRSR, @@ -439,7 +439,7 @@ async function main() { { kind: 'uups', } - ) + ) as string } stRSRImpl = await ethers.getContractAt('StRSRP1Votes', stRSRImplAddr) diff --git a/scripts/deployment/phase2-assets/2_deploy_collateral.ts b/scripts/deployment/phase2-assets/2_deploy_collateral.ts index c121fbf2a..475dd1d5c 100644 --- a/scripts/deployment/phase2-assets/2_deploy_collateral.ts +++ b/scripts/deployment/phase2-assets/2_deploy_collateral.ts @@ -390,7 +390,7 @@ async function main() { fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) /******** Deploy CToken Fiat Collateral - cDAI **************************/ - const CTokenFactory = await ethers.getContractFactory('CTokenVault') + const CTokenFactory = await ethers.getContractFactory('CTokenWrapper') const cDai = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.cDAI!) const cDaiVault = await CTokenFactory.deploy( diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts new file mode 100644 index 000000000..a3a562d7f --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_cbeth_collateral.ts @@ -0,0 +1,84 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { priceTimeout, oracleTimeout, combinedError } from '../../utils' +import { CBEthCollateral__factory } from '../../../../typechain' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Coinbase ETH Collateral - CBETH **************************/ + + const CBETHCollateralFactory: CBEthCollateral__factory = (await hre.ethers.getContractFactory( + 'CBEthCollateral' + )) as CBEthCollateral__factory + + const collateral = await CBETHCollateralFactory.connect(deployer).deploy( + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH!, + oracleError: combinedError(fp('0.005'), fp('0.02')).toString(), // 0.5% & 2%, + erc20: networkConfig[chainId].tokens.cbETH!, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + targetName: hre.ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0.15').toString(), // 15% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-4').toString(), // revenueHiding = 0.01% + networkConfig[chainId].chainlinkFeeds.cbETH!, // refPerTokChainlinkFeed + oracleTimeout(chainId, '86400').toString() // refPerTokChainlinkTimeout + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log(`Deployed Coinbase cbETH to ${hre.network.name} (${chainId}): ${collateral.address}`) + + assetCollDeployments.collateral.cbETH = collateral.address + assetCollDeployments.erc20s.cbETH = networkConfig[chainId].tokens.cbETH + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts index 36eef8c21..e66b89dcd 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_rToken_metapool_plugin.ts @@ -13,7 +13,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { CvxStableRTokenMetapoolCollateral } from '../../../../typechain' +import { CurveStableRTokenMetapoolCollateral } from '../../../../typechain' import { revenueHiding, oracleTimeout } from '../../utils' import { CurvePoolType, @@ -31,7 +31,7 @@ import { USDC_ORACLE_ERROR, USDC_ORACLE_TIMEOUT, USDC_USD_FEED, -} from '../../../../test/plugins/individual-collateral/convex/constants' +} from '../../../../test/plugins/individual-collateral/curve/constants' // This file specifically deploys Convex RToken Metapool Plugin for eUSD/fraxBP @@ -41,7 +41,7 @@ async function main() { const chainId = await getChainId(hre) - console.log(`Deploying CvxStableRTokenMetapoolCollateral to network ${hre.network.name} (${chainId}) + console.log(`Deploying CurveStableRTokenMetapoolCollateral to network ${hre.network.name} (${chainId}) with burner account: ${deployer.address}`) if (!networkConfig[chainId]) { @@ -64,8 +64,8 @@ async function main() { /******** Deploy Convex Stable Metapool for eUSD/fraxBP **************************/ const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) - const CvxStableCollateralFactory = await hre.ethers.getContractFactory( - 'CvxStableRTokenMetapoolCollateral' + const CurveStableCollateralFactory = await hre.ethers.getContractFactory( + 'CurveStableRTokenMetapoolCollateral' ) const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { libraries: { CvxMining: CvxMining.address }, @@ -79,35 +79,35 @@ async function main() { `Deployed wrapper for Convex eUSD/FRAX Metapool on ${hre.network.name} (${chainId}): ${wPool.address} ` ) - const collateral = await CvxStableCollateralFactory.connect( - deployer - ).deploy( - { - erc20: wPool.address, - targetName: ethers.utils.formatBytes32String('USD'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - oracleTimeout: bn('1'), // unused but cannot be zero - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold - delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, - }, - revenueHiding.toString(), - { - nTokens: 2, - curvePool: FRAX_BP, - poolType: CurvePoolType.Plain, - feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], - oracleTimeouts: [ - [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], - [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], - ], - oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], - lpToken: FRAX_BP_TOKEN, - }, - eUSD_FRAX_BP, - DEFAULT_THRESHOLD // 2% + const collateral = ( + await CurveStableCollateralFactory.connect(deployer).deploy( + { + erc20: wPool.address, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold + delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 2, + curvePool: FRAX_BP, + poolType: CurvePoolType.Plain, + feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + ], + oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], + lpToken: FRAX_BP_TOKEN, + }, + eUSD_FRAX_BP, + DEFAULT_THRESHOLD // 2% + ) ) await collateral.deployed() await (await collateral.refresh()).wait() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts index 7eaf5dc89..72d0f7deb 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_metapool_plugin.ts @@ -12,7 +12,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { CvxStableMetapoolCollateral } from '../../../../typechain' +import { CurveStableMetapoolCollateral } from '../../../../typechain' import { revenueHiding, oracleTimeout } from '../../utils' import { CurvePoolType, @@ -37,7 +37,7 @@ import { USDT_ORACLE_ERROR, USDT_ORACLE_TIMEOUT, USDT_USD_FEED, -} from '../../../../test/plugins/individual-collateral/convex/constants' +} from '../../../../test/plugins/individual-collateral/curve/constants' // This file specifically deploys Convex Metapool Plugin for MIM/3Pool @@ -47,7 +47,7 @@ async function main() { const chainId = await getChainId(hre) - console.log(`Deploying CvxStableMetapoolCollateral to network ${hre.network.name} (${chainId}) + console.log(`Deploying CurveStableMetapoolCollateral to network ${hre.network.name} (${chainId}) with burner account: ${deployer.address}`) if (!networkConfig[chainId]) { @@ -70,8 +70,8 @@ async function main() { /******** Deploy Convex Stable Metapool for MIM/3Pool **************************/ const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) - const CvxStableCollateralFactory = await hre.ethers.getContractFactory( - 'CvxStableMetapoolCollateral' + const CurveStableCollateralFactory = await hre.ethers.getContractFactory( + 'CurveStableMetapoolCollateral' ) const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { libraries: { CvxMining: CvxMining.address }, @@ -85,7 +85,7 @@ async function main() { `Deployed wrapper for Convex Stable MIM/3Pool on ${hre.network.name} (${chainId}): ${wPool.address} ` ) - const collateral = await CvxStableCollateralFactory.connect( + const collateral = await CurveStableCollateralFactory.connect( deployer ).deploy( { diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts index 106003a22..cc3b31856 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_stable_plugin.ts @@ -13,7 +13,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { CvxStableCollateral } from '../../../../typechain' +import { CurveStableCollateral } from '../../../../typechain' import { revenueHiding, oracleTimeout } from '../../utils' import { CurvePoolType, @@ -33,7 +33,7 @@ import { USDT_ORACLE_ERROR, USDT_ORACLE_TIMEOUT, USDT_USD_FEED, -} from '../../../../test/plugins/individual-collateral/convex/constants' +} from '../../../../test/plugins/individual-collateral/curve/constants' // This file specifically deploys Convex Stable Plugin for 3pool @@ -66,7 +66,7 @@ async function main() { /******** Deploy Convex Stable Pool for 3pool **************************/ const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) - const CvxStableCollateralFactory = await hre.ethers.getContractFactory('CvxStableCollateral') + const CurveStableCollateralFactory = await hre.ethers.getContractFactory('CurveStableCollateral') const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { libraries: { CvxMining: CvxMining.address }, }) @@ -79,7 +79,9 @@ async function main() { `Deployed wrapper for Convex Stable 3Pool on ${hre.network.name} (${chainId}): ${w3Pool.address} ` ) - const collateral = await CvxStableCollateralFactory.connect(deployer).deploy( + const collateral = await CurveStableCollateralFactory.connect( + deployer + ).deploy( { erc20: w3Pool.address, targetName: ethers.utils.formatBytes32String('USD'), diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts index f0c7b7332..71ff38d29 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_convex_volatile_plugin.ts @@ -13,7 +13,7 @@ import { getDeploymentFilename, fileExists, } from '../../common' -import { CvxVolatileCollateral } from '../../../../typechain' +import { CurveVolatileCollateral } from '../../../../typechain' import { revenueHiding, oracleTimeout } from '../../utils' import { CurvePoolType, @@ -36,7 +36,7 @@ import { USDT_ORACLE_ERROR, USDT_ORACLE_TIMEOUT, USDT_USD_FEED, -} from '../../../../test/plugins/individual-collateral/convex/constants' +} from '../../../../test/plugins/individual-collateral/curve/constants' // This file specifically deploys Convex Volatile Plugin for Tricrypto @@ -69,7 +69,9 @@ async function main() { /******** Deploy Convex Volatile Pool for 3pool **************************/ const CvxMining = await ethers.getContractAt('CvxMining', deployments.cvxMiningLib) - const CvxVolatileCollateralFactory = await hre.ethers.getContractFactory('CvxVolatileCollateral') + const CurveVolatileCollateralFactory = await hre.ethers.getContractFactory( + 'CurveVolatileCollateral' + ) const ConvexStakingWrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { libraries: { CvxMining: CvxMining.address }, }) @@ -82,7 +84,7 @@ async function main() { `Deployed wrapper for Convex Volatile TriCrypto on ${hre.network.name} (${chainId}): ${w3Pool.address} ` ) - const collateral = await CvxVolatileCollateralFactory.connect( + const collateral = await CurveVolatileCollateralFactory.connect( deployer ).deploy( { diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts new file mode 100644 index 000000000..a52f03d7c --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_rToken_metapool_plugin.ts @@ -0,0 +1,135 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus, ONE_ADDRESS } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { CurveStableRTokenMetapoolCollateral } from '../../../../typechain' +import { revenueHiding, oracleTimeout } from '../../utils' +import { + CRV, + CurvePoolType, + DEFAULT_THRESHOLD, + eUSD_FRAX_BP, + eUSD_GAUGE, + FRAX_BP, + FRAX_BP_TOKEN, + FRAX_ORACLE_ERROR, + FRAX_ORACLE_TIMEOUT, + FRAX_USD_FEED, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + RTOKEN_DELAY_UNTIL_DEFAULT, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, +} from '../../../../test/plugins/individual-collateral/curve/constants' + +// Deploy Curve RToken Metapool Plugin for eUSD/fraxBP + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying CurveStableRTokenMetapoolCollateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Curve Stable Metapool for eUSD/fraxBP **************************/ + + const CurveStableCollateralFactory = await hre.ethers.getContractFactory( + 'CurveStableRTokenMetapoolCollateral' + ) + const CurveStakingWrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + + const wPool = await CurveStakingWrapperFactory.deploy( + eUSD_FRAX_BP, + 'Wrapped Curve eUSD+FRAX/USDC', + 'weUSDFRAXBP', + CRV, + eUSD_GAUGE + ) + await wPool.deployed() + + console.log( + `Deployed wrapper for Curve eUSD/FRAX Metapool on ${hre.network.name} (${chainId}): ${wPool.address} ` + ) + + const collateral = ( + await CurveStableCollateralFactory.connect(deployer).deploy( + { + erc20: wPool.address, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold + delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 2, + curvePool: FRAX_BP, + poolType: CurvePoolType.Plain, + feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + ], + oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], + lpToken: FRAX_BP_TOKEN, + }, + eUSD_FRAX_BP, + DEFAULT_THRESHOLD // 2% + ) + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + `Deployed Curve RToken Metapool Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + + assetCollDeployments.collateral.crveUSDFRAXBP = collateral.address + assetCollDeployments.erc20s.crveUSDFRAXBP = wPool.address + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts new file mode 100644 index 000000000..f97882d21 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_metapool_plugin.ts @@ -0,0 +1,141 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { CurveStableMetapoolCollateral } from '../../../../typechain' +import { revenueHiding, oracleTimeout } from '../../utils' +import { + CRV, + CurvePoolType, + DELAY_UNTIL_DEFAULT, + MIM_DEFAULT_THRESHOLD, + MIM_THREE_POOL, + MIM_THREE_POOL_GAUGE, + MIM_USD_FEED, + MIM_ORACLE_ERROR, + MIM_ORACLE_TIMEOUT, + MAX_TRADE_VOL, + THREE_POOL_DEFAULT_THRESHOLD, + THREE_POOL, + THREE_POOL_TOKEN, + PRICE_TIMEOUT, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, + DAI_ORACLE_ERROR, + DAI_ORACLE_TIMEOUT, + DAI_USD_FEED, + USDT_ORACLE_ERROR, + USDT_ORACLE_TIMEOUT, + USDT_USD_FEED, +} from '../../../../test/plugins/individual-collateral/curve/constants' + +// Deploy Curve Metapool Plugin for MIM/3Pool + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying CurveStableMetapoolCollateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Curve Stable Metapool for MIM/3Pool **************************/ + + const CurveStableCollateralFactory = await hre.ethers.getContractFactory( + 'CurveStableMetapoolCollateral' + ) + const CurveStakingWrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const wPool = await CurveStakingWrapperFactory.deploy( + MIM_THREE_POOL, + 'Wrapped Curve MIM+3Pool', + 'wMIM3CRV', + CRV, + MIM_THREE_POOL_GAUGE + ) + await wPool.deployed() + + console.log( + `Deployed wrapper for Curve Stable MIM/3Pool on ${hre.network.name} (${chainId}): ${wPool.address} ` + ) + + const collateral = await CurveStableCollateralFactory.connect( + deployer + ).deploy( + { + erc20: wPool.address, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: MIM_USD_FEED, + oracleError: MIM_ORACLE_ERROR, + oracleTimeout: MIM_ORACLE_TIMEOUT, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: THREE_POOL_DEFAULT_THRESHOLD, // 1.25% + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 3, + curvePool: THREE_POOL, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], + ], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], + lpToken: THREE_POOL_TOKEN, + }, + MIM_THREE_POOL, + MIM_DEFAULT_THRESHOLD + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + `Deployed Curve Metapool Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + + assetCollDeployments.collateral.crvMIM3Pool = collateral.address + assetCollDeployments.erc20s.crvMIM3Pool = wPool.address + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts new file mode 100644 index 000000000..8aba2f5aa --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_stable_plugin.ts @@ -0,0 +1,134 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus, ONE_ADDRESS } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { CurveStableCollateral } from '../../../../typechain' +import { revenueHiding, oracleTimeout } from '../../utils' +import { + CRV, + CurvePoolType, + DAI_ORACLE_ERROR, + DAI_ORACLE_TIMEOUT, + DAI_USD_FEED, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + THREE_POOL, + THREE_POOL_TOKEN, + THREE_POOL_GAUGE, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, + USDT_ORACLE_ERROR, + USDT_ORACLE_TIMEOUT, + USDT_USD_FEED, +} from '../../../../test/plugins/individual-collateral/curve/constants' + +// Deploy Curve Stable Plugin for 3pool + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Curve Stable Pool for 3pool **************************/ + + const CurveStableCollateralFactory = await hre.ethers.getContractFactory('CurveStableCollateral') + const CurveGaugeWrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + + const w3Pool = await CurveGaugeWrapperFactory.deploy( + THREE_POOL_TOKEN, + 'Wrapped Staked Curve.fi DAI/USDC/USDT', + 'ws3Crv', + CRV, + THREE_POOL_GAUGE + ) + await w3Pool.deployed() + + console.log( + `Deployed wrapper for Curve Stable 3Pool on ${hre.network.name} (${chainId}): ${w3Pool.address} ` + ) + + const collateral = await CurveStableCollateralFactory.connect( + deployer + ).deploy( + { + erc20: w3Pool.address, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 3, + curvePool: THREE_POOL, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], + ], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], + lpToken: THREE_POOL_TOKEN, + } + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + `Deployed Curve Stable Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + + assetCollDeployments.collateral.cvx3Pool = collateral.address + assetCollDeployments.erc20s.crv3Pool = w3Pool.address + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts b/scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts new file mode 100644 index 000000000..938c9e168 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_curve_volatile_plugin.ts @@ -0,0 +1,142 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus, ONE_ADDRESS } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { CurveVolatileCollateral } from '../../../../typechain' +import { revenueHiding, oracleTimeout } from '../../utils' +import { + CRV, + CurvePoolType, + BTC_USD_ORACLE_ERROR, + BTC_ORACLE_TIMEOUT, + BTC_USD_FEED, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + TRI_CRYPTO, + TRI_CRYPTO_TOKEN, + TRI_CRYPTO_GAUGE, + WBTC_BTC_ORACLE_ERROR, + WETH_ORACLE_TIMEOUT, + WBTC_BTC_FEED, + WBTC_ORACLE_TIMEOUT, + WETH_USD_FEED, + WETH_ORACLE_ERROR, + USDT_ORACLE_ERROR, + USDT_ORACLE_TIMEOUT, + USDT_USD_FEED, +} from '../../../../test/plugins/individual-collateral/curve/constants' + +// Deploy Curve Volatile Plugin for Tricrypto + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy Curve Volatile Pool for 3pool **************************/ + + const CurveVolatileCollateralFactory = await hre.ethers.getContractFactory( + 'CurveVolatileCollateral' + ) + const CurveStakingWrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const w3Pool = await CurveStakingWrapperFactory.deploy( + TRI_CRYPTO_TOKEN, + 'Wrapped Curve.fi USD-BTC-ETH', + 'wcrv3crypto', + CRV, + TRI_CRYPTO_GAUGE + ) + await w3Pool.deployed() + + console.log( + `Deployed wrapper for Curve Volatile TriCrypto on ${hre.network.name} (${chainId}): ${w3Pool.address} ` + ) + + const collateral = await CurveVolatileCollateralFactory.connect( + deployer + ).deploy( + { + erc20: w3Pool.address, + targetName: ethers.utils.formatBytes32String('TRICRYPTO'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 3, + curvePool: TRI_CRYPTO, + poolType: CurvePoolType.Plain, + feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, WBTC_ORACLE_TIMEOUT), oracleTimeout(chainId, BTC_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, WETH_ORACLE_TIMEOUT)], + ], + oracleErrors: [ + [USDT_ORACLE_ERROR], + [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], + [WETH_ORACLE_ERROR], + ], + lpToken: TRI_CRYPTO_TOKEN, + } + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + `Deployed Curve Volatile Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + + assetCollDeployments.collateral.crvTriCrypto = collateral.address + assetCollDeployments.erc20s.crvTriCrypto = w3Pool.address + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts b/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts new file mode 100644 index 000000000..c3fa6bbe4 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_dsr_sdai.ts @@ -0,0 +1,87 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { bn, fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { POT } from '../../../../test/plugins/individual-collateral/dsr/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { priceTimeout, oracleTimeout } from '../../utils' +import { SDaiCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy SDAI Collateral - sDAI **************************/ + + const SDaiCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'SDaiCollateral' + ) + + const collateral = await SDaiCollateralFactory.connect(deployer).deploy( + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI, + oracleError: fp('0.0025').toString(), // 0.25% + erc20: networkConfig[chainId].tokens.sDAI, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.0125').toString(), // 1.25% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6').toString(), // revenueHiding = 0.0001% + POT + ) + await collateral.deployed() + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + console.log( + `Deployed DSR-wrapping sDAI to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + + assetCollDeployments.collateral.sDAI = collateral.address + assetCollDeployments.erc20s.sDAI = networkConfig[chainId].tokens.sDAI + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts index f18688918..0aeb08cef 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_flux_finance_collateral.ts @@ -41,7 +41,7 @@ async function main() { const deployedCollateral: string[] = [] /******** Deploy FToken Fiat Collateral - fUSDC **************************/ - const FTokenFactory = await ethers.getContractFactory('CTokenVault') + const FTokenFactory = await ethers.getContractFactory('CTokenWrapper') const fUsdc = await ethers.getContractAt('IERC20Metadata', networkConfig[chainId].tokens.fUSDC!) const fUsdcVault = await FTokenFactory.deploy( diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts index 4f85427eb..7381cf92e 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_lido_wsteth_collateral.ts @@ -40,6 +40,30 @@ async function main() { const deployedCollateral: string[] = [] + const deployedOracle: string[] = [] + + /******** Deploy Mock Oracle (if needed) **************************/ + let stethUsdOracleAddress: string = networkConfig[chainId].chainlinkFeeds.stETHUSD! + let stethEthOracleAddress: string = networkConfig[chainId].chainlinkFeeds.stETHETH! + if (chainId == 5) { + const MockOracleFactory = await hre.ethers.getContractFactory('MockV3Aggregator') + const mockStethUsdOracle = await MockOracleFactory.connect(deployer).deploy(8, bn(2000e8)) + await mockStethUsdOracle.deployed() + console.log( + `Deployed MockV3Aggregator on ${hre.network.name} (${chainId}): ${mockStethUsdOracle.address} ` + ) + deployedOracle.push(mockStethUsdOracle.address) + stethUsdOracleAddress = mockStethUsdOracle.address + + const mockStethEthOracle = await MockOracleFactory.connect(deployer).deploy(8, bn(1e8)) + await mockStethEthOracle.deployed() + console.log( + `Deployed MockV3Aggregator on ${hre.network.name} (${chainId}): ${mockStethEthOracle.address} ` + ) + deployedOracle.push(mockStethEthOracle.address) + stethEthOracleAddress = mockStethEthOracle.address + } + /******** Deploy Lido Staked ETH Collateral - wstETH **************************/ const LidoStakedEthCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( @@ -51,7 +75,7 @@ async function main() { ).deploy( { priceTimeout: priceTimeout.toString(), - chainlinkFeed: networkConfig[chainId].chainlinkFeeds.stETHUSD, + chainlinkFeed: stethUsdOracleAddress, oracleError: fp('0.01').toString(), // 1%: only for stETHUSD feed erc20: networkConfig[chainId].tokens.wstETH, maxTradeVolume: fp('1e6').toString(), // $1m, @@ -61,7 +85,7 @@ async function main() { delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4').toString(), // revenueHiding = 0.01% - networkConfig[chainId].chainlinkFeeds.stETHETH, // targetPerRefChainlinkFeed + stethEthOracleAddress, // targetPerRefChainlinkFeed oracleTimeout(chainId, '86400').toString() // targetPerRefChainlinkTimeout ) await collateral.deployed() diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts b/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts index cc8a2e4cd..386985881 100644 --- a/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts +++ b/scripts/deployment/phase2-assets/collaterals/deploy_rocket_pool_reth_collateral.ts @@ -13,7 +13,7 @@ import { fileExists, } from '../../common' import { priceTimeout, oracleTimeout, combinedError } from '../../utils' -import { RethCollateral } from '../../../../typechain' +import { MockV3Aggregator, RethCollateral } from '../../../../typechain' import { ContractFactory } from 'ethers' async function main() { @@ -40,6 +40,21 @@ async function main() { const deployedCollateral: string[] = [] + const deployedOracle: string[] = [] + + /******** Deploy Mock Oracle (if needed) **************************/ + let rethOracleAddress: string = networkConfig[chainId].chainlinkFeeds.rETH! + if (chainId == 5) { + const MockOracleFactory = await hre.ethers.getContractFactory('MockV3Aggregator') + const mockOracle = await MockOracleFactory.connect(deployer).deploy(8, fp(2000)) + await mockOracle.deployed() + console.log( + `Deployed MockV3Aggregator on ${hre.network.name} (${chainId}): ${mockOracle.address} ` + ) + deployedOracle.push(mockOracle.address) + rethOracleAddress = mockOracle.address + } + /******** Deploy Rocket Pool ETH Collateral - rETH **************************/ const RethCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( @@ -59,7 +74,7 @@ async function main() { delayUntilDefault: bn('86400').toString(), // 24h }, fp('1e-4').toString(), // revenueHiding = 0.01% - networkConfig[chainId].chainlinkFeeds.rETH, // refPerTokChainlinkFeed + rethOracleAddress, // refPerTokChainlinkFeed oracleTimeout(chainId, '86400').toString() // refPerTokChainlinkTimeout ) await collateral.deployed() diff --git a/scripts/deployment/utils.ts b/scripts/deployment/utils.ts index e5450ae9b..8b39f6a2e 100644 --- a/scripts/deployment/utils.ts +++ b/scripts/deployment/utils.ts @@ -1,4 +1,4 @@ -import hre from 'hardhat' +import hre, { tenderly } from 'hardhat' import * as readline from 'readline' import axios from 'axios' import { exec } from 'child_process' @@ -106,42 +106,50 @@ export async function verifyContract( console.time(`Verifying ${contract}`) console.log(`Verifying ${contract}`) - // Sleep 0.5s to not overwhelm API - await new Promise((r) => setTimeout(r, 500)) - - const ETHERSCAN_API_KEY = useEnv('ETHERSCAN_API_KEY') - - // Check to see if already verified - const url = `${getEtherscanBaseURL( - chainId, - true - )}/api/?module=contract&action=getsourcecode&address=${address}&apikey=${ETHERSCAN_API_KEY}` - const { data, status } = await axios.get(url, { headers: { Accept: 'application/json' } }) - if (status != 200 || data['status'] != '1') { - throw new Error("Can't communicate with Etherscan API") - } - - // Only run verification script if not verified - if (data['result'][0]['SourceCode']?.length > 0) { - console.log('Already verified. Continuing') + if (hre.network.name == 'tenderly') { + await tenderly.verify({ + name: contract, + address: address!, + libraries + }); } else { - console.log('Running new verification') - try { - await hre.run('verify:verify', { - address, - constructorArguments, - contract, - libraries, - }) - } catch (e) { - console.log( - `IMPORTANT: failed to verify ${contract}. - ${getEtherscanBaseURL(chainId)}/address/${address}#code`, - e - ) + // Sleep 0.5s to not overwhelm API + await new Promise((r) => setTimeout(r, 500)) + + const ETHERSCAN_API_KEY = useEnv('ETHERSCAN_API_KEY') + + // Check to see if already verified + const url = `${getEtherscanBaseURL( + chainId, + true + )}/api/?module=contract&action=getsourcecode&address=${address}&apikey=${ETHERSCAN_API_KEY}` + const { data, status } = await axios.get(url, { headers: { Accept: 'application/json' } }) + if (status != 200 || data['status'] != '1') { + throw new Error("Can't communicate with Etherscan API") + } + + // Only run verification script if not verified + if (data['result'][0]['SourceCode']?.length > 0) { + console.log('Already verified. Continuing') + } else { + console.log('Running new verification') + try { + await hre.run('verify:verify', { + address, + constructorArguments, + contract, + libraries, + }) + } catch (e) { + console.log( + `IMPORTANT: failed to verify ${contract}. + ${getEtherscanBaseURL(chainId)}/address/${address}#code`, + e + ) + } } + console.timeEnd(`Verifying ${contract}`) } - console.timeEnd(`Verifying ${contract}`) } export const getEtherscanBaseURL = (chainId: number, api = false) => { @@ -197,6 +205,7 @@ export const prompt = async (query: string): Promise => { rl.question(query, (ans) => { rl.close() resolve(ans) + return ans }) ) } else { diff --git a/scripts/exhaustive-tests/run-1.sh b/scripts/exhaustive-tests/run-1.sh index a8e0fa737..fbad597e1 100644 --- a/scripts/exhaustive-tests/run-1.sh +++ b/scripts/exhaustive-tests/run-1.sh @@ -1,3 +1,3 @@ echo "Running RToken & Furnace exhaustive tests for commit hash: " git rev-parse HEAD; -NODE_OPTIONS=--max-old-space-size=30000 EXTREME=1 SLOW=1 PROTO_IMPL=1 npx hardhat test test/RToken.test.ts test/Furnace.test.ts; +NODE_OPTIONS=--max-old-space-size=30000 EXTREME=1 SLOW=1 PROTO_IMPL=1 npx hardhat test test/RTokenExtremes.test.ts test/Furnace.test.ts; diff --git a/scripts/verification/1_verify_implementations.ts b/scripts/verification/1_verify_implementations.ts index 1e5094c1b..8d2f853f9 100644 --- a/scripts/verification/1_verify_implementations.ts +++ b/scripts/verification/1_verify_implementations.ts @@ -64,11 +64,17 @@ async function main() { name: 'backingManager', desc: 'BackingManager', contract: 'contracts/p1/BackingManager.sol:BackingManagerP1', + libraries: { + 'RecollateralizationLibP1': deployments.tradingLib, + } }, { name: 'basketHandler', desc: 'BasketHandler', contract: 'contracts/p1/BasketHandler.sol:BasketHandlerP1', + libraries: { + 'BasketLibP1': deployments.basketLib, + } }, { name: 'broker', diff --git a/scripts/verification/5_verify_facadeWrite.ts b/scripts/verification/5_verify_facadeWrite.ts index b27bcb903..e6d569ac4 100644 --- a/scripts/verification/5_verify_facadeWrite.ts +++ b/scripts/verification/5_verify_facadeWrite.ts @@ -33,7 +33,8 @@ async function main() { chainId, deployments.facadeWrite, [deployments.deployer], - 'contracts/facade/FacadeWrite.sol:FacadeWrite' + 'contracts/facade/FacadeWrite.sol:FacadeWrite', + { FacadeWriteLib: deployments.facadeWriteLib} ) } diff --git a/scripts/verification/6_verify_collateral.ts b/scripts/verification/6_verify_collateral.ts index 31be19856..347c5fd2a 100644 --- a/scripts/verification/6_verify_collateral.ts +++ b/scripts/verification/6_verify_collateral.ts @@ -15,7 +15,7 @@ import { revenueHiding, verifyContract, } from '../deployment/utils' -import { ATokenMock, ATokenFiatCollateral } from '../../typechain' +import { ATokenMock, ATokenFiatCollateral, ICToken, CTokenFiatCollateral } from '../../typechain' let deployments: IAssetCollDeployments @@ -70,7 +70,7 @@ async function main() { 'Static ' + (await aToken.name()), 's' + (await aToken.symbol()), ], - 'contracts/plugins/assets/aave/StaticATokenLM.sol:StaticATokenLM' + 'contracts/plugins/assets/aave/vendor/StaticATokenLM.sol:StaticATokenLM' ) /******** Verify ATokenFiatCollateral - aDAI **************************/ await verifyContract( @@ -92,6 +92,25 @@ async function main() { ], 'contracts/plugins/assets/aave/ATokenFiatCollateral.sol:ATokenFiatCollateral' ) + /******** Verify CTokenWrapper - cDAI **************************/ + const cToken: ICToken = ( + await ethers.getContractAt('ICToken', networkConfig[chainId].tokens.cDAI as string) + ) + const cTokenCollateral: CTokenFiatCollateral = ( + await ethers.getContractAt('CTokenFiatCollateral', deployments.collateral.cDAI as string) + ) + + await verifyContract( + chainId, + await cTokenCollateral.erc20(), + [ + cToken.address, + `${await cToken.name()} Vault`, + `${await cToken.symbol()}-VAULT`, + networkConfig[chainId].COMPTROLLER!, + ], + 'contracts/plugins/assets/compoundv2/CTokenWrapper.sol:CTokenWrapper' + ) /********************** Verify CTokenFiatCollateral - cDAI ****************************************/ await verifyContract( chainId, @@ -101,15 +120,14 @@ async function main() { priceTimeout: priceTimeout.toString(), chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI, oracleError: fp('0.0025').toString(), // 0.25% - erc20: networkConfig[chainId].tokens.cDAI, + erc20: deployments.erc20s.cDAI, maxTradeVolume: fp('1e6').toString(), // $1m, oracleTimeout: oracleTimeout(chainId, '3600').toString(), targetName: hre.ethers.utils.formatBytes32String('USD'), defaultThreshold: fp('0.0125').toString(), // 1.25% delayUntilDefault: bn('86400').toString(), // 24h }, - revenueHiding.toString(), - networkConfig[chainId].COMPTROLLER, + revenueHiding.toString() ], 'contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol:CTokenFiatCollateral' ) @@ -127,7 +145,7 @@ async function main() { priceTimeout: priceTimeout.toString(), chainlinkFeed: networkConfig[chainId].chainlinkFeeds.WBTC, oracleError: combinedBTCWBTCError.toString(), - erc20: networkConfig[chainId].tokens.cWBTC, + erc20: deployments.erc20s.cWBTC, maxTradeVolume: fp('1e6').toString(), // $1m, oracleTimeout: oracleTimeout(chainId, '86400').toString(), // 24 hr targetName: hre.ethers.utils.formatBytes32String('BTC'), @@ -136,8 +154,7 @@ async function main() { }, networkConfig[chainId].chainlinkFeeds.BTC, oracleTimeout(chainId, '3600').toString(), - revenueHiding.toString(), - networkConfig[chainId].COMPTROLLER, + revenueHiding.toString() ], 'contracts/plugins/assets/compoundv2/CTokenNonFiatCollateral.sol:CTokenNonFiatCollateral' ) @@ -150,7 +167,7 @@ async function main() { priceTimeout: priceTimeout.toString(), chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH, oracleError: fp('0.005').toString(), // 0.5% - erc20: networkConfig[chainId].tokens.cETH, + erc20: deployments.erc20s.cETH, maxTradeVolume: fp('1e6').toString(), // $1m, oracleTimeout: oracleTimeout(chainId, '3600').toString(), targetName: hre.ethers.utils.formatBytes32String('ETH'), @@ -158,8 +175,7 @@ async function main() { delayUntilDefault: '0', }, revenueHiding.toString(), - '18', - networkConfig[chainId].COMPTROLLER, + '18' ], 'contracts/plugins/assets/compoundv2/CTokenSelfReferentialCollateral.sol:CTokenSelfReferentialCollateral' ) diff --git a/scripts/verification/collateral-plugins/verify_cbeth.ts b/scripts/verification/collateral-plugins/verify_cbeth.ts new file mode 100644 index 000000000..abe6322e5 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_cbeth.ts @@ -0,0 +1,55 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp, bn } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { priceTimeout, oracleTimeout, verifyContract, combinedError } from '../../deployment/utils' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + /******** Verify Coinbase staked ETH - CBETH **************************/ + await verifyContract( + chainId, + deployments.collateral.cbETH, + [ + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH, + oracleError: combinedError(fp('0.005'), fp('0.02')).toString(), // 0.5% & 2%, + erc20: networkConfig[chainId].tokens.cbETH!, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr, + targetName: hre.ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0.15').toString(), // 15% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-4'), // revenueHiding = 0.01% + networkConfig[chainId].chainlinkFeeds.cbETH!, // refPerTokChainlinkFeed + oracleTimeout(chainId, '86400').toString(), // refPerTokChainlinkTimeout + ], + 'contracts/plugins/assets/cbeth/CBEthCollateral.sol:CBEthCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_convex_stable_plugin.ts b/scripts/verification/collateral-plugins/verify_convex_stable.ts similarity index 92% rename from scripts/verification/collateral-plugins/verify_convex_stable_plugin.ts rename to scripts/verification/collateral-plugins/verify_convex_stable.ts index fbbac7cca..4a1a4b7bb 100644 --- a/scripts/verification/collateral-plugins/verify_convex_stable_plugin.ts +++ b/scripts/verification/collateral-plugins/verify_convex_stable.ts @@ -29,7 +29,7 @@ import { USDT_ORACLE_ERROR, USDT_ORACLE_TIMEOUT, USDT_USD_FEED, -} from '../../../test/plugins/individual-collateral/convex/constants' +} from '../../../test/plugins/individual-collateral/curve/constants' let deployments: IAssetCollDeployments @@ -51,7 +51,7 @@ async function main() { deployments = getDeploymentFile(assetCollDeploymentFilename) const w3PoolCollateral = await ethers.getContractAt( - 'CvxStableCollateral', + 'CurveStableCollateral', deployments.collateral.cvx3Pool as string ) @@ -61,7 +61,8 @@ async function main() { chainId, await w3PoolCollateral.erc20(), [], - 'contracts/plugins/assets/convex/vendor/ConvexStakingWrapper.sol:ConvexStakingWrapper' + 'contracts/plugins/assets/convex/vendor/ConvexStakingWrapper.sol:ConvexStakingWrapper', + {CvxMining: coreDeployments.cvxMiningLib} ) /******** Verify CvxMining Lib **************************/ @@ -104,7 +105,7 @@ async function main() { lpToken: THREE_POOL_TOKEN, }, ], - 'contracts/plugins/assets/convex/CvxStableCollateral.sol:CvxStableCollateral' + 'contracts/plugins/assets/convex/CurveStableCollateral.sol:CurveStableCollateral' ) } diff --git a/scripts/verification/collateral-plugins/verify_convex_stable_metapool_plugin.ts b/scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts similarity index 93% rename from scripts/verification/collateral-plugins/verify_convex_stable_metapool_plugin.ts rename to scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts index d0e3b0911..417ff521c 100644 --- a/scripts/verification/collateral-plugins/verify_convex_stable_metapool_plugin.ts +++ b/scripts/verification/collateral-plugins/verify_convex_stable_metapool.ts @@ -30,7 +30,7 @@ import { USDT_ORACLE_ERROR, USDT_ORACLE_TIMEOUT, USDT_USD_FEED, -} from '../../../test/plugins/individual-collateral/convex/constants' +} from '../../../test/plugins/individual-collateral/curve/constants' let deployments: IAssetCollDeployments @@ -49,7 +49,7 @@ async function main() { deployments = getDeploymentFile(assetCollDeploymentFilename) const wPoolCollateral = await ethers.getContractAt( - 'CvxStableMetapoolCollateral', + 'CurveStableMetapoolCollateral', deployments.collateral.cvxMIM3Pool as string ) @@ -86,7 +86,7 @@ async function main() { MIM_THREE_POOL, MIM_DEFAULT_THRESHOLD, ], - 'contracts/plugins/assets/convex/CvxStableMetapoolCollateral.sol:CvxStableMetapoolCollateral' + 'contracts/plugins/assets/convex/CurveStableMetapoolCollateral.sol:CurveStableMetapoolCollateral' ) } diff --git a/scripts/verification/collateral-plugins/verify_eusd_fraxbp_collateral.ts b/scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts similarity index 92% rename from scripts/verification/collateral-plugins/verify_eusd_fraxbp_collateral.ts rename to scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts index 0f4b68a0d..00d7ae5b3 100644 --- a/scripts/verification/collateral-plugins/verify_eusd_fraxbp_collateral.ts +++ b/scripts/verification/collateral-plugins/verify_convex_stable_rtoken_metapool.ts @@ -25,7 +25,7 @@ import { USDC_ORACLE_ERROR, USDC_ORACLE_TIMEOUT, USDC_USD_FEED, -} from '../../../test/plugins/individual-collateral/convex/constants' +} from '../../../test/plugins/individual-collateral/curve/constants' let deployments: IAssetCollDeployments @@ -44,7 +44,7 @@ async function main() { deployments = getDeploymentFile(assetCollDeploymentFilename) const eUSDPlugin = await ethers.getContractAt( - 'CvxStableRTokenMetapoolCollateral', + 'CurveStableRTokenMetapoolCollateral', deployments.collateral.cvxeUSDFRAXBP as string ) @@ -80,7 +80,7 @@ async function main() { eUSD_FRAX_BP, DEFAULT_THRESHOLD, // 2% ], - 'contracts/plugins/assets/convex/CvxStableRTokenMetapoolCollateral.sol:CvxStableRTokenMetapoolCollateral' + 'contracts/plugins/assets/convex/CurveStableRTokenMetapoolCollateral.sol:CurveStableRTokenMetapoolCollateral' ) } diff --git a/scripts/verification/collateral-plugins/verify_convex_volatile_plugin.ts b/scripts/verification/collateral-plugins/verify_convex_volatile.ts similarity index 93% rename from scripts/verification/collateral-plugins/verify_convex_volatile_plugin.ts rename to scripts/verification/collateral-plugins/verify_convex_volatile.ts index d1255deed..8c48da0e5 100644 --- a/scripts/verification/collateral-plugins/verify_convex_volatile_plugin.ts +++ b/scripts/verification/collateral-plugins/verify_convex_volatile.ts @@ -30,7 +30,7 @@ import { USDT_ORACLE_ERROR, USDT_ORACLE_TIMEOUT, USDT_USD_FEED, -} from '../../../test/plugins/individual-collateral/convex/constants' +} from '../../../test/plugins/individual-collateral/curve/constants' let deployments: IAssetCollDeployments @@ -49,7 +49,7 @@ async function main() { deployments = getDeploymentFile(assetCollDeploymentFilename) const wTriCrypto = await ethers.getContractAt( - 'CvxVolatileCollateral', + 'CurveVolatileCollateral', deployments.collateral.cvxTriCrypto as string ) @@ -88,7 +88,7 @@ async function main() { lpToken: TRI_CRYPTO_TOKEN, }, ], - 'contracts/plugins/assets/convex/CvxVolatileCollateral.sol:CvxVolatileCollateral' + 'contracts/plugins/assets/convex/CurveVolatileCollateral.sol:CurveVolatileCollateral' ) } diff --git a/scripts/verification/collateral-plugins/verify_curve_stable.ts b/scripts/verification/collateral-plugins/verify_curve_stable.ts new file mode 100644 index 000000000..e43317350 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_curve_stable.ts @@ -0,0 +1,102 @@ +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { bn } from '../../../common/numbers' +import { ONE_ADDRESS } from '../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { + CRV, + CurvePoolType, + DAI_ORACLE_ERROR, + DAI_ORACLE_TIMEOUT, + DAI_USD_FEED, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + THREE_POOL, + THREE_POOL_TOKEN, + THREE_POOL_GAUGE, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, + USDT_ORACLE_ERROR, + USDT_ORACLE_TIMEOUT, + USDT_USD_FEED, +} from '../../../test/plugins/individual-collateral/curve/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + const w3PoolCollateral = await ethers.getContractAt( + 'CurveStableCollateral', + deployments.collateral.crv3Pool as string + ) + + /******** Verify CurveGaugeWrapper **************************/ + + await verifyContract( + chainId, + await w3PoolCollateral.erc20(), + [THREE_POOL_TOKEN, 'Wrapped Staked Curve.fi DAI/USDC/USDT', 'ws3Crv', CRV, THREE_POOL_GAUGE], + 'contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol:CurveGaugeWrapper' + ) + + /******** Verify 3Pool plugin **************************/ + await verifyContract( + chainId, + deployments.collateral.crv3Pool, + [ + { + erc20: await w3PoolCollateral.erc20(), + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 3, + curvePool: THREE_POOL, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], + ], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], + lpToken: THREE_POOL_TOKEN, + }, + ], + 'contracts/plugins/assets/curve/CurveStableCollateral.sol:CurveStableCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts b/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts new file mode 100644 index 000000000..60be29f1e --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_curve_stable_metapool.ts @@ -0,0 +1,96 @@ +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { + CurvePoolType, + DAI_ORACLE_ERROR, + DAI_ORACLE_TIMEOUT, + DAI_USD_FEED, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + MIM_DEFAULT_THRESHOLD, + MIM_USD_FEED, + MIM_ORACLE_ERROR, + MIM_ORACLE_TIMEOUT, + MIM_THREE_POOL, + PRICE_TIMEOUT, + THREE_POOL_DEFAULT_THRESHOLD, + THREE_POOL, + THREE_POOL_TOKEN, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, + USDT_ORACLE_ERROR, + USDT_ORACLE_TIMEOUT, + USDT_USD_FEED, +} from '../../../test/plugins/individual-collateral/curve/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + const wPoolCollateral = await ethers.getContractAt( + 'CurveStableMetapoolCollateral', + deployments.collateral.crvMIM3Pool as string + ) + + /******** Verify Curve MIM/3Pool plugin **************************/ + await verifyContract( + chainId, + deployments.collateral.crvMIM3Pool, + [ + { + erc20: await wPoolCollateral.erc20(), + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: MIM_USD_FEED, + oracleError: MIM_ORACLE_ERROR, + oracleTimeout: MIM_ORACLE_TIMEOUT, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: THREE_POOL_DEFAULT_THRESHOLD, // 1.25% + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 3, + curvePool: THREE_POOL, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, DAI_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], + ], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], + lpToken: THREE_POOL_TOKEN, + }, + MIM_THREE_POOL, + MIM_DEFAULT_THRESHOLD, + ], + 'contracts/plugins/assets/convex/CurveStableMetapoolCollateral.sol:CurveStableMetapoolCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts b/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts new file mode 100644 index 000000000..45101d93f --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_curve_stable_rtoken_metapool.ts @@ -0,0 +1,90 @@ +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { bn } from '../../../common/numbers' +import { ONE_ADDRESS } from '../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { + CurvePoolType, + DEFAULT_THRESHOLD, + eUSD_FRAX_BP, + FRAX_BP, + FRAX_BP_TOKEN, + FRAX_ORACLE_ERROR, + FRAX_ORACLE_TIMEOUT, + FRAX_USD_FEED, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + RTOKEN_DELAY_UNTIL_DEFAULT, + USDC_ORACLE_ERROR, + USDC_ORACLE_TIMEOUT, + USDC_USD_FEED, +} from '../../../test/plugins/individual-collateral/curve/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + const eUSDPlugin = await ethers.getContractAt( + 'CurveStableRTokenMetapoolCollateral', + deployments.collateral.crveUSDFRAXBP as string + ) + + /******** Verify eUSD/fraxBP plugin **************************/ + await verifyContract( + chainId, + deployments.collateral.crveUSDFRAXBP, + [ + { + erc20: await eUSDPlugin.erc20(), + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, // 2%: 1% error on FRAX oracle + 1% base defaultThreshold + delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 2, + curvePool: FRAX_BP, + poolType: CurvePoolType.Plain, + feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, FRAX_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, USDC_ORACLE_TIMEOUT)], + ], + oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], + lpToken: FRAX_BP_TOKEN, + }, + eUSD_FRAX_BP, + DEFAULT_THRESHOLD, // 2% + ], + 'contracts/plugins/assets/convex/CurveStableRTokenMetapoolCollateral.sol:CurveStableRTokenMetapoolCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_curve_volatile.ts b/scripts/verification/collateral-plugins/verify_curve_volatile.ts new file mode 100644 index 000000000..2f5c53b2c --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_curve_volatile.ts @@ -0,0 +1,98 @@ +import hre, { ethers } from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { bn } from '../../../common/numbers' +import { ONE_ADDRESS } from '../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { revenueHiding, oracleTimeout } from '../../deployment/utils' +import { + CurvePoolType, + BTC_USD_ORACLE_ERROR, + BTC_ORACLE_TIMEOUT, + BTC_USD_FEED, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + MAX_TRADE_VOL, + PRICE_TIMEOUT, + TRI_CRYPTO, + TRI_CRYPTO_TOKEN, + WBTC_BTC_ORACLE_ERROR, + WETH_ORACLE_TIMEOUT, + WBTC_BTC_FEED, + WBTC_ORACLE_TIMEOUT, + WETH_USD_FEED, + WETH_ORACLE_ERROR, + USDT_ORACLE_ERROR, + USDT_ORACLE_TIMEOUT, + USDT_USD_FEED, +} from '../../../test/plugins/individual-collateral/curve/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + const wTriCrypto = await ethers.getContractAt( + 'CurveVolatileCollateral', + deployments.collateral.crvTriCrypto as string + ) + + /******** Verify TriCrypto plugin **************************/ + await verifyContract( + chainId, + deployments.collateral.crvTriCrypto, + [ + { + erc20: await wTriCrypto.erc20(), + targetName: ethers.utils.formatBytes32String('TRICRYPTO'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }, + revenueHiding.toString(), + { + nTokens: 3, + curvePool: TRI_CRYPTO, + poolType: CurvePoolType.Plain, + feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], + oracleTimeouts: [ + [oracleTimeout(chainId, USDT_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, WBTC_ORACLE_TIMEOUT), oracleTimeout(chainId, BTC_ORACLE_TIMEOUT)], + [oracleTimeout(chainId, WETH_ORACLE_TIMEOUT)], + ], + oracleErrors: [ + [USDT_ORACLE_ERROR], + [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], + [WETH_ORACLE_ERROR], + ], + lpToken: TRI_CRYPTO_TOKEN, + }, + ], + 'contracts/plugins/assets/convex/CurveVolatileCollateral.sol:CurveVolatileCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_cusdcv3_collateral.ts b/scripts/verification/collateral-plugins/verify_cusdcv3.ts similarity index 100% rename from scripts/verification/collateral-plugins/verify_cusdcv3_collateral.ts rename to scripts/verification/collateral-plugins/verify_cusdcv3.ts diff --git a/scripts/verification/collateral-plugins/verify_reth_collateral.ts b/scripts/verification/collateral-plugins/verify_reth.ts similarity index 100% rename from scripts/verification/collateral-plugins/verify_reth_collateral.ts rename to scripts/verification/collateral-plugins/verify_reth.ts diff --git a/scripts/verification/collateral-plugins/verify_sdai.ts b/scripts/verification/collateral-plugins/verify_sdai.ts new file mode 100644 index 000000000..e5d9290c3 --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_sdai.ts @@ -0,0 +1,55 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp, bn } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { priceTimeout, oracleTimeout, verifyContract } from '../../deployment/utils' +import { POT } from '../../../test/plugins/individual-collateral/dsr/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + /******** Verify sDAI **************************/ + await verifyContract( + chainId, + deployments.collateral.sDAI, + [ + { + priceTimeout: priceTimeout.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI, + oracleError: fp('0.0025').toString(), // 0.25% + erc20: networkConfig[chainId].tokens.sDAI, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: oracleTimeout(chainId, '3600').toString(), // 1 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.0125').toString(), // 1.25% + delayUntilDefault: bn('86400').toString(), // 24h + }, + fp('1e-6').toString(), // revenueHiding = 0.0001% + POT, + ], + 'contracts/plugins/assets/dsr/SDaiCollateral.sol:SDaiCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_wsteth_collateral.ts b/scripts/verification/collateral-plugins/verify_wsteth.ts similarity index 100% rename from scripts/verification/collateral-plugins/verify_wsteth_collateral.ts rename to scripts/verification/collateral-plugins/verify_wsteth.ts diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index 3d10e2a98..857d72645 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -45,13 +45,18 @@ async function main() { '6_verify_collateral.ts', '7_verify_rToken.ts', '8_verify_governance.ts', - 'collateral-plugins/verify_convex_stable_plugin.ts', - 'collateral-plugins/verify_convex_stable_metapool_plugin.ts', - 'collateral-plugins/verify_convex_volatile_plugin.ts', - 'collateral-plugins/verify_eusd_fraxbp_collateral.ts', - 'collateral-plugins/verify_cusdcv3_collateral.ts', - 'collateral-plugins/verify_reth_collateral.ts', - 'collateral-plugins/verify_wsteth_collateral.ts', + 'collateral-plugins/verify_convex_stable.ts', + 'collateral-plugins/verify_convex_stable_metapool.ts', + 'collateral-plugins/verify_convex_volatile.ts', + 'collateral-plugins/verify_convex_stable_rtoken_metapool.ts', + 'collateral-plugins/verify_curve_stable.ts', + 'collateral-plugins/verify_curve_stable_metapool.ts', + 'collateral-plugins/verify_curve_volatile.ts', + 'collateral-plugins/verify_curve_stable_rtoken_metapool.ts', + 'collateral-plugins/verify_cusdcv3.ts', + 'collateral-plugins/verify_reth.ts', + 'collateral-plugins/verify_wsteth.ts', + 'collateral-plugins/verify_cbeth.ts', ] for (const script of scripts) { diff --git a/test/Broker.test.ts b/test/Broker.test.ts index 1f788f3fb..a0eccff66 100644 --- a/test/Broker.test.ts +++ b/test/Broker.test.ts @@ -10,9 +10,11 @@ import { bn, fp, divCeil, toBNDecimals } from '../common/numbers' import { DutchTrade, ERC20Mock, + FiatCollateral, GnosisMock, GnosisMockReentrant, GnosisTrade, + IAssetRegistry, TestIBackingManager, TestIBroker, TestIMain, @@ -27,12 +29,19 @@ import { defaultFixture, Implementation, IMPLEMENTATION, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, } from './fixtures' import snapshotGasCost from './utils/snapshotGasCost' +import { setOraclePrice } from './utils/oracles' import { advanceTime, advanceToTimestamp, getLatestBlockTimestamp } from './utils/time' import { ITradeRequest } from './utils/trades' import { useEnv } from '#/utils/env' +const DEFAULT_THRESHOLD = fp('0.01') // 1% +const DELAY_UNTIL_DEFAULT = bn('86400') // 24h + const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip @@ -59,6 +68,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // Main contracts let main: TestIMain + let assetRegistry: IAssetRegistry let backingManager: TestIBackingManager let rsrTrader: TestIRevenueTrader let rTokenTrader: TestIRevenueTrader @@ -73,6 +83,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { basket, config, main, + assetRegistry, backingManager, broker, gnosis, @@ -972,6 +983,43 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { ).to.be.revertedWith('Invalid trade state') }) + it('Should not initialize DutchTrade with bad prices', async () => { + // Fund trade + await token0.connect(owner).mint(trade.address, amount) + + // Set bad price for sell token + await setOraclePrice(collateral0.address, bn(0)) + await collateral0.refresh() + + // Attempt to initialize with bad sell price + await expect( + trade.init( + backingManager.address, + collateral0.address, + collateral1.address, + amount, + config.dutchAuctionLength + ) + ).to.be.revertedWith('bad sell pricing') + + // Fix sell price, set bad buy price + await setOraclePrice(collateral0.address, bn(1e8)) + await collateral0.refresh() + + await setOraclePrice(collateral1.address, bn(0)) + await collateral1.refresh() + + await expect( + trade.init( + backingManager.address, + collateral0.address, + collateral1.address, + amount, + config.dutchAuctionLength + ) + ).to.be.revertedWith('bad buy pricing') + }) + it('Should apply full maxTradeSlippage to lowPrice at minTradeVolume', async () => { amount = config.minTradeVolume @@ -998,6 +1046,48 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await trade.lowPrice()).to.be.closeTo(withSlippage, withSlippage.div(bn('1e9'))) }) + it('Should apply full maxTradeSlippage with low maxTradeVolume', async () => { + // Set low maxTradeVolume for collateral + const FiatCollateralFactory = await ethers.getContractFactory('FiatCollateral') + const newCollateral0: FiatCollateral = await FiatCollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: await collateral0.chainlinkFeed(), + oracleError: ORACLE_ERROR, + erc20: token0.address, + maxTradeVolume: bn(500), + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + }) + + // Refresh and swap collateral + await newCollateral0.refresh() + await assetRegistry.connect(owner).swapRegistered(newCollateral0.address) + + // Fund trade and initialize + await token0.connect(owner).mint(trade.address, amount) + await expect( + trade.init( + backingManager.address, + newCollateral0.address, + collateral1.address, + amount, + config.dutchAuctionLength + ) + ).to.not.be.reverted + + // Check trade values + const [sellLow, sellHigh] = await newCollateral0.price() + const [buyLow, buyHigh] = await collateral1.price() + expect(await trade.middlePrice()).to.equal(divCeil(sellHigh.mul(fp('1')), buyLow)) + const withoutSlippage = sellLow.mul(fp('1')).div(buyHigh) + const withSlippage = withoutSlippage.sub( + withoutSlippage.mul(config.maxTradeSlippage).div(fp('1')) + ) + expect(await trade.lowPrice()).to.be.closeTo(withSlippage, withSlippage.div(bn('1e9'))) + }) + it('Should not allow to initialize an unfunded trade', async () => { // Attempt to initialize without funding await expect( @@ -1011,6 +1101,40 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { ).to.be.revertedWith('unfunded trade') }) + it('Should not allow to settle until auction is over', async () => { + // Fund trade and initialize + await token0.connect(owner).mint(trade.address, amount) + await expect( + trade.init( + backingManager.address, + collateral0.address, + collateral1.address, + amount, + config.dutchAuctionLength + ) + ).to.not.be.reverted + + // Should not be able to settle + expect(await trade.canSettle()).to.equal(false) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await expect(trade.connect(bmSigner).settle()).to.be.revertedWith('auction not over') + }) + + // Advance time till trade can be settled + await advanceTime(config.dutchAuctionLength.add(100).toString()) + + // Settle trade + expect(await trade.canSettle()).to.equal(true) + await whileImpersonating(backingManager.address, async (bmSigner) => { + await expect(trade.connect(bmSigner).settle()).to.not.be.reverted + }) + + // Cannot settle again with trade closed + await whileImpersonating(backingManager.address, async (bmSigner) => { + await expect(trade.connect(bmSigner).settle()).to.be.revertedWith('Invalid trade state') + }) + }) + it('Should allow anyone to transfer to origin after a DutchTrade is complete', async () => { // Fund trade and initialize await token0.connect(owner).mint(trade.address, amount) diff --git a/test/Facade.test.ts b/test/Facade.test.ts index 951146b49..ffc8bea93 100644 --- a/test/Facade.test.ts +++ b/test/Facade.test.ts @@ -1,23 +1,37 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' -import { BigNumber } from 'ethers' +import { BigNumber, ContractFactory } from 'ethers' import { ethers } from 'hardhat' +import { expectEvents } from '../common/events' +import { IConfig } from '#/common/configuration' import { bn, fp } from '../common/numbers' import { setOraclePrice } from './utils/oracles' import { Asset, - CTokenVaultMock, + BackingManagerP1, + BackingMgrCompatibleV1, + BackingMgrCompatibleV2, + BackingMgrInvalidVersion, + ComptrollerMock, + CTokenWrapperMock, ERC20Mock, FacadeAct, FacadeRead, FacadeTest, MockV3Aggregator, + RecollateralizationLibP1, + RevenueTraderCompatibleV1, + RevenueTraderCompatibleV2, + RevenueTraderP1InvalidReverts, + RevenueTraderInvalidVersion, + RevenueTraderP1, StaticATokenMock, StRSRP1, IAssetRegistry, - IBackingManager, IBasketHandler, + TestIBackingManager, TestIBroker, TestIRevenueTrader, TestIMain, @@ -34,9 +48,14 @@ import { ORACLE_ERROR, } from './fixtures' import { getLatestBlockTimestamp, setNextBlockTimestamp } from './utils/time' -import { CollateralStatus, TradeKind, MAX_UINT256 } from '#/common/constants' +import { CollateralStatus, TradeKind, MAX_UINT256, ZERO_ADDRESS } from '#/common/constants' +import { expectTrade } from './utils/trades' import { mintCollaterals } from './utils/tokens' +const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip + +const itP1 = IMPLEMENTATION == Implementation.P1 ? it : it.skip + describe('FacadeRead + FacadeAct contracts', () => { let owner: SignerWithAddress let addr1: SignerWithAddress @@ -48,7 +67,10 @@ describe('FacadeRead + FacadeAct contracts', () => { let token: ERC20Mock let usdc: USDCMock let aToken: StaticATokenMock - let cTokenVault: CTokenVaultMock + let cTokenVault: CTokenWrapperMock + let aaveToken: ERC20Mock + let compToken: ERC20Mock + let compoundMock: ComptrollerMock let rsr: ERC20Mock let basket: Collateral[] @@ -70,22 +92,38 @@ describe('FacadeRead + FacadeAct contracts', () => { let basketHandler: IBasketHandler let rTokenTrader: TestIRevenueTrader let rsrTrader: TestIRevenueTrader - let backingManager: IBackingManager + let backingManager: TestIBackingManager let broker: TestIBroker let assetRegistry: IAssetRegistry // RSR let rsrAsset: Asset + // Config values + let config: IConfig + + // Factories + let RevenueTraderV2ImplFactory: ContractFactory + let RevenueTraderV1ImplFactory: ContractFactory + let RevenueTraderInvalidVerImplFactory: ContractFactory + let RevenueTraderRevertsImplFactory: ContractFactory + let BackingMgrV2ImplFactory: ContractFactory + let BackingMgrV1ImplFactory: ContractFactory + let BackingMgrInvalidVerImplFactory: ContractFactory + beforeEach(async () => { ;[owner, addr1, addr2, other] = await ethers.getSigners() // Deploy fixture ;({ stRSR, + aaveToken, + compToken, + compoundMock, rsr, rsrAsset, basket, + config, facade, facadeAct, facadeTest, @@ -107,12 +145,47 @@ describe('FacadeRead + FacadeAct contracts', () => { aToken = ( await ethers.getContractAt('StaticATokenMock', await aTokenAsset.erc20()) ) - cTokenVault = ( - await ethers.getContractAt('CTokenVaultMock', await cTokenAsset.erc20()) + cTokenVault = ( + await ethers.getContractAt('CTokenWrapperMock', await cTokenAsset.erc20()) + ) + + // Factories used in tests + RevenueTraderV2ImplFactory = await ethers.getContractFactory('RevenueTraderCompatibleV2') + + RevenueTraderV1ImplFactory = await ethers.getContractFactory('RevenueTraderCompatibleV1') + + RevenueTraderInvalidVerImplFactory = await ethers.getContractFactory( + 'RevenueTraderInvalidVersion' + ) + + RevenueTraderRevertsImplFactory = await ethers.getContractFactory( + 'RevenueTraderP1InvalidReverts' + ) + + const tradingLib: RecollateralizationLibP1 = ( + await (await ethers.getContractFactory('RecollateralizationLibP1')).deploy() ) + + BackingMgrV2ImplFactory = await ethers.getContractFactory('BackingMgrCompatibleV2', { + libraries: { + RecollateralizationLibP1: tradingLib.address, + }, + }) + + BackingMgrV1ImplFactory = await ethers.getContractFactory('BackingMgrCompatibleV1', { + libraries: { + RecollateralizationLibP1: tradingLib.address, + }, + }) + + BackingMgrInvalidVerImplFactory = await ethers.getContractFactory('BackingMgrInvalidVersion', { + libraries: { + RecollateralizationLibP1: tradingLib.address, + }, + }) }) - describe('Views', () => { + describe('FacadeRead + interactions with FacadeAct', () => { let issueAmount: BigNumber const expectValidBasketBreakdown = async (rToken: TestIRToken) => { @@ -194,6 +267,28 @@ describe('FacadeRead + FacadeAct contracts', () => { expect(uoas[3]).to.equal(issueAmount.div(4)) }) + it('Should handle UNPRICED when returning issuable quantities', async () => { + // Set unpriced assets, should return UoA = 0 + await setOraclePrice(tokenAsset.address, MAX_UINT256.div(2).sub(1)) + const [toks, quantities, uoas] = await facade.callStatic.issue(rToken.address, issueAmount) + expect(toks.length).to.equal(4) + expect(toks[0]).to.equal(token.address) + expect(toks[1]).to.equal(usdc.address) + expect(toks[2]).to.equal(aToken.address) + expect(toks[3]).to.equal(cTokenVault.address) + expect(quantities.length).to.equal(4) + expect(quantities[0]).to.equal(issueAmount.div(4)) + expect(quantities[1]).to.equal(issueAmount.div(4).div(bn('1e12'))) + expect(quantities[2]).to.equal(issueAmount.div(4)) + expect(quantities[3]).to.equal(issueAmount.div(4).mul(50).div(bn('1e10'))) + expect(uoas.length).to.equal(4) + // Three assets are unpriced + expect(uoas[0]).to.equal(0) + expect(uoas[1]).to.equal(issueAmount.div(4)) + expect(uoas[2]).to.equal(0) + expect(uoas[3]).to.equal(0) + }) + it('Should return redeemable quantities correctly', async () => { const [toks, quantities, isProrata] = await facade.callStatic.redeem( rToken.address, @@ -296,18 +391,6 @@ describe('FacadeRead + FacadeAct contracts', () => { expect(overCollateralization).to.equal(0) }) - it('Should return backingOverview backing correctly when RSR is UNPRICED', async () => { - await setOraclePrice(tokenAsset.address, MAX_UINT256.div(2).sub(1)) - await basketHandler.refreshBasket() - const [backing, overCollateralization] = await facade.callStatic.backingOverview( - rToken.address - ) - - // Check values - Fully collateralized and no over-collateralization - expect(backing).to.equal(fp('1')) - expect(overCollateralization).to.equal(0) - }) - it('Should return backingOverview over-collateralization correctly when RSR price is 0', async () => { // Mint some RSR const stakeAmount = bn('50e18') // Half in value compared to issued RTokens @@ -413,7 +496,7 @@ describe('FacadeRead + FacadeAct contracts', () => { expect(lotLow).to.equal(0) // revenue - const [erc20s, canStart, surpluses, minTradeAmounts] = + let [erc20s, canStart, surpluses, minTradeAmounts] = await facadeAct.callStatic.revenueOverview(trader.address) expect(erc20s.length).to.equal(8) // should be full set of registered ERC20s @@ -443,6 +526,131 @@ describe('FacadeRead + FacadeAct contracts', () => { const data = funcSig.substring(0, 10) + args.slice(2) await expect(facadeAct.multicall([data])).to.emit(trader, 'TradeStarted') + // Another call to revenueOverview should not propose any auction + ;[erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + expect(canStart).to.eql(Array(8).fill(false)) + + // Nothing should be settleable + expect((await facade.auctionsSettleable(trader.address)).length).to.equal(0) + + // Advance time till auction ended + await advanceTime(auctionLength + 13) + + // Now should be settleable + const settleable = await facade.auctionsSettleable(trader.address) + expect(settleable.length).to.equal(1) + expect(settleable[0]).to.equal(token.address) + + // Another call to revenueOverview should settle and propose new auction + ;[erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + + // Should repeat the same auctions + for (let i = 0; i < 8; i++) { + if (erc20s[i] == token.address) { + expect(canStart[i]).to.equal(true) + expect(surpluses[i]).to.equal(tokenSurplus) + } else { + expect(canStart[i]).to.equal(false) + expect(surpluses[i]).to.equal(0) + } + } + + // Settle and start new auction + await facadeAct.runRevenueAuctions( + trader.address, + erc20sToStart, + erc20sToStart, + TradeKind.DUTCH_AUCTION + ) + + // Send additional revenues + await token.connect(addr1).transfer(trader.address, tokenSurplus) + + // Call revenueOverview, cannot open new auctions + ;[erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + expect(canStart).to.eql(Array(8).fill(false)) + } + }) + + itP1('Should handle other versions when running revenueOverview revenue', async () => { + // Use P1 specific versions + backingManager = ( + await ethers.getContractAt('BackingManagerP1', backingManager.address) + ) + rTokenTrader = ( + await ethers.getContractAt('RevenueTraderP1', rTokenTrader.address) + ) + rsrTrader = await ethers.getContractAt('RevenueTraderP1', rsrTrader.address) + + const revTraderV2: RevenueTraderCompatibleV2 = ( + await RevenueTraderV2ImplFactory.deploy() + ) + + const revTraderV1: RevenueTraderCompatibleV1 = ( + await RevenueTraderV1ImplFactory.deploy() + ) + + const backingManagerV2: BackingMgrCompatibleV2 = ( + await BackingMgrV2ImplFactory.deploy() + ) + + const backingManagerV1: BackingMgrCompatibleV1 = ( + await BackingMgrV1ImplFactory.deploy() + ) + + // Upgrade RevenueTraders and BackingManager to V2 + await rsrTrader.connect(owner).upgradeTo(revTraderV2.address) + await rTokenTrader.connect(owner).upgradeTo(revTraderV2.address) + await backingManager.connect(owner).upgradeTo(backingManagerV2.address) + + const traders = [rTokenTrader, rsrTrader] + for (let traderIndex = 0; traderIndex < traders.length; traderIndex++) { + const trader = traders[traderIndex] + + const minTradeVolume = await trader.minTradeVolume() + const auctionLength = await broker.dutchAuctionLength() + const tokenSurplus = bn('0.5e18') + await token.connect(addr1).transfer(trader.address, tokenSurplus) + + // revenue + let [erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + expect(erc20s.length).to.equal(8) // should be full set of registered ERC20s + + const erc20sToStart = [] + for (let i = 0; i < 8; i++) { + if (erc20s[i] == token.address) { + erc20sToStart.push(erc20s[i]) + expect(canStart[i]).to.equal(true) + expect(surpluses[i]).to.equal(tokenSurplus) + } else { + expect(canStart[i]).to.equal(false) + expect(surpluses[i]).to.equal(0) + } + const asset = await ethers.getContractAt('IAsset', await assetRegistry.toAsset(erc20s[i])) + const [low] = await asset.price() + expect(minTradeAmounts[i]).to.equal( + minTradeVolume.mul(bn('10').pow(await asset.erc20Decimals())).div(low) + ) // 1% oracleError + } + + // Run revenue auctions via multicall + const funcSig = ethers.utils.id('runRevenueAuctions(address,address[],address[],uint8)') + const args = ethers.utils.defaultAbiCoder.encode( + ['address', 'address[]', 'address[]', 'uint8'], + [trader.address, [], erc20sToStart, TradeKind.DUTCH_AUCTION] + ) + const data = funcSig.substring(0, 10) + args.slice(2) + await expect(facadeAct.multicall([data])).to.emit(trader, 'TradeStarted') + + // Another call to revenueOverview should not propose any auction + ;[erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + expect(canStart).to.eql(Array(8).fill(false)) + // Nothing should be settleable expect((await facade.auctionsSettleable(trader.address)).length).to.equal(0) @@ -453,9 +661,96 @@ describe('FacadeRead + FacadeAct contracts', () => { const settleable = await facade.auctionsSettleable(trader.address) expect(settleable.length).to.equal(1) expect(settleable[0]).to.equal(token.address) + + // Upgrade to V1 + await trader.connect(owner).upgradeTo(revTraderV1.address) + await backingManager.connect(owner).upgradeTo(backingManagerV1.address) + + // Another call to revenueOverview should settle and propose new auction + ;[erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + + // Should repeat the same auctions + for (let i = 0; i < 8; i++) { + if (erc20s[i] == token.address) { + expect(canStart[i]).to.equal(true) + expect(surpluses[i]).to.equal(tokenSurplus) + } else { + expect(canStart[i]).to.equal(false) + expect(surpluses[i]).to.equal(0) + } + } + + // Settle and start new auction + await facadeAct.runRevenueAuctions( + trader.address, + erc20sToStart, + erc20sToStart, + TradeKind.DUTCH_AUCTION + ) + + // Send additional revenues + await token.connect(addr1).transfer(trader.address, tokenSurplus) + + // Call revenueOverview, cannot open new auctions + ;[erc20s, canStart, surpluses, minTradeAmounts] = + await facadeAct.callStatic.revenueOverview(trader.address) + expect(canStart).to.eql(Array(8).fill(false)) } }) + itP1('Should handle invalid versions when running revenueOverview', async () => { + // Use P1 specific versions + rsrTrader = await ethers.getContractAt('RevenueTraderP1', rsrTrader.address) + backingManager = ( + await ethers.getContractAt('BackingManagerP1', backingManager.address) + ) + + const revTraderInvalidVer: RevenueTraderInvalidVersion = ( + await RevenueTraderInvalidVerImplFactory.deploy() + ) + + const bckMgrInvalidVer: BackingMgrInvalidVersion = ( + await BackingMgrInvalidVerImplFactory.deploy() + ) + + const revTraderV2: RevenueTraderCompatibleV2 = ( + await RevenueTraderV2ImplFactory.deploy() + ) + + // Upgrade RevenueTrader to V0 - Use RSR as an example + await rsrTrader.connect(owner).upgradeTo(revTraderInvalidVer.address) + + const tokenSurplus = bn('0.5e18') + await token.connect(addr1).transfer(rsrTrader.address, tokenSurplus) + + await expect(facadeAct.callStatic.revenueOverview(rsrTrader.address)).to.be.revertedWith( + 'unrecognized version' + ) + + // Upgrade to a version where manageToken reverts in Traders + const revTraderReverts: RevenueTraderP1InvalidReverts = ( + await RevenueTraderRevertsImplFactory.deploy() + ) + await rsrTrader.connect(owner).upgradeTo(revTraderReverts.address) + + // revenue + const [erc20s, canStart, ,] = await facadeAct.callStatic.revenueOverview(rsrTrader.address) + expect(erc20s.length).to.equal(8) // should be full set of registered ERC20s + + // No auction can be started + expect(canStart).to.eql(Array(8).fill(false)) + + // Set revenue trader to a valid version but have an invalid Backing Manager + await rsrTrader.connect(owner).upgradeTo(revTraderV2.address) + await backingManager.connect(owner).upgradeTo(bckMgrInvalidVer.address) + + // Reverts due to invalid version when forwarding revenue + await expect(facadeAct.callStatic.revenueOverview(rsrTrader.address)).to.be.revertedWith( + 'unrecognized version' + ) + }) + it('Should return nextRecollateralizationAuction', async () => { // Setup prime basket await basketHandler.connect(owner).setPrimeBasket([usdc.address], [fp('1')]) @@ -469,12 +764,131 @@ describe('FacadeRead + FacadeAct contracts', () => { const sellAmt: BigNumber = await token.balanceOf(backingManager.address) // Confirm nextRecollateralizationAuction is true - const [canStart, sell, buy, sellAmount] = + let [canStart, sell, buy, sellAmount] = + await facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) + expect(canStart).to.equal(true) + expect(sell).to.equal(token.address) + expect(buy).to.equal(usdc.address) + expect(sellAmount).to.equal(sellAmt) + + // Trigger auction + await backingManager.rebalance(TradeKind.BATCH_AUCTION) + + const auctionTimestamp: number = await getLatestBlockTimestamp() + + // Check auction registered + // token -> usdc Auction + await expectTrade(backingManager, { + sell: token.address, + buy: usdc.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // nextRecollateralizationAuction should return false (trade open) + ;[canStart, sell, buy, sellAmount] = + await facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) + expect(canStart).to.equal(false) + expect(sell).to.equal(ZERO_ADDRESS) + expect(buy).to.equal(ZERO_ADDRESS) + expect(sellAmount).to.equal(0) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // nextRecollateralizationAuction should return the next trade + // In this case it will retry the same auction + ;[canStart, sell, buy, sellAmount] = + await facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) + expect(canStart).to.equal(true) + expect(sell).to.equal(token.address) + expect(buy).to.equal(usdc.address) + expect(sellAmount).to.equal(sellAmt) + }) + + itP1('Should handle other versions for nextRecollateralizationAuction', async () => { + // Use P1 specific versions + backingManager = ( + await ethers.getContractAt('BackingManagerP1', backingManager.address) + ) + + const backingManagerV2: BackingMgrCompatibleV2 = ( + await BackingMgrV2ImplFactory.deploy() + ) + + const backingManagerV1: BackingMgrCompatibleV1 = ( + await BackingMgrV1ImplFactory.deploy() + ) + + const backingManagerInvalidVer: BackingMgrInvalidVersion = ( + await BackingMgrInvalidVerImplFactory.deploy() + ) + + // Upgrade BackingManager to V2 + await backingManager.connect(owner).upgradeTo(backingManagerV2.address) + + // Setup prime basket + await basketHandler.connect(owner).setPrimeBasket([usdc.address], [fp('1')]) + + // Switch Basket + await expect(basketHandler.connect(owner).refreshBasket()) + .to.emit(basketHandler, 'BasketSet') + .withArgs(2, [usdc.address], [fp('1')], false) + + // Trigger recollateralization + const sellAmt: BigNumber = await token.balanceOf(backingManager.address) + + // Confirm nextRecollateralizationAuction is true + let [canStart, sell, buy, sellAmount] = + await facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) + expect(canStart).to.equal(true) + expect(sell).to.equal(token.address) + expect(buy).to.equal(usdc.address) + expect(sellAmount).to.equal(sellAmt) + + // Trigger auction + await backingManager.rebalance(TradeKind.BATCH_AUCTION) + + const auctionTimestamp: number = await getLatestBlockTimestamp() + + // Check auction registered + // token -> usdc Auction + await expectTrade(backingManager, { + sell: token.address, + buy: usdc.address, + endTime: auctionTimestamp + Number(config.batchAuctionLength), + externalId: bn('0'), + }) + + // Upgrade BackingManager to V1 + await backingManager.connect(owner).upgradeTo(backingManagerV1.address) + + // nextRecollateralizationAuction should return false (trade open) + ;[canStart, sell, buy, sellAmount] = + await facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) + expect(canStart).to.equal(false) + expect(sell).to.equal(ZERO_ADDRESS) + expect(buy).to.equal(ZERO_ADDRESS) + expect(sellAmount).to.equal(0) + + // Advance time till auction ended + await advanceTime(config.batchAuctionLength.add(100).toString()) + + // nextRecollateralizationAuction should return the next trade + // In this case it will retry the same auction + ;[canStart, sell, buy, sellAmount] = await facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) expect(canStart).to.equal(true) expect(sell).to.equal(token.address) expect(buy).to.equal(usdc.address) expect(sellAmount).to.equal(sellAmt) + + // Invalid versions are also handled + await backingManager.connect(owner).upgradeTo(backingManagerInvalidVer.address) + + await expect( + facadeAct.callStatic.nextRecollateralizationAuction(backingManager.address) + ).to.be.revertedWith('unrecognized version') }) it('Should return basketBreakdown correctly for paused token', async () => { @@ -631,4 +1045,246 @@ describe('FacadeRead + FacadeAct contracts', () => { }) } }) + + // P1 only + describeP1('FacadeAct', () => { + let issueAmount: BigNumber + + beforeEach(async () => { + // Mint Tokens + initialBal = bn('10000000000e18') + await token.connect(owner).mint(addr1.address, initialBal) + await usdc.connect(owner).mint(addr1.address, initialBal) + await aToken.connect(owner).mint(addr1.address, initialBal) + await cTokenVault.connect(owner).mint(addr1.address, initialBal) + + await token.connect(owner).mint(addr2.address, initialBal) + await usdc.connect(owner).mint(addr2.address, initialBal) + await aToken.connect(owner).mint(addr2.address, initialBal) + await cTokenVault.connect(owner).mint(addr2.address, initialBal) + + // Mint RSR + await rsr.connect(owner).mint(addr1.address, initialBal) + + // Issue some RTokens + issueAmount = bn('100e18') + + // Provide approvals + await token.connect(addr1).approve(rToken.address, initialBal) + await usdc.connect(addr1).approve(rToken.address, initialBal) + await aToken.connect(addr1).approve(rToken.address, initialBal) + await cTokenVault.connect(addr1).approve(rToken.address, initialBal) + + // Issue rTokens + await rToken.connect(addr1).issue(issueAmount) + + // Use P1 specific versions + backingManager = ( + await ethers.getContractAt('BackingManagerP1', backingManager.address) + ) + rTokenTrader = ( + await ethers.getContractAt('RevenueTraderP1', rTokenTrader.address) + ) + rsrTrader = await ethers.getContractAt('RevenueTraderP1', rsrTrader.address) + }) + + it('Should claim rewards', async () => { + const rewardAmountAAVE = bn('0.5e18') + const rewardAmountCOMP = bn('0.8e18') + + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + expect(await compToken.balanceOf(rsrTrader.address)).to.equal(0) + + // AAVE Rewards + await aToken.setRewards(backingManager.address, rewardAmountAAVE) + await aToken.setRewards(rTokenTrader.address, rewardAmountAAVE) + + // COMP Rewards + await compoundMock.setRewards(rsrTrader.address, rewardAmountCOMP) + + // Via Facade, claim rewards from backingManager + await expectEvents(facadeAct.claimRewards(rToken.address), [ + { + contract: aToken, + name: 'RewardsClaimed', + args: [aaveToken.address, rewardAmountAAVE], + emitted: true, + }, + { + contract: aToken, + name: 'RewardsClaimed', + args: [aaveToken.address, rewardAmountAAVE], + emitted: true, + }, + { + contract: cTokenVault, + name: 'RewardsClaimed', + args: [compToken.address, rewardAmountCOMP], + emitted: true, + }, + ]) + + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmountAAVE) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(rewardAmountAAVE) + expect(await compToken.balanceOf(rsrTrader.address)).to.equal(rewardAmountCOMP) + }) + + it('Should run revenue auctions correctly', async () => { + const auctionLength = await broker.dutchAuctionLength() + const tokenSurplus = bn('0.5e18') + await token.connect(addr1).transfer(rsrTrader.address, tokenSurplus) + + // Run revenue auctions + await expect( + facadeAct.runRevenueAuctions( + rsrTrader.address, + [], + [token.address], + TradeKind.DUTCH_AUCTION + ) + ) + .to.emit(rsrTrader, 'TradeStarted') + .withArgs(anyValue, token.address, rsr.address, anyValue, anyValue) + + // Nothing should be settleable + expect((await facade.auctionsSettleable(rsrTrader.address)).length).to.equal(0) + + // Advance time till auction ended + await advanceTime(auctionLength + 13) + + // Settle and start new auction - Will retry + await expectEvents( + facadeAct.runRevenueAuctions( + rsrTrader.address, + [token.address], + [token.address], + TradeKind.DUTCH_AUCTION + ), + [ + { + contract: rsrTrader, + name: 'TradeSettled', + args: [anyValue, token.address, rsr.address, anyValue, anyValue], + emitted: true, + }, + { + contract: rsrTrader, + name: 'TradeStarted', + args: [anyValue, token.address, rsr.address, anyValue, anyValue], + emitted: true, + }, + ] + ) + }) + + it('Should handle other versions when running revenue auctions', async () => { + const revTraderV2: RevenueTraderCompatibleV2 = ( + await RevenueTraderV2ImplFactory.deploy() + ) + + const revTraderV1: RevenueTraderCompatibleV1 = ( + await RevenueTraderV1ImplFactory.deploy() + ) + + const backingManagerV2: BackingMgrCompatibleV2 = ( + await BackingMgrV2ImplFactory.deploy() + ) + + const backingManagerV1: BackingMgrCompatibleV1 = ( + await BackingMgrV1ImplFactory.deploy() + ) + + // Upgrade components to V2 + await backingManager.connect(owner).upgradeTo(backingManagerV2.address) + await rTokenTrader.connect(owner).upgradeTo(revTraderV2.address) + + const auctionLength = await broker.dutchAuctionLength() + const tokenSurplus = bn('0.5e18') + await token.connect(addr1).transfer(rTokenTrader.address, tokenSurplus) + + // Run revenue auctions + await expect( + facadeAct.runRevenueAuctions( + rTokenTrader.address, + [], + [token.address], + TradeKind.DUTCH_AUCTION + ) + ) + .to.emit(rTokenTrader, 'TradeStarted') + .withArgs(anyValue, token.address, rToken.address, anyValue, anyValue) + + // Nothing should be settleable + expect((await facade.auctionsSettleable(rTokenTrader.address)).length).to.equal(0) + + // Advance time till auction ended + await advanceTime(auctionLength + 13) + + // Upgrade to V1 + await backingManager.connect(owner).upgradeTo(backingManagerV1.address) + await rTokenTrader.connect(owner).upgradeTo(revTraderV1.address) + + // Settle and start new auction - Will retry + await expectEvents( + facadeAct.runRevenueAuctions( + rTokenTrader.address, + [token.address], + [token.address], + TradeKind.DUTCH_AUCTION + ), + [ + { + contract: rTokenTrader, + name: 'TradeSettled', + args: [anyValue, token.address, rToken.address, anyValue, anyValue], + emitted: true, + }, + { + contract: rTokenTrader, + name: 'TradeStarted', + args: [anyValue, token.address, rToken.address, anyValue, anyValue], + emitted: true, + }, + ] + ) + }) + + it('Should handle invalid versions when running revenue auctions', async () => { + const revTraderInvalidVer: RevenueTraderInvalidVersion = ( + await RevenueTraderInvalidVerImplFactory.deploy() + ) + + const backingManagerInvalidVer: BackingMgrInvalidVersion = ( + await BackingMgrInvalidVerImplFactory.deploy() + ) + + // Upgrade RevenueTrader to invalid version - Use RSR as an example + await rsrTrader.connect(owner).upgradeTo(revTraderInvalidVer.address) + + const tokenSurplus = bn('0.5e18') + await token.connect(addr1).transfer(rsrTrader.address, tokenSurplus) + + await expect( + facadeAct.runRevenueAuctions( + rsrTrader.address, + [], + [token.address], + TradeKind.DUTCH_AUCTION + ) + ).to.be.revertedWith('unrecognized version') + + // Also set BackingManager to invalid version + await backingManager.connect(owner).upgradeTo(backingManagerInvalidVer.address) + + await expect( + facadeAct.runRevenueAuctions( + rsrTrader.address, + [], + [token.address], + TradeKind.DUTCH_AUCTION + ) + ).to.be.revertedWith('unrecognized version') + }) + }) }) diff --git a/test/FacadeWrite.test.ts b/test/FacadeWrite.test.ts index ab843462f..2618e777d 100644 --- a/test/FacadeWrite.test.ts +++ b/test/FacadeWrite.test.ts @@ -49,7 +49,7 @@ import { TestIRToken, TimelockController, USDCMock, - CTokenVaultMock, + CTokenWrapperMock, } from '../typechain' import { Collateral, @@ -79,7 +79,7 @@ describe('FacadeWrite contract', () => { // Tokens let token: ERC20Mock let usdc: USDCMock - let cTokenVault: CTokenVaultMock + let cTokenVault: CTokenWrapperMock let basket: Collateral[] // Aave / Comp @@ -144,8 +144,8 @@ describe('FacadeWrite contract', () => { token = await ethers.getContractAt('ERC20Mock', await tokenAsset.erc20()) usdc = await ethers.getContractAt('USDCMock', await usdcAsset.erc20()) - cTokenVault = ( - await ethers.getContractAt('CTokenVaultMock', await cTokenAsset.erc20()) + cTokenVault = ( + await ethers.getContractAt('CTokenWrapperMock', await cTokenAsset.erc20()) ) // Deploy DFacadeWriteLib lib diff --git a/test/Furnace.test.ts b/test/Furnace.test.ts index 12d8da224..f74e01ba2 100644 --- a/test/Furnace.test.ts +++ b/test/Furnace.test.ts @@ -6,7 +6,7 @@ import hre, { ethers, upgrades } from 'hardhat' import { IConfig, MAX_RATIO } from '../common/configuration' import { bn, fp } from '../common/numbers' import { - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, StaticATokenMock, TestIFurnace, @@ -52,7 +52,7 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { let token0: ERC20Mock let token1: ERC20Mock let token2: StaticATokenMock - let token3: CTokenVaultMock + let token3: CTokenWrapperMock let collateral0: Collateral let collateral1: Collateral @@ -85,8 +85,8 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { token2 = ( await ethers.getContractAt('StaticATokenMock', await collateral2.erc20()) ) - token3 = ( - await ethers.getContractAt('CTokenVaultMock', await collateral3.erc20()) + token3 = ( + await ethers.getContractAt('CTokenWrapperMock', await collateral3.erc20()) ) // Mint Tokens @@ -103,18 +103,18 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { // Applies to all components - used here as an example it('Deployment does not accept invalid main address', async () => { let FurnaceFactory: ContractFactory + let newFurnace: TestIFurnace if (IMPLEMENTATION == Implementation.P0) { FurnaceFactory = await ethers.getContractFactory('FurnaceP0') - return await FurnaceFactory.deploy() + newFurnace = await FurnaceFactory.deploy() } else if (IMPLEMENTATION == Implementation.P1) { FurnaceFactory = await ethers.getContractFactory('FurnaceP1') - return await upgrades.deployProxy(FurnaceFactory, [], { + newFurnace = await upgrades.deployProxy(FurnaceFactory, [], { kind: 'uups', }) } else { throw new Error('PROTO_IMPL must be set to either `0` or `1`') } - const newFurnace = await FurnaceFactory.deploy() await expect(newFurnace.init(ZERO_ADDRESS, config.rewardRatio)).to.be.revertedWith( 'main is zero address' ) @@ -237,7 +237,7 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { expect(await rToken.balanceOf(furnace.address)).to.equal(hndAmt) // Advance one period - await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await advanceTime(Number(ONE_PERIOD)) // Melt await expect(furnace.connect(addr1).melt()).to.not.emit(rToken, 'Melted') @@ -256,7 +256,7 @@ describe(`FurnaceP${IMPLEMENTATION} contract`, () => { await rToken.connect(addr1).transfer(furnace.address, hndAmt) // Get past first noop melt - await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + Number(ONE_PERIOD)) + await advanceTime(Number(ONE_PERIOD)) await expect(furnace.connect(addr1).melt()).to.not.emit(rToken, 'Melted') diff --git a/test/Main.test.ts b/test/Main.test.ts index 3efe71f4e..841d2bf79 100644 --- a/test/Main.test.ts +++ b/test/Main.test.ts @@ -33,10 +33,11 @@ import { bn, fp } from '../common/numbers' import { Asset, ATokenFiatCollateral, + BackingManagerP1, BasketHandlerP1, CTokenFiatCollateral, DutchTrade, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FacadeRead, FacadeTest, @@ -47,6 +48,7 @@ import { InvalidRefPerTokCollateralMock, MockV3Aggregator, MockableCollateral, + RevenueTraderP1, RTokenAsset, StaticATokenMock, TestIBackingManager, @@ -80,6 +82,8 @@ const DEFAULT_THRESHOLD = fp('0.01') // 1% const itP1 = IMPLEMENTATION == Implementation.P1 ? it : it.skip +const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip + const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip @@ -114,7 +118,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { let token0: ERC20Mock let token1: USDCMock let token2: StaticATokenMock - let token3: CTokenVaultMock + let token3: CTokenWrapperMock let backupToken1: ERC20Mock let backupToken2: ERC20Mock let collateral0: FiatCollateral @@ -179,7 +183,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { token0 = erc20s[collateral.indexOf(basket[0])] token1 = erc20s[collateral.indexOf(basket[1])] token2 = erc20s[collateral.indexOf(basket[2])] - token3 = erc20s[collateral.indexOf(basket[3])] + token3 = erc20s[collateral.indexOf(basket[3])] backupToken1 = erc20s[2] // USDT backupCollateral1 = collateral[2] @@ -1818,6 +1822,28 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { ) }) + it('Should be able to set prime basket multiple times', async () => { + await expect( + basketHandler + .connect(owner) + .setPrimeBasket([token0.address, token3.address], [fp('0.5'), fp('0.5')]) + ) + .to.emit(basketHandler, 'PrimeBasketSet') + .withArgs( + [token0.address, token3.address], + [fp('0.5'), fp('0.5')], + [ethers.utils.formatBytes32String('USD')] + ) + + await expect(basketHandler.connect(owner).setPrimeBasket([token1.address], [fp('1')])) + .to.emit(basketHandler, 'PrimeBasketSet') + .withArgs([token1.address], [fp('1')], [ethers.utils.formatBytes32String('USD')]) + + await expect(basketHandler.connect(owner).setPrimeBasket([token2.address], [fp('1')])) + .to.emit(basketHandler, 'PrimeBasketSet') + .withArgs([token2.address], [fp('1')], [ethers.utils.formatBytes32String('USD')]) + }) + it('Should not allow to set prime Basket as superset of old basket', async () => { await assetRegistry.connect(owner).register(backupCollateral1.address) await expect( @@ -1967,6 +1993,15 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { } } + it('Should perform validations on quoteCustomRedemption', async () => { + const basketNonces = [1, 2] + const portions = [fp('1')] + const amount = fp('10000') + await expect( + basketHandler.quoteCustomRedemption(basketNonces, portions, amount) + ).to.be.revertedWith('portions does not mirror basketNonces') + }) + it('Should correctly quote the current basket, same as quote()', async () => { /* Test Quote @@ -2250,6 +2285,157 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { expectDelta(balsBefore, quote.quantities, balsAfter) }) + it('Should correctly quote a historical redemption with unregistered asset', async () => { + // default usdc & refresh basket to use backup collateral + await usdcChainlink.updateAnswer(bn('0.8e8')) // default token1 + await daiChainlink.updateAnswer(bn('0.8e8')) // default token0, token2, token3 + await basketHandler.refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + expect(await basketHandler.fullyCollateralized()).to.equal(false) + + // Unregister asset in previous basket + await assetRegistry.connect(owner).unregister(collateral1.address) + /* + Test Quote + */ + const basketNonces = [1] + const portions = [fp('1')] + const amount = fp('10000') + const quote = await basketHandler.quoteCustomRedemption(basketNonces, portions, amount) + + expect(quote.erc20s.length).equal(4) + expect(quote.quantities.length).equal(4) + + const expectedTokens = [token0, token1, token2, token3] + const expectedAddresses = expectedTokens.map((t) => t.address) + const expectedQuantities = [ + fp('0.25') + .mul(issueAmount) + .div(await collateral0.refPerTok()) + .div(bn(`1e${18 - (await token0.decimals())}`)), + bn('0') + .mul(issueAmount) + .div(await collateral1.refPerTok()) + .div(bn(`1e${18 - (await token1.decimals())}`)), + fp('0.25') + .mul(issueAmount) + .div(await collateral2.refPerTok()) + .div(bn(`1e${18 - (await token2.decimals())}`)), + fp('0.25') + .mul(issueAmount) + .div(await collateral3.refPerTok()) + .div(bn(`1e${18 - (await token3.decimals())}`)), + ] + expectEqualArrays(quote.erc20s, expectedAddresses) + expectEqualArrays(quote.quantities, expectedQuantities) + + /* + Test Custom Redemption + */ + const balsBefore = await getBalances(addr1.address, expectedTokens) + await backupToken1.mint(backingManager.address, issueAmount) + + // rToken redemption + await expect( + rToken + .connect(addr1) + .redeemCustom( + addr1.address, + amount, + basketNonces, + portions, + quote.erc20s, + quote.quantities + ) + ).to.not.be.reverted + + const balsAfter = await getBalances(addr1.address, expectedTokens) + expectDelta(balsBefore, quote.quantities, balsAfter) + }) + + it('Should correctly quote a historical redemption with an non-collateral asset', async () => { + // default usdc & refresh basket to use backup collateral + await usdcChainlink.updateAnswer(bn('0.8e8')) // default token1 + await daiChainlink.updateAnswer(bn('0.8e8')) // default token0, token2, token3 + await basketHandler.refreshBasket() + await advanceTime(Number(config.warmupPeriod) + 1) + expect(await basketHandler.status()).to.equal(CollateralStatus.SOUND) + expect(await basketHandler.fullyCollateralized()).to.equal(false) + + // Swap collateral for asset in previous basket + const AssetFactory: ContractFactory = await ethers.getContractFactory('Asset') + const newAsset1: Asset = ( + await AssetFactory.deploy( + PRICE_TIMEOUT, + await collateral1.chainlinkFeed(), + ORACLE_ERROR, + token1.address, + config.rTokenMaxTradeVolume, + 1 + ) + ) + + await assetRegistry.connect(owner).swapRegistered(newAsset1.address) + + /* + Test Quote + */ + const basketNonces = [1] + const portions = [fp('1')] + const amount = fp('10000') + const quote = await basketHandler.quoteCustomRedemption(basketNonces, portions, amount) + + expect(quote.erc20s.length).equal(4) + expect(quote.quantities.length).equal(4) + + const expectedTokens = [token0, token1, token2, token3] + const expectedAddresses = expectedTokens.map((t) => t.address) + const expectedQuantities = [ + fp('0.25') + .mul(issueAmount) + .div(await collateral0.refPerTok()) + .div(bn(`1e${18 - (await token0.decimals())}`)), + bn('0') + .mul(issueAmount) + .div(await collateral1.refPerTok()) + .div(bn(`1e${18 - (await token1.decimals())}`)), + fp('0.25') + .mul(issueAmount) + .div(await collateral2.refPerTok()) + .div(bn(`1e${18 - (await token2.decimals())}`)), + fp('0.25') + .mul(issueAmount) + .div(await collateral3.refPerTok()) + .div(bn(`1e${18 - (await token3.decimals())}`)), + ] + expectEqualArrays(quote.erc20s, expectedAddresses) + expectEqualArrays(quote.quantities, expectedQuantities) + + /* + Test Custom Redemption - Should behave as if token is not registered + */ + const balsBefore = await getBalances(addr1.address, expectedTokens) + await backupToken1.mint(backingManager.address, issueAmount) + + // rToken redemption + await expect( + rToken + .connect(addr1) + .redeemCustom( + addr1.address, + amount, + basketNonces, + portions, + quote.erc20s, + quote.quantities + ) + ).to.not.be.reverted + + const balsAfter = await getBalances(addr1.address, expectedTokens) + expectDelta(balsBefore, quote.quantities, balsAfter) + }) + itP1('Should return historical basket correctly', async () => { const bskHandlerP1: BasketHandlerP1 = ( await ethers.getContractAt('BasketHandlerP1', basketHandler.address) @@ -2303,6 +2489,37 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { expect(erc20s[i]).to.equal(prevERC20s[i]) expect(quantities[i]).to.equal(prevQtys[i]) } + + // Swap collateral for asset in previous basket + const AssetFactory: ContractFactory = await ethers.getContractFactory('Asset') + const newAsset1: Asset = ( + await AssetFactory.deploy( + PRICE_TIMEOUT, + await collateral1.chainlinkFeed(), + ORACLE_ERROR, + token1.address, + config.rTokenMaxTradeVolume, + 1 + ) + ) + + await assetRegistry.connect(owner).swapRegistered(newAsset1.address) + + // Get basket for prior nonce - returns 0 qty for the non-collateral + ;[erc20s, quantities] = await bskHandlerP1.getHistoricalBasket(1) + expect(erc20s.length).to.equal(4) + expect(quantities.length).to.equal(4) + + expect(erc20s).to.eql(prevERC20s) + expect(quantities).to.eql([prevQtys[0], bn(0), prevQtys[2], prevQtys[3]]) + + // Unregister that same asset + await assetRegistry.connect(owner).unregister(newAsset1.address) + + // Returns same result as before + ;[erc20s, quantities] = await bskHandlerP1.getHistoricalBasket(1) + expect(erc20s).to.eql(prevERC20s) + expect(quantities).to.eql([prevQtys[0], bn(0), prevQtys[2], prevQtys[3]]) }) }) @@ -2393,6 +2610,23 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { ) }) + it('Should allow anyone to refresh basket if disabled and not paused/frozen', async () => { + // Set backup configuration + await basketHandler + .connect(owner) + .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), [token0.address]) + + // Unregister one basket collateral + await expect(assetRegistry.connect(owner).unregister(collateral1.address)).to.emit( + assetRegistry, + 'AssetUnregistered' + ) + + expect(await basketHandler.status()).to.equal(CollateralStatus.DISABLED) + + await expect(basketHandler.connect(other).refreshBasket()).to.emit(basketHandler, 'BasketSet') + }) + it('Should allow to poke when trading paused', async () => { await main.connect(owner).pauseTrading() await main.connect(other).poke() @@ -3000,6 +3234,26 @@ describe(`MainP${IMPLEMENTATION} contract`, () => { }) }) + describeP1('BackingManagerP1', () => { + it('Should allow to cache components', async () => { + const bckMgrP1: BackingManagerP1 = await ethers.getContractAt( + 'BackingManagerP1', + backingManager.address + ) + await expect(bckMgrP1.cacheComponents()).to.not.be.reverted + }) + }) + + describeP1('RevenueTraderP1', () => { + it('Should allow to cache components', async () => { + const rsrTrader: RevenueTraderP1 = await ethers.getContractAt( + 'RevenueTraderP1', + await main.rsrTrader() + ) + await expect(rsrTrader.cacheComponents()).to.not.be.reverted + }) + }) + describeGas('Gas Reporting', () => { it('Asset Registry - Refresh', async () => { // Basket handler can run refresh diff --git a/test/RToken.test.ts b/test/RToken.test.ts index 1e018b402..d4436ec7a 100644 --- a/test/RToken.test.ts +++ b/test/RToken.test.ts @@ -2,28 +2,20 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { signERC2612Permit } from 'eth-permit' -import { BigNumber, ContractFactory } from 'ethers' +import { BigNumber } from 'ethers' import hre, { ethers } from 'hardhat' import { getChainId } from '../common/blockchain-utils' import { IConfig, ThrottleParams, MAX_THROTTLE_AMT_RATE } from '../common/configuration' -import { - BN_SCALE_FACTOR, - CollateralStatus, - MAX_UINT256, - ONE_PERIOD, - ZERO_ADDRESS, -} from '../common/constants' +import { CollateralStatus, MAX_UINT256, ONE_PERIOD, ZERO_ADDRESS } from '../common/constants' import { expectRTokenPrice, setOraclePrice } from './utils/oracles' -import { bn, fp, shortString, toBNDecimals } from '../common/numbers' +import { bn, fp, toBNDecimals } from '../common/numbers' import { ATokenFiatCollateral, CTokenFiatCollateral, ERC20Mock, ERC1271Mock, FacadeTest, - FiatCollateral, IAssetRegistry, - MockV3Aggregator, RTokenAsset, StaticATokenMock, TestIBackingManager, @@ -31,7 +23,7 @@ import { TestIMain, TestIRToken, USDCMock, - CTokenVaultMock, + CTokenWrapperMock, } from '../typechain' import { whileImpersonating } from './utils/impersonation' import snapshotGasCost from './utils/snapshotGasCost' @@ -47,24 +39,19 @@ import { Implementation, IMPLEMENTATION, ORACLE_ERROR, - SLOW, ORACLE_TIMEOUT, - PRICE_TIMEOUT, VERSION, } from './fixtures' import { expectEqualArrays } from './utils/matchers' -import { cartesianProduct } from './utils/cases' import { useEnv } from '#/utils/env' import { mintCollaterals } from './utils/tokens' +import { expectEvents } from '#/common/events' const BLOCKS_PER_HOUR = bn(300) const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip -const describeExtreme = - IMPLEMENTATION == Implementation.P1 && useEnv('EXTREME') ? describe.only : describe.skip - describe(`RTokenP${IMPLEMENTATION} contract`, () => { let owner: SignerWithAddress let addr1: SignerWithAddress @@ -76,7 +63,7 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { let token0: ERC20Mock let token1: USDCMock let token2: StaticATokenMock - let token3: CTokenVaultMock + let token3: CTokenWrapperMock let tokens: ERC20Mock[] let collateral0: Collateral @@ -123,8 +110,8 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { token2 = ( await ethers.getContractAt('StaticATokenMock', await collateral2.erc20()) ) - token3 = ( - await ethers.getContractAt('CTokenVaultMock', await collateral3.erc20()) + token3 = ( + await ethers.getContractAt('CTokenWrapperMock', await collateral3.erc20()) ) tokens = [token0, token1, token2, token3] @@ -1032,6 +1019,40 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { expect(await rToken.totalSupply()).to.equal(0) }) + it('Should handle an extremely small redeem #fast', async function () { + expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) + + const reedemAmt = bn(10) + + // Skips transfers for tokens with less than 18 decimals + await expectEvents(rToken.connect(addr1).redeem(reedemAmt), [ + { + contract: token0, + name: 'Transfer', + emitted: true, + }, + { + contract: token1, + name: 'Transfer', + emitted: false, + }, + { + contract: token2, + name: 'Transfer', + emitted: true, + }, + { + contract: token3, + name: 'Transfer', + emitted: false, + }, + ]) + + // Checkbalances + expect(await rToken.totalSupply()).to.equal(issueAmount.sub(reedemAmt)) + expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount.sub(reedemAmt)) + }) + it('Should redeem if paused #fast', async function () { await main.connect(owner).pauseTrading() await main.connect(owner).pauseIssuance() @@ -2706,183 +2727,6 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => { }) }) - describeExtreme(`Extreme Values ${SLOW ? 'slow mode' : 'fast mode'}`, () => { - // makeColl: Deploy and register a new constant-price collateral - async function makeColl(index: number | string): Promise { - const ERC20: ContractFactory = await ethers.getContractFactory('ERC20Mock') - const erc20: ERC20Mock = await ERC20.deploy('Token ' + index, 'T' + index) - const OracleFactory: ContractFactory = await ethers.getContractFactory('MockV3Aggregator') - const oracle: MockV3Aggregator = await OracleFactory.deploy(8, bn('1e8')) - await oracle.deployed() // fix extreme value tests failing - const CollateralFactory: ContractFactory = await ethers.getContractFactory('FiatCollateral') - const coll: FiatCollateral = await CollateralFactory.deploy({ - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: oracle.address, - oracleError: ORACLE_ERROR, - erc20: erc20.address, - maxTradeVolume: fp('1e36'), - oracleTimeout: ORACLE_TIMEOUT, - targetName: ethers.utils.formatBytes32String('USD'), - defaultThreshold: fp('0.01'), - delayUntilDefault: bn(86400), - }) - await assetRegistry.register(coll.address) - expect(await assetRegistry.isRegistered(erc20.address)).to.be.true - await backingManager.grantRTokenAllowance(erc20.address) - return erc20 - } - - async function forceUpdateGetStatus(): Promise { - await whileImpersonating(basketHandler.address, async (bhSigner) => { - await assetRegistry.connect(bhSigner).refresh() - }) - return basketHandler.status() - } - - async function runScenario([ - toIssue, - toRedeem, - totalSupply, // in this scenario, rtoken supply _after_ issuance. - numBasketAssets, - weightFirst, // target amount per asset (weight of first asset) - weightRest, // another target amount per asset (weight of second+ assets) - issuancePctAmt, // range under test: [.000_001 to 1.0] - redemptionPctAmt, // range under test: [.000_001 to 1.0] - ]: BigNumber[]) { - // skip nonsense cases - if ( - (numBasketAssets.eq(1) && !weightRest.eq(1)) || - toRedeem.gt(totalSupply) || - toIssue.gt(totalSupply) - ) { - return - } - - // ==== Deploy and register basket collateral - - const N = numBasketAssets.toNumber() - const erc20s: ERC20Mock[] = [] - const weights: BigNumber[] = [] - let totalWeight: BigNumber = fp(0) - for (let i = 0; i < N; i++) { - const erc20 = await makeColl(i) - erc20s.push(erc20) - const currWeight = i == 0 ? weightFirst : weightRest - weights.push(currWeight) - totalWeight = totalWeight.add(currWeight) - } - expect(await forceUpdateGetStatus()).to.equal(CollateralStatus.SOUND) - - // ==== Switch Basket - - const basketAddresses: string[] = erc20s.map((erc20) => erc20.address) - await basketHandler.connect(owner).setPrimeBasket(basketAddresses, weights) - await basketHandler.connect(owner).refreshBasket() - expect(await forceUpdateGetStatus()).to.equal(CollateralStatus.SOUND) - - for (let i = 0; i < basketAddresses.length; i++) { - expect(await basketHandler.quantity(basketAddresses[i])).to.equal(weights[i]) - } - - // ==== Mint basket tokens to owner and addr1 - - const toIssue0 = totalSupply.sub(toIssue) - const e18 = BN_SCALE_FACTOR - for (let i = 0; i < N; i++) { - const erc20: ERC20Mock = erc20s[i] - // user owner starts with enough basket assets to issue (totalSupply - toIssue) - const toMint0: BigNumber = toIssue0.mul(weights[i]).add(e18.sub(1)).div(e18) - await erc20.mint(owner.address, toMint0) - await erc20.connect(owner).increaseAllowance(rToken.address, toMint0) - - // user addr1 starts with enough basket assets to issue (toIssue) - const toMint: BigNumber = toIssue.mul(weights[i]).add(e18.sub(1)).div(e18) - await erc20.mint(addr1.address, toMint) - await erc20.connect(addr1).increaseAllowance(rToken.address, toMint) - } - - // Set up throttles - const issuanceThrottleParams = { amtRate: bn('1e48'), pctRate: issuancePctAmt } - const redemptionThrottleParams = { amtRate: bn('1e48'), pctRate: redemptionPctAmt } - - await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) - await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) - - // ==== Issue the "initial" rtoken supply to owner - - expect(await rToken.balanceOf(owner.address)).to.equal(bn(0)) - if (toIssue0.gt(0)) { - await rToken.connect(owner).issue(toIssue0) - expect(await rToken.balanceOf(owner.address)).to.equal(toIssue0) - } - - // ==== Issue the toIssue supply to addr1 - - expect(await rToken.balanceOf(addr1.address)).to.equal(0) - await rToken.connect(addr1).issue(toIssue) - expect(await rToken.balanceOf(addr1.address)).to.equal(toIssue) - - // ==== Send enough rTokens to addr2 that it can redeem the amount `toRedeem` - - // owner has toIssue0 rToken, addr1 has toIssue rToken. - if (toRedeem.lte(toIssue0)) { - await rToken.connect(owner).transfer(addr2.address, toRedeem) - } else { - await rToken.connect(owner).transfer(addr2.address, toIssue0) - await rToken.connect(addr1).transfer(addr2.address, toRedeem.sub(toIssue0)) - } - expect(await rToken.balanceOf(addr2.address)).to.equal(toRedeem) - - // ==== Redeem tokens - - await rToken.connect(addr2).redeem(toRedeem) - expect(await rToken.balanceOf(addr2.address)).to.equal(0) - } - - // ==== Generate the tests - const MAX_RTOKENS = bn('1e48') - const MAX_WEIGHT = fp(1000) - const MIN_WEIGHT = fp('1e-6') - const MIN_ISSUANCE_PCT = fp('1e-6') - const MIN_REDEMPTION_PCT = fp('1e-6') - const MIN_RTOKENS = fp('1e-6') - - let paramList - - if (SLOW) { - const bounds: BigNumber[][] = [ - [MIN_RTOKENS, MAX_RTOKENS, bn('1.205e24')], // toIssue - [MIN_RTOKENS, MAX_RTOKENS, bn('4.4231e24')], // toRedeem - [MAX_RTOKENS, bn('7.907e24')], // totalSupply - [bn(1), bn(3), bn(100)], // numAssets - [MIN_WEIGHT, MAX_WEIGHT, fp('0.1')], // weightFirst - [MIN_WEIGHT, MAX_WEIGHT, fp('0.2')], // weightRest - [MIN_ISSUANCE_PCT, fp('1e-2'), fp(1)], // issuanceThrottle.pctRate - [MIN_REDEMPTION_PCT, fp('1e-2'), fp(1)], // redemptionThrottle.pctRate - ] - - paramList = cartesianProduct(...bounds) - } else { - const bounds: BigNumber[][] = [ - [MIN_RTOKENS, MAX_RTOKENS], // toIssue - [MIN_RTOKENS, MAX_RTOKENS], // toRedeem - [MAX_RTOKENS], // totalSupply - [bn(1)], // numAssets - [MIN_WEIGHT, MAX_WEIGHT], // weightFirst - [MIN_WEIGHT], // weightRest - [MIN_ISSUANCE_PCT, fp(1)], // issuanceThrottle.pctRate - [MIN_REDEMPTION_PCT, fp(1)], // redemptionThrottle.pctRate - ] - paramList = cartesianProduct(...bounds) - } - const numCases = paramList.length.toString() - paramList.forEach((params, index) => { - it(`case ${index + 1} of ${numCases}: ${params.map(shortString).join(' ')}`, async () => { - await runScenario(params) - }) - }) - }) - describeGas('Gas Reporting', () => { let issueAmount: BigNumber diff --git a/test/RTokenExtremes.test.ts b/test/RTokenExtremes.test.ts new file mode 100644 index 000000000..f55c9f619 --- /dev/null +++ b/test/RTokenExtremes.test.ts @@ -0,0 +1,232 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, ContractFactory } from 'ethers' +import { ethers } from 'hardhat' +import { BN_SCALE_FACTOR, CollateralStatus } from '../common/constants' +import { bn, fp, shortString } from '../common/numbers' +import { + ERC20Mock, + FiatCollateral, + IAssetRegistry, + MockV3Aggregator, + TestIBackingManager, + TestIBasketHandler, + TestIRToken, +} from '../typechain' +import { whileImpersonating } from './utils/impersonation' +import { advanceTime } from './utils/time' +import { + Implementation, + IMPLEMENTATION, + ORACLE_ERROR, + SLOW, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + defaultFixtureNoBasket, +} from './fixtures' +import { cartesianProduct } from './utils/cases' +import { useEnv } from '#/utils/env' + +const describeExtreme = + IMPLEMENTATION == Implementation.P1 && useEnv('EXTREME') ? describe.only : describe.skip + +describe(`RTokenP${IMPLEMENTATION} contract`, () => { + let owner: SignerWithAddress + let addr1: SignerWithAddress + let addr2: SignerWithAddress + + // Main + let rToken: TestIRToken + let assetRegistry: IAssetRegistry + let backingManager: TestIBackingManager + let basketHandler: TestIBasketHandler + + beforeEach(async () => { + ;[owner, addr1, addr2] = await ethers.getSigners() + + // Deploy fixture + ;({ assetRegistry, backingManager, basketHandler, rToken } = await loadFixture( + defaultFixtureNoBasket + )) + }) + + describeExtreme(`Extreme Values ${SLOW ? 'slow mode' : 'fast mode'}`, () => { + // makeColl: Deploy and register a new constant-price collateral + async function makeColl(index: number | string): Promise { + const ERC20: ContractFactory = await ethers.getContractFactory('ERC20Mock') + const erc20: ERC20Mock = await ERC20.deploy('Token ' + index, 'T' + index) + const OracleFactory: ContractFactory = await ethers.getContractFactory('MockV3Aggregator') + const oracle: MockV3Aggregator = await OracleFactory.deploy(8, bn('1e8')) + await oracle.deployed() // fix extreme value tests failing + const CollateralFactory: ContractFactory = await ethers.getContractFactory('FiatCollateral') + const coll: FiatCollateral = await CollateralFactory.deploy({ + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: oracle.address, + oracleError: ORACLE_ERROR, + erc20: erc20.address, + maxTradeVolume: fp('1e36'), + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01'), + delayUntilDefault: bn(86400), + }) + await assetRegistry.register(coll.address) + expect(await assetRegistry.isRegistered(erc20.address)).to.be.true + await backingManager.grantRTokenAllowance(erc20.address) + return erc20 + } + + async function forceUpdateGetStatus(): Promise { + await whileImpersonating(basketHandler.address, async (bhSigner) => { + await assetRegistry.connect(bhSigner).refresh() + }) + return basketHandler.status() + } + + async function runScenario([ + toIssue, + toRedeem, + totalSupply, // in this scenario, rtoken supply _after_ issuance. + numBasketAssets, + weightFirst, // target amount per asset (weight of first asset) + weightRest, // another target amount per asset (weight of second+ assets) + issuancePctAmt, // range under test: [.000_001 to 1.0] + redemptionPctAmt, // range under test: [.000_001 to 1.0] + ]: BigNumber[]) { + // skip nonsense cases + if ( + (numBasketAssets.eq(1) && !weightRest.eq(1)) || + toRedeem.gt(totalSupply) || + toIssue.gt(totalSupply) + ) { + return + } + + // ==== Deploy and register basket collateral + + const N = numBasketAssets.toNumber() + const erc20s: ERC20Mock[] = [] + const weights: BigNumber[] = [] + let totalWeight: BigNumber = fp(0) + for (let i = 0; i < N; i++) { + const erc20 = await makeColl(i) + erc20s.push(erc20) + const currWeight = i == 0 ? weightFirst : weightRest + weights.push(currWeight) + totalWeight = totalWeight.add(currWeight) + } + expect(await forceUpdateGetStatus()).to.equal(CollateralStatus.DISABLED) + + // ==== Switch Basket + + const basketAddresses: string[] = erc20s.map((erc20) => erc20.address) + await basketHandler.connect(owner).setPrimeBasket(basketAddresses, weights) + await basketHandler.connect(owner).refreshBasket() + expect(await forceUpdateGetStatus()).to.equal(CollateralStatus.SOUND) + + for (let i = 0; i < basketAddresses.length; i++) { + expect(await basketHandler.quantity(basketAddresses[i])).to.equal(weights[i]) + } + + // ==== Mint basket tokens to owner and addr1 + + const toIssue0 = totalSupply.sub(toIssue) + const e18 = BN_SCALE_FACTOR + for (let i = 0; i < N; i++) { + const erc20: ERC20Mock = erc20s[i] + // user owner starts with enough basket assets to issue (totalSupply - toIssue) + const toMint0: BigNumber = toIssue0.mul(weights[i]).add(e18.sub(1)).div(e18) + await erc20.mint(owner.address, toMint0) + await erc20.connect(owner).increaseAllowance(rToken.address, toMint0) + + // user addr1 starts with enough basket assets to issue (toIssue) + const toMint: BigNumber = toIssue.mul(weights[i]).add(e18.sub(1)).div(e18) + await erc20.mint(addr1.address, toMint) + await erc20.connect(addr1).increaseAllowance(rToken.address, toMint) + } + + // Set up throttles + const issuanceThrottleParams = { amtRate: bn('1e48'), pctRate: issuancePctAmt } + const redemptionThrottleParams = { amtRate: bn('1e48'), pctRate: redemptionPctAmt } + + await rToken.connect(owner).setIssuanceThrottleParams(issuanceThrottleParams) + await rToken.connect(owner).setRedemptionThrottleParams(redemptionThrottleParams) + + await advanceTime(await basketHandler.warmupPeriod()) + + // ==== Issue the "initial" rtoken supply to owner + expect(await rToken.balanceOf(owner.address)).to.equal(bn(0)) + if (toIssue0.gt(0)) { + await rToken.connect(owner).issue(toIssue0) + expect(await rToken.balanceOf(owner.address)).to.equal(toIssue0) + } + + // ==== Issue the toIssue supply to addr1 + + expect(await rToken.balanceOf(addr1.address)).to.equal(0) + await rToken.connect(addr1).issue(toIssue) + expect(await rToken.balanceOf(addr1.address)).to.equal(toIssue) + + // ==== Send enough rTokens to addr2 that it can redeem the amount `toRedeem` + + // owner has toIssue0 rToken, addr1 has toIssue rToken. + if (toRedeem.lte(toIssue0)) { + await rToken.connect(owner).transfer(addr2.address, toRedeem) + } else { + await rToken.connect(owner).transfer(addr2.address, toIssue0) + await rToken.connect(addr1).transfer(addr2.address, toRedeem.sub(toIssue0)) + } + expect(await rToken.balanceOf(addr2.address)).to.equal(toRedeem) + + // ==== Redeem tokens + + await rToken.connect(addr2).redeem(toRedeem) + expect(await rToken.balanceOf(addr2.address)).to.equal(0) + } + + // ==== Generate the tests + const MAX_RTOKENS = bn('1e48') + const MAX_WEIGHT = fp(1000) + const MIN_WEIGHT = fp('1e-6') + const MIN_ISSUANCE_PCT = fp('1e-6') + const MIN_REDEMPTION_PCT = fp('1e-6') + const MIN_RTOKENS = fp('1e-6') + + let paramList + + if (SLOW) { + const bounds: BigNumber[][] = [ + [MIN_RTOKENS, MAX_RTOKENS, bn('1.205e24')], // toIssue + [MIN_RTOKENS, MAX_RTOKENS, bn('4.4231e24')], // toRedeem + [MAX_RTOKENS, bn('7.907e24')], // totalSupply + [bn(1), bn(3), bn(100)], // numAssets + [MIN_WEIGHT, MAX_WEIGHT, fp('0.1')], // weightFirst + [MIN_WEIGHT, MAX_WEIGHT, fp('0.2')], // weightRest + [MIN_ISSUANCE_PCT, fp('1e-2'), fp(1)], // issuanceThrottle.pctRate + [MIN_REDEMPTION_PCT, fp('1e-2'), fp(1)], // redemptionThrottle.pctRate + ] + + paramList = cartesianProduct(...bounds) + } else { + const bounds: BigNumber[][] = [ + [MIN_RTOKENS, MAX_RTOKENS], // toIssue + [MIN_RTOKENS, MAX_RTOKENS], // toRedeem + [MAX_RTOKENS], // totalSupply + [bn(1)], // numAssets + [MIN_WEIGHT, MAX_WEIGHT], // weightFirst + [MIN_WEIGHT], // weightRest + [MIN_ISSUANCE_PCT, fp(1)], // issuanceThrottle.pctRate + [MIN_REDEMPTION_PCT, fp(1)], // redemptionThrottle.pctRate + ] + paramList = cartesianProduct(...bounds) + } + const numCases = paramList.length.toString() + + paramList.forEach((params, index) => { + it(`case ${index + 1} of ${numCases}: ${params.map(shortString).join(' ')}`, async () => { + await runScenario(params) + }) + }) + }) +}) diff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts index 86cd3e88b..21225ecbc 100644 --- a/test/Recollateralization.test.ts +++ b/test/Recollateralization.test.ts @@ -19,7 +19,7 @@ import { ATokenFiatCollateral, CTokenMock, DutchTrade, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FacadeRead, FacadeTest, @@ -81,7 +81,7 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { let token0: ERC20Mock let token1: USDCMock let token2: StaticATokenMock - let token3Vault: CTokenVaultMock + let token3Vault: CTokenWrapperMock let token3: CTokenMock let backupToken1: ERC20Mock let backupToken2: ERC20Mock @@ -173,10 +173,10 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { token0 = erc20s[collateral.indexOf(basket[0])] token1 = erc20s[collateral.indexOf(basket[1])] token2 = erc20s[collateral.indexOf(basket[2])] - token3Vault = ( - await ethers.getContractAt('CTokenVaultMock', await basket[3].erc20()) + token3Vault = ( + await ethers.getContractAt('CTokenWrapperMock', await basket[3].erc20()) ) - token3 = await ethers.getContractAt('CTokenMock', await token3Vault.asset()) + token3 = await ethers.getContractAt('CTokenMock', await token3Vault.underlying()) // Set Aave revenue token await token2.setAaveToken(aaveToken.address) @@ -1009,6 +1009,20 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { }) }) + it('Should not settle trades if trading paused', async () => { + await main.connect(owner).pauseTrading() + await expect(backingManager.settleTrade(token0.address)).to.be.revertedWith( + 'frozen or trading paused' + ) + }) + + it('Should not settle trades if frozen', async () => { + await main.connect(owner).freezeShort() + await expect(backingManager.settleTrade(token0.address)).to.be.revertedWith( + 'frozen or trading paused' + ) + }) + context('Should successfully recollateralize after governance basket switch', () => { afterEach(async () => { // Should be fully capitalized again diff --git a/test/Revenues.test.ts b/test/Revenues.test.ts index 904cddcb6..4e4314224 100644 --- a/test/Revenues.test.ts +++ b/test/Revenues.test.ts @@ -4,7 +4,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { BigNumber, ContractFactory, Wallet } from 'ethers' import { ethers, upgrades } from 'hardhat' -import { IConfig } from '../common/configuration' +import { IConfig, GNOSIS_MAX_TOKENS } from '../common/configuration' import { BN_SCALE_FACTOR, FURNACE_DEST, @@ -12,6 +12,7 @@ import { ZERO_ADDRESS, CollateralStatus, TradeKind, + MAX_UINT192, } from '../common/constants' import { expectEvents } from '../common/events' import { bn, divCeil, fp, near } from '../common/numbers' @@ -20,7 +21,7 @@ import { ATokenFiatCollateral, ComptrollerMock, CTokenFiatCollateral, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FacadeTest, GnosisMock, @@ -88,7 +89,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { let token0: ERC20Mock let token1: USDCMock let token2: StaticATokenMock - let token3: CTokenVaultMock + let token3: CTokenWrapperMock let collateral0: FiatCollateral let collateral1: FiatCollateral let collateral2: ATokenFiatCollateral @@ -182,8 +183,8 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { token2 = ( await ethers.getContractAt('StaticATokenMock', await collateral2.erc20()) ) - token3 = ( - await ethers.getContractAt('CTokenVaultMock', await collateral3.erc20()) + token3 = ( + await ethers.getContractAt('CTokenWrapperMock', await collateral3.erc20()) ) // Mint initial balances @@ -449,6 +450,119 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { .withArgs(anyValue, token0.address, rToken.address, issueAmount, withinQuad(minBuyAmt)) }) + it('Should forward revenue to traders', async () => { + const rewardAmt = bn('100e18') + await token2.setRewards(backingManager.address, rewardAmt) + await backingManager.claimRewardsSingle(token2.address) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + await expect(backingManager.forwardRevenue([aaveToken.address])).to.emit( + aaveToken, + 'Transfer' + ) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(0) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(rewardAmt.mul(60).div(100)) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(rewardAmt.mul(40).div(100)) + }) + + it('Should not forward revenue if basket not ready', async () => { + const rewardAmt = bn('100e18') + await token2.setRewards(backingManager.address, rewardAmt) + await backingManager.claimRewardsSingle(token2.address) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + // Default a token and update basket status + await setOraclePrice(collateral0.address, bn('0.5e8')) + await collateral0.refresh() + expect(await collateral0.status()).to.equal(CollateralStatus.IFFY) + await basketHandler.trackStatus() + + // Cannot forward if not SOUND + await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( + 'basket not ready' + ) + + // Regain SOUND + await setOraclePrice(collateral0.address, bn('1e8')) + await collateral0.refresh() + expect(await collateral0.status()).to.equal(CollateralStatus.SOUND) + await basketHandler.trackStatus() + + // Still cannot forward revenue, in warmup period + await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( + 'basket not ready' + ) + + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + // Advance time post warmup period + await advanceTime(Number(config.warmupPeriod) + 1) + + // Now we can forward revenue successfully + await expect(backingManager.forwardRevenue([aaveToken.address])).to.emit( + aaveToken, + 'Transfer' + ) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(0) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(rewardAmt.mul(60).div(100)) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(rewardAmt.mul(40).div(100)) + }) + + it('Should not forward revenue if paused', async () => { + const rewardAmt = bn('100e18') + await token2.setRewards(backingManager.address, rewardAmt) + await backingManager.claimRewardsSingle(token2.address) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + // Pause + await main.connect(owner).pauseTrading() + + // Cannot forward revenue + await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( + 'frozen or trading paused' + ) + }) + + it('Should not forward revenue if frozen', async () => { + const rewardAmt = bn('100e18') + await token2.setRewards(backingManager.address, rewardAmt) + await backingManager.claimRewardsSingle(token2.address) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + // Pause + await main.connect(owner).freezeShort() + + // Cannot forward revenue + await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( + 'frozen or trading paused' + ) + }) + + it('Should forward RSR revenue directly to StRSR', async () => { + const amount = bn('2000e18') + await rsr.connect(owner).mint(backingManager.address, amount) + expect(await rsr.balanceOf(backingManager.address)).to.equal(amount) + expect(await rsr.balanceOf(stRSR.address)).to.equal(0) + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rsr.balanceOf(rTokenTrader.address)).to.equal(0) + + await expect(backingManager.forwardRevenue([rsr.address])).to.emit(rsr, 'Transfer') + expect(await rsr.balanceOf(backingManager.address)).to.equal(0) + expect(await rsr.balanceOf(stRSR.address)).to.equal(amount) + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rsr.balanceOf(rTokenTrader.address)).to.equal(0) + }) + it('Should not launch revenue auction if UNPRICED', async () => { await advanceTime(ORACLE_TIMEOUT.toString()) await rsr.connect(addr1).transfer(rTokenTrader.address, issueAmount) @@ -802,6 +916,53 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rToken.balanceOf(furnace.address)).to.equal(0) }) + it('Should handle GNOSIS_MAX_TOKENS cap in BATCH_AUCTION', async () => { + // Set Max trade volume very high for both assets in trade + const chainlinkFeed = ( + await (await ethers.getContractFactory('MockV3Aggregator')).deploy(8, bn('1e8')) + ) + const newSellAsset: Asset = ( + await AssetFactory.deploy( + PRICE_TIMEOUT, + chainlinkFeed.address, + ORACLE_ERROR, + aaveToken.address, + MAX_UINT192, + ORACLE_TIMEOUT + ) + ) + + const newRSRAsset: Asset = ( + await AssetFactory.deploy( + PRICE_TIMEOUT, + await rsrAsset.chainlinkFeed(), + ORACLE_ERROR, + rsr.address, + MAX_UINT192, + ORACLE_TIMEOUT + ) + ) + + // Perform asset swap + await assetRegistry.connect(owner).swapRegistered(newSellAsset.address) + await assetRegistry.connect(owner).swapRegistered(newRSRAsset.address) + await basketHandler.refreshBasket() + + // Set rewards manually + const amount = MAX_UINT192 + await aaveToken.connect(owner).mint(rsrTrader.address, amount) + + const p1RevenueTrader = await ethers.getContractAt('RevenueTraderP1', rsrTrader.address) + + await expect( + p1RevenueTrader.manageToken(aaveToken.address, TradeKind.BATCH_AUCTION) + ).to.emit(rsrTrader, 'TradeStarted') + + // Check trade is using the GNOSIS limits + const trade = await getTrade(rsrTrader, aaveToken.address) + expect(await trade.initBal()).to.equal(GNOSIS_MAX_TOKENS) + }) + it('Should claim AAVE and handle revenue auction correctly - small amount processed in single auction', async () => { rewardAmountAAVE = bn('0.5e18') @@ -2236,6 +2397,50 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { await rTokenTrader.manageToken(token1.address, TradeKind.DUTCH_AUCTION) }) + it('Should not return bid amount before auction starts', async () => { + await token0.connect(addr1).transfer(rTokenTrader.address, issueAmount) + await rTokenTrader.manageToken(token0.address, TradeKind.DUTCH_AUCTION) + + const trade = await ethers.getContractAt( + 'DutchTrade', + await rTokenTrader.trades(token0.address) + ) + + // Cannot get bid amount yet + await expect( + trade.connect(addr1).bidAmount(await getLatestBlockTimestamp()) + ).to.be.revertedWith('auction not started') + + // Advance to start time + const start = await trade.startTime() + await advanceToTimestamp(start) + + // Now we can get bid amount + const actual = await trade.connect(addr1).bidAmount(await getLatestBlockTimestamp()) + expect(actual).to.be.gt(bn(0)) + }) + + it('Should allow one bidder', async () => { + await token0.connect(addr1).transfer(rTokenTrader.address, issueAmount.div(2)) + await rTokenTrader.manageToken(token0.address, TradeKind.DUTCH_AUCTION) + + const trade = await ethers.getContractAt( + 'DutchTrade', + await rTokenTrader.trades(token0.address) + ) + + // Advance to auction on-going + await advanceToTimestamp((await trade.endTime()) - 1000) + + // Bid + await rToken.connect(addr1).approve(trade.address, initialBal) + await trade.connect(addr1).bid() + expect(await trade.bidder()).to.equal(addr1.address) + + // Cannot bid once is settled + await expect(trade.connect(addr1).bid()).to.be.revertedWith('bid already received') + }) + it('Should quote piecewise-falling price correctly throughout entirety of auction', async () => { issueAmount = issueAmount.div(10000) await token0.connect(addr1).transfer(rTokenTrader.address, issueAmount) @@ -2277,6 +2482,7 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { await rTokenTrader.trades(token0.address) ) await rToken.connect(addr1).approve(trade.address, initialBal) + await advanceToTimestamp((await trade.endTime()) + 1) await expect( trade.connect(addr1).bidAmount(await getLatestBlockTimestamp()) @@ -3114,6 +3320,46 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rToken.totalSupply()).to.equal(mintAmt.mul(2)) expect(await rToken.basketsNeeded()).to.equal(mintAmt.mul(2)) }) + + it('Should not forward revenue before trading delay', async () => { + // Set trading delay + const newDelay = 3600 + await backingManager.connect(owner).setTradingDelay(newDelay) // 1 hour + + const rewardAmt = bn('100e18') + await token2.setRewards(backingManager.address, rewardAmt) + await backingManager.claimRewardsSingle(token2.address) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + // Switch basket + await basketHandler.connect(owner).setPrimeBasket([token1.address], [fp('1')]) + await expect(basketHandler.connect(owner).refreshBasket()) + .to.emit(basketHandler, 'BasketSet') + .withArgs(3, [token1.address], [fp('1')], false) + + // Cannot forward revenue yet + await expect(backingManager.forwardRevenue([aaveToken.address])).to.be.revertedWith( + 'trading delayed' + ) + + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(rewardAmt) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(0) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(0) + + // Advance time post trading delay + await advanceTime(newDelay + 1) + + // Now we can forward revenue successfully + await expect(backingManager.forwardRevenue([aaveToken.address])).to.emit( + aaveToken, + 'Transfer' + ) + expect(await aaveToken.balanceOf(backingManager.address)).to.equal(0) + expect(await aaveToken.balanceOf(rsrTrader.address)).to.equal(rewardAmt.mul(60).div(100)) + expect(await aaveToken.balanceOf(rTokenTrader.address)).to.equal(rewardAmt.mul(40).div(100)) + }) }) }) diff --git a/test/ZTradingExtremes.test.ts b/test/ZTradingExtremes.test.ts index 103afb886..537ba8176 100644 --- a/test/ZTradingExtremes.test.ts +++ b/test/ZTradingExtremes.test.ts @@ -11,7 +11,7 @@ import { ATokenFiatCollateral, ComptrollerMock, CTokenFiatCollateral, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FacadeTest, FiatCollateral, @@ -76,7 +76,7 @@ describeExtreme(`Trading Extreme Values (${SLOW ? 'slow mode' : 'fast mode'})`, let ERC20Mock: ContractFactory let ATokenMockFactory: ContractFactory - let CTokenVaultMockFactory: ContractFactory + let CTokenWrapperMockFactory: ContractFactory let ATokenCollateralFactory: ContractFactory let CTokenCollateralFactory: ContractFactory @@ -110,7 +110,7 @@ describeExtreme(`Trading Extreme Values (${SLOW ? 'slow mode' : 'fast mode'})`, ERC20Mock = await ethers.getContractFactory('ERC20Mock') ATokenMockFactory = await ethers.getContractFactory('StaticATokenMock') - CTokenVaultMockFactory = await ethers.getContractFactory('CTokenVaultMock') + CTokenWrapperMockFactory = await ethers.getContractFactory('CTokenWrapperMock') ATokenCollateralFactory = await ethers.getContractFactory('ATokenFiatCollateral') CTokenCollateralFactory = await ethers.getContractFactory('CTokenFiatCollateral') @@ -157,12 +157,12 @@ describeExtreme(`Trading Extreme Values (${SLOW ? 'slow mode' : 'fast mode'})`, return erc20 } - const prepCToken = async (index: number): Promise => { + const prepCToken = async (index: number): Promise => { const underlying: ERC20Mock = ( await ERC20Mock.deploy(`ERC20_NAME:${index}`, `ERC20_SYM:${index}`) ) - const erc20: CTokenVaultMock = ( - await CTokenVaultMockFactory.deploy( + const erc20: CTokenWrapperMock = ( + await CTokenWrapperMockFactory.deploy( `CToken_NAME:${index}`, `CToken_SYM:${index}`, underlying.address, diff --git a/test/ZZStRSR.test.ts b/test/ZZStRSR.test.ts index ecc31f682..9f7503df5 100644 --- a/test/ZZStRSR.test.ts +++ b/test/ZZStRSR.test.ts @@ -21,7 +21,7 @@ import { TestIMain, TestIRToken, TestIStRSR, - CTokenVaultMock, + CTokenWrapperMock, } from '../typechain' import { IConfig, MAX_RATIO, MAX_UNSTAKING_DELAY } from '../common/configuration' import { CollateralStatus, MAX_UINT256, ONE_PERIOD, ZERO_ADDRESS } from '../common/constants' @@ -79,7 +79,7 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { let token0: ERC20Mock let token1: ERC20Mock let token2: StaticATokenMock - let token3: CTokenVaultMock + let token3: CTokenWrapperMock let collateral0: Collateral let collateral1: Collateral let collateral2: Collateral @@ -178,8 +178,8 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { token2 = ( await ethers.getContractAt('StaticATokenMock', await collateral2.erc20()) ) - token3 = ( - await ethers.getContractAt('CTokenVaultMock', await collateral3.erc20()) + token3 = ( + await ethers.getContractAt('CTokenWrapperMock', await collateral3.erc20()) ) }) @@ -668,6 +668,12 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { // Exchange rate remains steady expect(await stRSR.exchangeRate()).to.equal(fp('1')) + // Cancelling the unstake with invalid index does nothing + await expect(stRSR.connect(addr1).cancelUnstake(0)).to.not.emit(stRSR, 'UnstakingCancelled') + await expect(stRSR.connect(addr1).cancelUnstake(2)).to.be.revertedWith('index out-of-bounds') + expect(await stRSR.balanceOf(addr1.address)).to.equal(0) + expect(await stRSR.totalSupply()).to.equal(0) + // Let's cancel the unstake await expect(stRSR.connect(addr1).cancelUnstake(1)).to.emit(stRSR, 'UnstakingCancelled') @@ -1165,6 +1171,12 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { expect(await stRSR.endIdForWithdraw(addr1.address)).to.equal(1) expect(await stRSR.endIdForWithdraw(addr2.address)).to.equal(3) + // Cancelling the unstake with invalid index does nothing + await expect(stRSR.connect(addr2).cancelUnstake(1)).to.not.emit(stRSR, 'UnstakingCancelled') + await expect(stRSR.connect(addr2).cancelUnstake(4)).to.be.revertedWith( + 'index out-of-bounds' + ) + // Withdraw await stRSR .connect(addr1) @@ -2000,6 +2012,74 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { expect(await stRSR.totalSupply()).to.equal(amount.sub(one)) }) + it('Should handle cancel unstake after a significant RSR seizure', async () => { + stkWithdrawalDelay = bn(await stRSR.unstakingDelay()).toNumber() + + const unstakeAmount: BigNumber = fp('1e-9') + const amount: BigNumber = bn('1e18').add(unstakeAmount).add(1) + + // Stake enough for 2 unstakings + await rsr.connect(addr1).approve(stRSR.address, amount.add(1)) + await stRSR.connect(addr1).stake(amount.add(1)) + + // Check balances and stakes + expect(await rsr.balanceOf(stRSR.address)).to.equal(amount.add(1)) + expect(await rsr.balanceOf(stRSR.address)).to.equal(await stRSR.totalSupply()) + expect(await rsr.balanceOf(addr1.address)).to.equal(initialBal.sub(amount.add(1))) + expect(await stRSR.balanceOf(addr1.address)).to.equal(amount.add(1)) + + // Unstake twice + const availableAt = (await getLatestBlockTimestamp()) + config.unstakingDelay.toNumber() + 1 + // Set next block timestamp - for deterministic result + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 1) + + await expect(stRSR.connect(addr1).unstake(1)) + .emit(stRSR, 'UnstakingStarted') + .withArgs(0, 1, addr1.address, 1, 1, availableAt) + await expect(stRSR.connect(addr1).unstake(unstakeAmount)) + .emit(stRSR, 'UnstakingStarted') + .withArgs(1, 1, addr1.address, unstakeAmount, unstakeAmount, availableAt + 1) + + // Check balances and stakes + expect(await rsr.balanceOf(stRSR.address)).to.equal(amount.add(1)) + expect(await rsr.balanceOf(addr1.address)).to.equal(initialBal.sub(amount.add(1))) + + // All staked funds withdrawn upfront + expect(await stRSR.balanceOf(addr1.address)).to.equal(amount.sub(unstakeAmount)) + expect(await stRSR.totalSupply()).to.equal(amount.sub(unstakeAmount)) + + // Rate does not change + expect(await stRSR.exchangeRate()).to.equal(fp('1')) + + // Seize most of the RSR + const seizeAmt = fp('0.99999999').mul(amount).div(fp('1')).add(1) + const exchangeRate = fp('1e-8') + await whileImpersonating(backingManager.address, async (signer) => { + await expect(stRSR.connect(signer).seizeRSR(seizeAmt)).to.emit(stRSR, 'ExchangeRateSet') + }) + + // Check new rate + expect(await stRSR.exchangeRate()).to.be.closeTo(exchangeRate, bn(10)) + + // Check balances and stakes + expect(await rsr.balanceOf(stRSR.address)).to.equal(exchangeRate.add(10)) + expect(await stRSR.totalSupply()).to.equal(amount.sub(unstakeAmount)) + expect(await rsr.balanceOf(addr1.address)).to.equal(initialBal.sub(amount.add(1))) + expect(await stRSR.balanceOf(addr1.address)).to.equal(amount.sub(unstakeAmount)) + + // Move forward past stakingWithdrawalDelay + await setNextBlockTimestamp(Number(await getLatestBlockTimestamp()) + stkWithdrawalDelay) + + // Cancel the larger unstake -- should round down to 0 + await stRSR.connect(addr1).cancelUnstake(1) + + // Check balances and stakes - No changes + expect(await rsr.balanceOf(stRSR.address)).to.equal(exchangeRate.add(10)) + expect(await stRSR.totalSupply()).to.equal(amount.sub(unstakeAmount)) + expect(await rsr.balanceOf(addr1.address)).to.equal(initialBal.sub(amount.add(1))) + expect(await stRSR.balanceOf(addr1.address)).to.equal(amount.sub(unstakeAmount)) + }) + it('Should not allow stakeRate manipulation', async () => { // send RSR to stRSR (attempt to manipulate stake rate) await rsr.connect(addr1).transfer(stRSR.address, fp('200')) @@ -2092,13 +2172,13 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { // Attempt to send to zero address await expect(stRSR.connect(addr1).transfer(ZERO_ADDRESS, amount)).to.be.revertedWith( - 'ERC20: transfer to the zero address' + 'ERC20: transfer to or from the zero address' ) // Attempt to send from zero address - Impersonation is the only way to get to this validation await whileImpersonating(ZERO_ADDRESS, async (signer) => { await expect(stRSR.connect(signer).transfer(addr2.address, amount)).to.be.revertedWith( - 'ERC20: transfer from the zero address' + 'ERC20: transfer to or from the zero address' ) }) @@ -2305,13 +2385,13 @@ describe(`StRSRP${IMPLEMENTATION} contract`, () => { // Attempt to set allowance to zero address await expect(stRSR.connect(addr1).approve(ZERO_ADDRESS, amount)).to.be.revertedWith( - 'ERC20: approve to the zero address' + 'ERC20: approve to or from the zero address' ) // Attempt set allowance from zero address - Impersonation is the only way to get to this validation await whileImpersonating(ZERO_ADDRESS, async (signer) => { await expect(stRSR.connect(signer).approve(addr2.address, amount)).to.be.revertedWith( - 'ERC20: approve from the zero address' + 'ERC20: approve to or from the zero address' ) }) diff --git a/test/fixtures.ts b/test/fixtures.ts index ac8db13cc..2f20d2a66 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -17,7 +17,7 @@ import { ComptrollerMock, CTokenFiatCollateral, CTokenSelfReferentialCollateral, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, DeployerP0, DeployerP1, @@ -165,7 +165,9 @@ async function collateralFixture( const ERC20: ContractFactory = await ethers.getContractFactory('ERC20Mock') const USDC: ContractFactory = await ethers.getContractFactory('USDCMock') const ATokenMockFactory: ContractFactory = await ethers.getContractFactory('StaticATokenMock') - const CTokenVaultMockFactory: ContractFactory = await ethers.getContractFactory('CTokenVaultMock') + const CTokenWrapperMockFactory: ContractFactory = await ethers.getContractFactory( + 'CTokenWrapperMock' + ) const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory('FiatCollateral') const ATokenCollateralFactory = await ethers.getContractFactory('ATokenFiatCollateral') const CTokenCollateralFactory = await ethers.getContractFactory('CTokenFiatCollateral') @@ -240,9 +242,9 @@ async function collateralFixture( referenceERC20: ERC20Mock, chainlinkAddr: string, compToken: ERC20Mock - ): Promise<[CTokenVaultMock, CTokenFiatCollateral]> => { - const erc20: CTokenVaultMock = ( - await CTokenVaultMockFactory.deploy( + ): Promise<[CTokenWrapperMock, CTokenFiatCollateral]> => { + const erc20: CTokenWrapperMock = ( + await CTokenWrapperMockFactory.deploy( symbol + ' Token', symbol, referenceERC20.address, diff --git a/test/integration/AssetPlugins.test.ts b/test/integration/AssetPlugins.test.ts index 963812500..672f5566d 100644 --- a/test/integration/AssetPlugins.test.ts +++ b/test/integration/AssetPlugins.test.ts @@ -46,7 +46,7 @@ import { TestIRToken, USDCMock, WETH9, - CTokenVault, + CTokenWrapper, } from '../../typechain' import { useEnv } from '#/utils/env' @@ -110,20 +110,20 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, let stataUsdp: StaticATokenLM let cDai: CTokenMock - let cDaiVault: CTokenVault + let cDaiVault: CTokenWrapper let cUsdc: CTokenMock - let cUsdcVault: CTokenVault + let cUsdcVault: CTokenWrapper let cUsdt: CTokenMock - let cUsdtVault: CTokenVault + let cUsdtVault: CTokenWrapper let cUsdp: CTokenMock - let cUsdpVault: CTokenVault + let cUsdpVault: CTokenWrapper let wbtc: ERC20Mock let cWBTC: CTokenMock - let cWBTCVault: CTokenVault + let cWBTCVault: CTokenWrapper let weth: ERC20Mock let cETH: CTokenMock - let cETHVault: CTokenVault + let cETHVault: CTokenWrapper let eurt: ERC20Mock let daiCollateral: FiatCollateral @@ -228,44 +228,59 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, busd = erc20s[3] // BUSD usdp = erc20s[4] // USDP tusd = erc20s[5] // TUSD - cDaiVault = erc20s[6] // cDAI - cDai = await ethers.getContractAt('CTokenMock', await cDaiVault.asset()) // cDAI - cUsdcVault = erc20s[7] // cUSDC - cUsdc = await ethers.getContractAt('CTokenMock', await cUsdcVault.asset()) // cUSDC - cUsdtVault = erc20s[8] // cUSDT - cUsdt = await ethers.getContractAt('CTokenMock', await cUsdtVault.asset()) // cUSDT - cUsdpVault = erc20s[9] // cUSDT - cUsdp = await ethers.getContractAt('CTokenMock', await cUsdpVault.asset()) // cUSDT + cDaiVault = erc20s[6] // cDAI + cDai = await ethers.getContractAt('CTokenMock', await cDaiVault.underlying()) // cDAI + cUsdcVault = erc20s[7] // cUSDC + cUsdc = await ethers.getContractAt('CTokenMock', await cUsdcVault.underlying()) // cUSDC + cUsdtVault = erc20s[8] // cUSDT + cUsdt = await ethers.getContractAt('CTokenMock', await cUsdtVault.underlying()) // cUSDT + cUsdpVault = erc20s[9] // cUSDT + cUsdp = await ethers.getContractAt('CTokenMock', await cUsdpVault.underlying()) // cUSDT stataDai = erc20s[10] // static aDAI stataUsdc = erc20s[11] // static aUSDC stataUsdt = erc20s[12] // static aUSDT stataBusd = erc20s[13] // static aBUSD stataUsdp = erc20s[14] // static aUSDP wbtc = erc20s[15] // wBTC - cWBTCVault = erc20s[16] // cWBTC - cWBTC = await ethers.getContractAt('CTokenMock', await cWBTCVault.asset()) // cWBTC + cWBTCVault = erc20s[16] // cWBTC + cWBTC = await ethers.getContractAt('CTokenMock', await cWBTCVault.underlying()) // cWBTC weth = erc20s[17] // wETH - cETHVault = erc20s[18] // cETH - cETH = await ethers.getContractAt('CTokenMock', await cETHVault.asset()) // cETH + cETHVault = erc20s[18] // cETH + cETH = await ethers.getContractAt('CTokenMock', await cETHVault.underlying()) // cETH eurt = erc20s[19] // eurt // Get plain aTokens aDai = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aDAI || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) ) aUsdc = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aUSDC || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aUSDC || '' + ) ) aUsdt = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aUSDT || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aUSDT || '' + ) ) aBusd = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aBUSD || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aBUSD || '' + ) ) aUsdp = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aUSDP || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aUSDP || '' + ) ) // Get collaterals daiCollateral = collateral[0] // DAI @@ -301,7 +316,10 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, // Get plain aToken aDai = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aDAI || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) ) // Setup balances for addr1 - Transfer from Mainnet holders DAI, cDAI and aDAI (for default basket) @@ -321,7 +339,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await whileImpersonating(holderCDAI, async (cdaiSigner) => { await cDai.connect(cdaiSigner).transfer(addr1.address, toBNDecimals(initialBal, 8).mul(100)) await cDai.connect(addr1).approve(cDaiVault.address, toBNDecimals(initialBal, 8).mul(100)) - await cDaiVault.connect(addr1).mint(toBNDecimals(initialBal, 17).mul(100), addr1.address) + await cDaiVault.connect(addr1).deposit(toBNDecimals(initialBal, 8).mul(100), addr1.address) }) // Setup balances for USDT @@ -346,7 +364,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, .approve(cWBTCVault.address, toBNDecimals(initialBalBtcEth, 8).mul(1000)) await cWBTCVault .connect(addr1) - .mint(toBNDecimals(initialBalBtcEth, 17).mul(1000), addr1.address) + .deposit(toBNDecimals(initialBalBtcEth, 8).mul(1000), addr1.address) }) // WETH @@ -364,7 +382,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, .approve(cETHVault.address, toBNDecimals(initialBalBtcEth, 8).mul(1000)) await cETHVault .connect(addr1) - .mint(toBNDecimals(initialBalBtcEth, 17).mul(1000), addr1.address) + .deposit(toBNDecimals(initialBalBtcEth, 8).mul(1000), addr1.address) }) //EURT @@ -513,7 +531,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, token: ERC20Mock tokenAddress: string cToken: CTokenMock - cTokenVault: CTokenVault + cTokenVault: CTokenWrapper cTokenAddress: string cTokenCollateral: CTokenFiatCollateral pegPrice: BigNumber @@ -572,9 +590,9 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await ctkInf.token.decimals() ) expect(await ctkInf.cTokenCollateral.erc20()).to.equal(ctkInf.cTokenVault.address) - expect(await ctkInf.cTokenVault.asset()).to.equal(ctkInf.cTokenAddress) + expect(await ctkInf.cTokenVault.underlying()).to.equal(ctkInf.cTokenAddress) expect(await ctkInf.cToken.decimals()).to.equal(8) - expect(await ctkInf.cTokenVault.decimals()).to.equal(17) + expect(await ctkInf.cTokenVault.decimals()).to.equal(8) expect(await ctkInf.cTokenCollateral.targetName()).to.equal( ethers.utils.formatBytes32String('USD') ) @@ -802,7 +820,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, token: ERC20Mock tokenAddress: string cToken: CTokenMock - cTokenVault: CTokenVault + cTokenVault: CTokenWrapper cTokenAddress: string cTokenCollateral: CTokenNonFiatCollateral targetPrice: BigNumber @@ -835,9 +853,9 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await ctkInf.token.decimals() ) expect(await ctkInf.cTokenCollateral.erc20()).to.equal(ctkInf.cTokenVault.address) - expect(await ctkInf.cTokenVault.asset()).to.equal(ctkInf.cTokenAddress) + expect(await ctkInf.cTokenVault.underlying()).to.equal(ctkInf.cTokenAddress) expect(await ctkInf.cToken.decimals()).to.equal(8) - expect(await ctkInf.cTokenVault.decimals()).to.equal(17) + expect(await ctkInf.cTokenVault.decimals()).to.equal(8) expect(await ctkInf.cTokenCollateral.targetName()).to.equal( ethers.utils.formatBytes32String(ctkInf.targetName) ) @@ -933,7 +951,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, token: ERC20Mock tokenAddress: string cToken: CTokenMock - cTokenVault: CTokenVault + cTokenVault: CTokenWrapper cTokenAddress: string cTokenCollateral: CTokenSelfReferentialCollateral price: BigNumber @@ -964,9 +982,9 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await ctkInf.token.decimals() ) expect(await ctkInf.cTokenCollateral.erc20()).to.equal(ctkInf.cTokenVault.address) - expect(await ctkInf.cTokenVault.asset()).to.equal(ctkInf.cTokenAddress) + expect(await ctkInf.cTokenVault.underlying()).to.equal(ctkInf.cTokenAddress) expect(await ctkInf.cToken.decimals()).to.equal(8) - expect(await ctkInf.cTokenVault.decimals()).to.equal(17) + expect(await ctkInf.cTokenVault.decimals()).to.equal(8) expect(await ctkInf.cTokenCollateral.targetName()).to.equal( ethers.utils.formatBytes32String(ctkInf.targetName) ) @@ -1750,7 +1768,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await stataDai.connect(addr1).approve(rToken.address, issueAmount) await cDaiVault .connect(addr1) - .approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + .approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) await expect(rToken.connect(addr1).issue(issueAmount)).to.emit(rToken, 'Issuance') await expectRTokenPrice( @@ -1762,7 +1780,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, ) }) - it('Should issue/reedem correctly with simple basket ', async function () { + it('Should issue/reedem correctly with simple basket', async function () { const MIN_ISSUANCE_PER_BLOCK = bn('10000e18') const issueAmount: BigNumber = MIN_ISSUANCE_PER_BLOCK @@ -1776,7 +1794,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, const initialBalAToken = initialBal.mul(9321).div(10000) expect(await stataDai.balanceOf(addr1.address)).to.be.closeTo(initialBalAToken, fp('1.5')) expect(await cDaiVault.balanceOf(addr1.address)).to.equal( - toBNDecimals(initialBal, 17).mul(100) + toBNDecimals(initialBal, 8).mul(100) ) // Provide approvals @@ -1784,7 +1802,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await stataDai.connect(addr1).approve(rToken.address, issueAmount) await cDaiVault .connect(addr1) - .approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + .approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) // Check rToken balance expect(await rToken.balanceOf(addr1.address)).to.equal(0) @@ -1801,10 +1819,10 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, issueAmtAToken, fp('1') ) - const requiredCTokens: BigNumber = bn('227116e17') // approx 227K needed (~5K, 50% of basket) - Price: ~0.022 + const requiredCTokens: BigNumber = bn('227116e8') // approx 227K needed (~5K, 50% of basket) - Price: ~0.022 expect(await cDaiVault.balanceOf(backingManager.address)).to.be.closeTo( requiredCTokens, - bn('1e17') + bn('1e8') ) // Balances for user @@ -1814,8 +1832,8 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, fp('1.5') ) expect(await cDaiVault.balanceOf(addr1.address)).to.be.closeTo( - toBNDecimals(initialBal, 17).mul(100).sub(requiredCTokens), - bn('1e17') + toBNDecimals(initialBal, 8).mul(100).sub(requiredCTokens), + bn('1e8') ) // Check RTokens issued to user expect(await rToken.balanceOf(addr1.address)).to.equal(issueAmount) @@ -1844,7 +1862,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, expect(await dai.balanceOf(addr1.address)).to.equal(initialBal) expect(await stataDai.balanceOf(addr1.address)).to.be.closeTo(initialBalAToken, fp('1.5')) expect(await cDaiVault.balanceOf(addr1.address)).to.be.closeTo( - toBNDecimals(initialBal, 17).mul(100), + toBNDecimals(initialBal, 8).mul(100), bn('1e16') ) @@ -1864,7 +1882,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await stataDai.connect(addr1).approve(rToken.address, issueAmount) await cDaiVault .connect(addr1) - .approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + .approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) // Issue rTokens await expect(rToken.connect(addr1).issue(issueAmount)).to.emit(rToken, 'Issuance') @@ -2012,7 +2030,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, // Check received tokens represent ~10K in value at current prices expect(newBalanceAddr1Dai.sub(balanceAddr1Dai)).to.equal(issueAmount.div(4)) // = 2.5K (25% of basket) expect(newBalanceAddr1aDai.sub(balanceAddr1aDai)).to.be.closeTo(fp('2110.5'), fp('0.5')) // ~1.1873 * 2110.5 ~= 2.5K (25% of basket) - expect(newBalanceAddr1cDai.sub(balanceAddr1cDai)).to.be.closeTo(bn('151785e17'), bn('5e16')) // ~0.03294 * 151785.3 ~= 5K (50% of basket) + expect(newBalanceAddr1cDai.sub(balanceAddr1cDai)).to.be.closeTo(bn('151785e8'), bn('5e16')) // ~0.03294 * 151785.3 ~= 5K (50% of basket) // Check remainders in Backing Manager expect(await dai.balanceOf(backingManager.address)).to.equal(0) @@ -2021,7 +2039,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, fp('0.01') ) expect(await cDaiVault.balanceOf(backingManager.address)).to.be.closeTo( - bn('75331e17'), + bn('75331e8'), bn('5e16') ) // ~= 2481 usd in value @@ -2060,7 +2078,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await stataDai.connect(addr1).approve(rToken.address, issueAmount) await cDaiVault .connect(addr1) - .approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + .approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) // Check rToken balance expect(await rToken.balanceOf(addr1.address)).to.equal(0) @@ -2190,11 +2208,11 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, expect(await wbtc.balanceOf(addr1.address)).to.equal(toBNDecimals(initialBalBtcEth, 8)) expect(await cWBTCVault.balanceOf(addr1.address)).to.equal( - toBNDecimals(initialBalBtcEth, 17).mul(1000) + toBNDecimals(initialBalBtcEth, 8).mul(1000) ) expect(await weth.balanceOf(addr1.address)).to.equal(initialBalBtcEth) expect(await cETHVault.balanceOf(addr1.address)).to.equal( - toBNDecimals(initialBalBtcEth, 17).mul(1000) + toBNDecimals(initialBalBtcEth, 8).mul(1000) ) expect(await eurt.balanceOf(addr1.address)).to.equal( toBNDecimals(initialBalBtcEth, 6).mul(1000) @@ -2206,13 +2224,13 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, // Check Balances after expect(await wbtc.balanceOf(backingManager.address)).to.equal(toBNDecimals(issueAmount, 8)) //1 full units - const requiredCWBTC: BigNumber = toBNDecimals(fp('49.85'), 17) // approx 49.5 cWBTC needed (~1 wbtc / 0.02006) + const requiredCWBTC: BigNumber = toBNDecimals(fp('49.85'), 8) // approx 49.5 cWBTC needed (~1 wbtc / 0.02006) expect(await cWBTCVault.balanceOf(backingManager.address)).to.be.closeTo( requiredCWBTC, point1Pct(requiredCWBTC) ) expect(await weth.balanceOf(backingManager.address)).to.equal(issueAmount) //1 full units - const requiredCETH: BigNumber = toBNDecimals(fp('49.8'), 17) // approx 49.8 cETH needed (~1 weth / 0.02020) + const requiredCETH: BigNumber = toBNDecimals(fp('49.8'), 8) // approx 49.8 cETH needed (~1 weth / 0.02020) expect(await cETHVault.balanceOf(backingManager.address)).to.be.closeTo( requiredCETH, point1Pct(requiredCETH) @@ -2223,13 +2241,13 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, expect(await wbtc.balanceOf(addr1.address)).to.equal( toBNDecimals(initialBalBtcEth.sub(issueAmount), 8) ) - const expectedcWBTCBalance = toBNDecimals(initialBalBtcEth, 17).mul(1000).sub(requiredCWBTC) + const expectedcWBTCBalance = toBNDecimals(initialBalBtcEth, 8).mul(1000).sub(requiredCWBTC) expect(await cWBTCVault.balanceOf(addr1.address)).to.be.closeTo( expectedcWBTCBalance, point1Pct(expectedcWBTCBalance) ) expect(await weth.balanceOf(addr1.address)).to.equal(initialBalBtcEth.sub(issueAmount)) - const expectedcETHBalance = toBNDecimals(initialBalBtcEth, 17).mul(1000).sub(requiredCETH) + const expectedcETHBalance = toBNDecimals(initialBalBtcEth, 8).mul(1000).sub(requiredCETH) expect(await cWBTCVault.balanceOf(addr1.address)).to.be.closeTo( expectedcETHBalance, point1Pct(expectedcETHBalance) @@ -2266,12 +2284,12 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, // Check funds returned to user expect(await wbtc.balanceOf(addr1.address)).to.equal(toBNDecimals(initialBalBtcEth, 8)) expect(await cWBTCVault.balanceOf(addr1.address)).to.be.closeTo( - toBNDecimals(initialBalBtcEth, 17).mul(1000), + toBNDecimals(initialBalBtcEth, 8).mul(1000), bn('10e9') ) expect(await weth.balanceOf(addr1.address)).to.equal(initialBalBtcEth) expect(await cETHVault.balanceOf(addr1.address)).to.be.closeTo( - toBNDecimals(initialBalBtcEth, 17).mul(1000), + toBNDecimals(initialBalBtcEth, 8).mul(1000), bn('10e9') ) expect(await eurt.balanceOf(addr1.address)).to.equal( @@ -2493,14 +2511,17 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, stataDai = ( await ethers.getContractAt('StaticATokenLM', await aDaiCollateral.erc20()) ) - cDaiVault = ( - await ethers.getContractAt('CTokenVault', await cDaiCollateral.erc20()) + cDaiVault = ( + await ethers.getContractAt('CTokenWrapper', await cDaiCollateral.erc20()) ) - cDai = await ethers.getContractAt('CTokenMock', await cDaiVault.asset()) + cDai = await ethers.getContractAt('CTokenMock', await cDaiVault.underlying()) // Get plain aToken aDai = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aDAI || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) ) // Setup balances for addr1 - Transfer from Mainnet holders DAI, cDAI and aDAI (for default basket) @@ -2520,7 +2541,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, await whileImpersonating(holderCDAI, async (cdaiSigner) => { await cDai.connect(cdaiSigner).transfer(addr1.address, toBNDecimals(initialBal, 8).mul(100)) await cDai.connect(addr1).approve(cDaiVault.address, toBNDecimals(initialBal, 8).mul(100)) - await cDaiVault.connect(addr1).mint(toBNDecimals(initialBal, 17).mul(100), addr1.address) + await cDaiVault.connect(addr1).deposit(toBNDecimals(initialBal, 8).mul(100), addr1.address) }) }) @@ -2554,7 +2575,7 @@ describeFork(`Asset Plugins - Integration - Mainnet Forking P${IMPLEMENTATION}`, // Provide approvals for issuances await dai.connect(addr1).approve(rToken.address, issueAmount) await stataDai.connect(addr1).approve(rToken.address, issueAmount) - await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) // Issue rTokens await expect(rToken.connect(addr1).issue(issueAmount)).to.emit(rToken, 'Issuance') diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index c11414f9a..fa2a57358 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -156,7 +156,7 @@ async function collateralFixture( throw new Error(`Missing network configuration for ${hre.network.name}`) } - const CTokenVaultFactory = await ethers.getContractFactory('CTokenVault') + const CTokenWrapperFactory = await ethers.getContractFactory('CTokenWrapper') const StaticATokenFactory: ContractFactory = await ethers.getContractFactory('StaticATokenLM') const FiatCollateralFactory: ContractFactory = await ethers.getContractFactory('FiatCollateral') @@ -206,7 +206,7 @@ async function collateralFixture( const erc20: IERC20Metadata = ( await ethers.getContractAt('CTokenMock', tokenAddress) ) - const vault = await CTokenVaultFactory.deploy( + const vault = await CTokenWrapperFactory.deploy( erc20.address, `${await erc20.name()} Vault`, `${await erc20.symbol()}-VAULT`, @@ -301,7 +301,7 @@ async function collateralFixture( const erc20: IERC20Metadata = ( await ethers.getContractAt('CTokenMock', tokenAddress) ) - const vault = await CTokenVaultFactory.deploy( + const vault = await CTokenWrapperFactory.deploy( erc20.address, `${await erc20.name()} Vault`, `${await erc20.symbol()}-VAULT`, @@ -357,7 +357,7 @@ async function collateralFixture( const erc20: IERC20Metadata = ( await ethers.getContractAt('CTokenMock', tokenAddress) ) - const vault = await CTokenVaultFactory.deploy( + const vault = await CTokenWrapperFactory.deploy( erc20.address, `${await erc20.name()} Vault`, `${await erc20.symbol()}-VAULT`, diff --git a/test/integration/mainnet-test/StaticATokens.test.ts b/test/integration/mainnet-test/StaticATokens.test.ts index efc3bdada..3a363fea7 100644 --- a/test/integration/mainnet-test/StaticATokens.test.ts +++ b/test/integration/mainnet-test/StaticATokens.test.ts @@ -103,7 +103,10 @@ describeFork(`Static ATokens - Mainnet Check - Mainnet Forking P${IMPLEMENTATION // Get plain aTokens aDai = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aDAI || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) ) // Get collaterals @@ -159,18 +162,30 @@ describeFork(`Static ATokens - Mainnet Check - Mainnet Forking P${IMPLEMENTATION // Get plain aTokens aDai = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aDAI || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) ) aUsdc = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aUSDC || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aUSDC || '' + ) ) aUsdt = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aUSDT || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aUSDT || '' + ) ) aBusd = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aBUSD || '') + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aBUSD || '' + ) ) // Set balance amount @@ -260,7 +275,7 @@ describeFork(`Static ATokens - Mainnet Check - Mainnet Forking P${IMPLEMENTATION }) // Wrap aUSDC - Underlying = false - expect(await aUsdc.balanceOf(addr1.address)).to.equal(initialBalUSDC) + expect(await aUsdc.balanceOf(addr1.address)).to.be.closeTo(initialBalUSDC, 1) expect(await stataUsdc.balanceOf(addr1.address)).to.equal(0) // Wrap aUSDC into a staticaUSDC diff --git a/test/libraries/Fixed.test.ts b/test/libraries/Fixed.test.ts index 91c5d5580..2b8ca66eb 100644 --- a/test/libraries/Fixed.test.ts +++ b/test/libraries/Fixed.test.ts @@ -504,7 +504,7 @@ describe('In FixLib,', () => { const table = commutes.flatMap(([a, b, c]) => [[a, b, c]]) for (const [a, b, c] of table) { expect(await caller.mul(fp(a), fp(b)), `mul(fp(${a}), fp(${b}))`).to.equal(fp(c)) - expect(await caller.safeMul_(fp(a), fp(b), ROUND), `safeMul(fp(${a}), fp(${b}))`).to.equal( + expect(await caller.safeMul(fp(a), fp(b), ROUND), `safeMul(fp(${a}), fp(${b}))`).to.equal( fp(c) ) } @@ -513,7 +513,7 @@ describe('In FixLib,', () => { function mulTest(x: string, y: string, result: string) { it(`mul(${x}, ${y}) == ${result}`, async () => { expect(await caller.mul(fp(x), fp(y))).to.equal(fp(result)) - expect(await caller.safeMul_(fp(x), fp(y), ROUND)).to.equal(fp(result)) + expect(await caller.safeMul(fp(x), fp(y), ROUND)).to.equal(fp(result)) }) } @@ -531,8 +531,8 @@ describe('In FixLib,', () => { for (const [a, b, c] of table) { expect(await caller.mul(a, b), `mul(${a}, ${b})`).to.equal(c) expect(await caller.mul(b, a), `mul(${b}, ${a})`).to.equal(c) - expect(await caller.safeMul_(a, b, ROUND), `safeMul(${b}, ${a})`).to.equal(c) - expect(await caller.safeMul_(b, a, ROUND), `safeMul(${b}, ${a})`).to.equal(c) + expect(await caller.safeMul(a, b, ROUND), `safeMul(${b}, ${a})`).to.equal(c) + expect(await caller.safeMul(b, a, ROUND), `safeMul(${b}, ${a})`).to.equal(c) } }) @@ -547,9 +547,9 @@ describe('In FixLib,', () => { expect(await caller.mulRnd(a, b, FLOOR), `mulRnd((${a}, ${b}, FLOOR)`).to.equal(floor) expect(await caller.mulRnd(a, b, ROUND), `mulRnd((${a}, ${b}, ROUND)`).to.equal(round) expect(await caller.mulRnd(a, b, CEIL), `mulRnd((${a}, ${b}, CEIL)`).to.equal(ceil) - expect(await caller.safeMul_(a, b, FLOOR), `safeMul((${a}, ${b}, FLOOR)`).to.equal(floor) - expect(await caller.safeMul_(a, b, ROUND), `safeMul((${a}, ${b}, ROUND)`).to.equal(round) - expect(await caller.safeMul_(a, b, CEIL), `safeMul((${a}, ${b}, CEIL)`).to.equal(ceil) + expect(await caller.safeMul(a, b, FLOOR), `safeMul((${a}, ${b}, FLOOR)`).to.equal(floor) + expect(await caller.safeMul(a, b, ROUND), `safeMul((${a}, ${b}, ROUND)`).to.equal(round) + expect(await caller.safeMul(a, b, CEIL), `safeMul((${a}, ${b}, CEIL)`).to.equal(ceil) } }) it('fails outside its range', async () => { @@ -558,12 +558,10 @@ describe('In FixLib,', () => { .reverted // SafeMul should not fail + await expect(caller.safeMul(MAX_UINT192.div(2).add(1), fp(2), ROUND), 'safeMul(MAX/2 + 2, 2)') + .to.not.be.reverted await expect( - caller.safeMul_(MAX_UINT192.div(2).add(1), fp(2), ROUND), - 'safeMul(MAX/2 + 2, 2)' - ).to.not.be.reverted - await expect( - caller.safeMul_(fp(bn(2).pow(81)), fp(bn(2).pow(81)), ROUND), + caller.safeMul(fp(bn(2).pow(81)), fp(bn(2).pow(81)), ROUND), 'safeMul(2^81, 2^81)' ).to.not.be.reverted }) @@ -988,9 +986,135 @@ describe('In FixLib,', () => { describe('safeMul', () => { it('rounds up to FIX_MAX', async () => { - expect(await caller.safeMul_(MAX_UINT192.div(2).add(1), fp(2), FLOOR)).to.equal(MAX_UINT192) - expect(await caller.safeMul_(MAX_UINT192.div(2).add(1), fp(2), ROUND)).to.equal(MAX_UINT192) - expect(await caller.safeMul_(MAX_UINT192.div(2).add(1), fp(2), CEIL)).to.equal(MAX_UINT192) + expect(await caller.safeMul(MAX_UINT192.div(2).add(1), fp(2), FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeMul(MAX_UINT192.div(2).add(1), fp(2), ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeMul(MAX_UINT192.div(2).add(1), fp(2), CEIL)).to.equal(MAX_UINT192) + + expect(await caller.safeMul(MAX_UINT192.sub(1), MAX_UINT192.sub(1), CEIL)).to.equal( + MAX_UINT192 + ) + }) + }) + + describe('safeDiv', () => { + it('rounds up to FIX_MAX', async () => { + expect(await caller.safeDiv(MAX_UINT192, fp(1).sub(1), FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeDiv(MAX_UINT192, fp(1).sub(1), ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeDiv(MAX_UINT192, fp(1).sub(1), CEIL)).to.equal(MAX_UINT192) + + expect(await caller.safeDiv(MAX_UINT192, 0, FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeDiv(MAX_UINT192, 0, ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeDiv(MAX_UINT192, 0, CEIL)).to.equal(MAX_UINT192) + }) + + it('rounds down to 0', async () => { + expect(await caller.safeDiv(0, 0, FLOOR)).to.equal(0) + expect(await caller.safeDiv(0, 0, ROUND)).to.equal(0) + expect(await caller.safeDiv(0, 0, CEIL)).to.equal(0) + + expect(await caller.safeDiv(MAX_UINT192.div(fp(1)).sub(1), MAX_UINT192, FLOOR)).to.equal(0) + expect(await caller.safeDiv(MAX_UINT192.div(fp(1)).sub(1), MAX_UINT192, ROUND)).to.equal(1) + expect(await caller.safeDiv(MAX_UINT192.div(fp(2)).sub(1), MAX_UINT192, ROUND)).to.equal(0) + expect(await caller.safeDiv(MAX_UINT192.div(fp(1)).sub(1), MAX_UINT192, CEIL)).to.equal(1) + expect(await caller.safeDiv(MAX_UINT192.div(fp(2)).sub(1), MAX_UINT192, CEIL)).to.equal(1) + }) + }) + + describe('safeMulDiv', () => { + it('resolves to FIX_MAX', async () => { + expect(await caller.safeMulDiv(MAX_UINT192, fp(2), fp(1), FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(2), fp(1), ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(2), fp(1), CEIL)).to.equal(MAX_UINT192) + + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), fp(1).div(2), FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), fp(1).div(2), ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), fp(1).div(2), CEIL)).to.equal(MAX_UINT192) + + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, fp(1), FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, fp(1), ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, fp(1), CEIL)).to.equal(MAX_UINT192) + + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, fp(1).div(2), FLOOR)).to.equal( + MAX_UINT192 + ) + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, fp(1).div(2), ROUND)).to.equal( + MAX_UINT192 + ) + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, fp(1).div(2), CEIL)).to.equal( + MAX_UINT192 + ) + + expect( + await caller.safeMulDiv(MAX_UINT192.sub(1), MAX_UINT192.sub(10), fp(1), FLOOR) + ).to.equal(MAX_UINT192) + expect( + await caller.safeMulDiv(MAX_UINT192.sub(1), MAX_UINT192.sub(10), fp(1), ROUND) + ).to.equal(MAX_UINT192) + expect( + await caller.safeMulDiv(MAX_UINT192.sub(1), MAX_UINT192.sub(10), fp(1), CEIL) + ).to.equal(MAX_UINT192) + + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, MAX_UINT192, FLOOR)).to.equal( + MAX_UINT192 + ) + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, MAX_UINT192, ROUND)).to.equal( + MAX_UINT192 + ) + expect(await caller.safeMulDiv(MAX_UINT192, MAX_UINT192, MAX_UINT192, CEIL)).to.equal( + MAX_UINT192 + ) + + expect( + await caller.safeMulDiv(MAX_UINT192.sub(1), MAX_UINT192.sub(10), MAX_UINT192.sub(1), FLOOR) + ).to.equal(MAX_UINT192.sub(10)) + expect( + await caller.safeMulDiv(MAX_UINT192.sub(1), MAX_UINT192.sub(10), MAX_UINT192.sub(1), ROUND) + ).to.equal(MAX_UINT192.sub(10)) + expect( + await caller.safeMulDiv(MAX_UINT192.sub(1), MAX_UINT192.sub(10), MAX_UINT192.sub(1), CEIL) + ).to.equal(MAX_UINT192.sub(10)) + + expect(await caller.safeMulDiv(fp(1).sub(1), fp(1).add(1), fp(1).sub(1), FLOOR)).to.equal( + fp(1).add(1) + ) + expect(await caller.safeMulDiv(fp(1).sub(1), fp(1).add(1), fp(1).sub(1), ROUND)).to.equal( + fp(1).add(1) + ) + expect(await caller.safeMulDiv(fp(1).sub(1), fp(1).add(1), fp(1).sub(1), CEIL)).to.equal( + fp(1).add(1) + ) + }) + + it('rounds up to FIX_MAX', async () => { + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), fp(1).sub(1), FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), fp(1).sub(1), ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), fp(1).sub(1), CEIL)).to.equal(MAX_UINT192) + + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), 0, FLOOR)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), 0, ROUND)).to.equal(MAX_UINT192) + expect(await caller.safeMulDiv(MAX_UINT192, fp(1), 0, CEIL)).to.equal(MAX_UINT192) + }) + + it('rounds down to 0', async () => { + expect(await caller.safeMulDiv(0, fp(1), 0, FLOOR)).to.equal(0) + expect(await caller.safeMulDiv(0, fp(1), 0, ROUND)).to.equal(0) + expect(await caller.safeMulDiv(0, fp(1), 0, CEIL)).to.equal(0) + + expect( + await caller.safeMulDiv(MAX_UINT192.div(fp(1)).sub(1), fp(1), MAX_UINT192, FLOOR) + ).to.equal(0) + expect( + await caller.safeMulDiv(MAX_UINT192.div(fp(1)).sub(1), fp(1), MAX_UINT192, ROUND) + ).to.equal(1) + expect( + await caller.safeMulDiv(MAX_UINT192.div(fp(2)).sub(1), fp(1), MAX_UINT192, ROUND) + ).to.equal(0) + expect( + await caller.safeMulDiv(MAX_UINT192.div(fp(1)).sub(1), fp(1), MAX_UINT192, CEIL) + ).to.equal(1) + expect( + await caller.safeMulDiv(MAX_UINT192.div(fp(2)).sub(1), fp(1), MAX_UINT192, CEIL) + ).to.equal(1) }) }) diff --git a/test/plugins/Asset.test.ts b/test/plugins/Asset.test.ts index e1ab30870..3706a3317 100644 --- a/test/plugins/Asset.test.ts +++ b/test/plugins/Asset.test.ts @@ -18,7 +18,7 @@ import { Asset, ATokenFiatCollateral, CTokenFiatCollateral, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FiatCollateral, IAssetRegistry, @@ -37,6 +37,7 @@ import { ORACLE_TIMEOUT, ORACLE_ERROR, PRICE_TIMEOUT, + VERSION, } from '../fixtures' const DEFAULT_THRESHOLD = fp('0.01') // 1% @@ -51,7 +52,7 @@ describe('Assets contracts #fast', () => { let token: ERC20Mock let usdc: USDCMock let aToken: StaticATokenMock - let cToken: CTokenVaultMock + let cToken: CTokenWrapperMock // Assets let collateral0: FiatCollateral @@ -111,8 +112,8 @@ describe('Assets contracts #fast', () => { aToken = ( await ethers.getContractAt('StaticATokenMock', await collateral2.erc20()) ) - cToken = ( - await ethers.getContractAt('CTokenVaultMock', await collateral3.erc20()) + cToken = ( + await ethers.getContractAt('CTokenWrapperMock', await collateral3.erc20()) ) await rsr.connect(wallet).mint(wallet.address, amt) @@ -137,6 +138,7 @@ describe('Assets contracts #fast', () => { expect(await rsrAsset.isCollateral()).to.equal(false) expect(await rsrAsset.erc20()).to.equal(rsr.address) expect(await rsr.decimals()).to.equal(18) + expect(await rsrAsset.version()).to.equal(VERSION) expect(await rsrAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) expect(await rsrAsset.bal(wallet.address)).to.equal(amt) await expectPrice(rsrAsset.address, fp('1'), ORACLE_ERROR, true) @@ -146,6 +148,7 @@ describe('Assets contracts #fast', () => { expect(await compAsset.isCollateral()).to.equal(false) expect(await compAsset.erc20()).to.equal(compToken.address) expect(await compToken.decimals()).to.equal(18) + expect(await compAsset.version()).to.equal(VERSION) expect(await compAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) expect(await compAsset.bal(wallet.address)).to.equal(amt) await expectPrice(compAsset.address, fp('1'), ORACLE_ERROR, true) @@ -155,6 +158,7 @@ describe('Assets contracts #fast', () => { expect(await aaveAsset.isCollateral()).to.equal(false) expect(await aaveAsset.erc20()).to.equal(aaveToken.address) expect(await aaveToken.decimals()).to.equal(18) + expect(await aaveAsset.version()).to.equal(VERSION) expect(await aaveAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) expect(await aaveAsset.bal(wallet.address)).to.equal(amt) await expectPrice(aaveAsset.address, fp('1'), ORACLE_ERROR, true) @@ -164,6 +168,7 @@ describe('Assets contracts #fast', () => { expect(await rTokenAsset.isCollateral()).to.equal(false) expect(await rTokenAsset.erc20()).to.equal(rToken.address) expect(await rToken.decimals()).to.equal(18) + expect(await rTokenAsset.version()).to.equal(VERSION) expect(await rTokenAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) expect(await rTokenAsset.bal(wallet.address)).to.equal(amt) await expectRTokenPrice( @@ -301,6 +306,12 @@ describe('Assets contracts #fast', () => { config.minTradeVolume.mul((await assetRegistry.erc20s()).length) ) expect(await rTokenAsset.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) + + // Should have lot price, equal to price when feed works OK + const [lowPrice, highPrice] = await rTokenAsset.price() + const [lotLow, lotHigh] = await rTokenAsset.lotPrice() + expect(lotLow).to.equal(lowPrice) + expect(lotHigh).to.equal(highPrice) }) it('Should calculate trade min correctly', async () => { @@ -389,7 +400,7 @@ describe('Assets contracts #fast', () => { await invalidFiatCollateral.setSimplyRevert(false) await expect(invalidFiatCollateral.price()).to.be.reverted - // Check RToken price reverrts + // Check RToken price reverts await expect(rTokenAsset.price()).to.be.reverted }) diff --git a/test/plugins/Collateral.test.ts b/test/plugins/Collateral.test.ts index dae6d7d07..d2fdbc017 100644 --- a/test/plugins/Collateral.test.ts +++ b/test/plugins/Collateral.test.ts @@ -11,7 +11,7 @@ import { ComptrollerMock, CTokenFiatCollateral, CTokenNonFiatCollateral, - CTokenVaultMock, + CTokenWrapperMock, CTokenSelfReferentialCollateral, ERC20Mock, EURFiatCollateral, @@ -64,7 +64,7 @@ describe('Collateral contracts', () => { let token: ERC20Mock let usdc: USDCMock let aToken: StaticATokenMock - let cToken: CTokenVaultMock + let cToken: CTokenWrapperMock let aaveToken: ERC20Mock let compToken: ERC20Mock @@ -125,8 +125,8 @@ describe('Collateral contracts', () => { aToken = ( await ethers.getContractAt('StaticATokenMock', await aTokenCollateral.erc20()) ) - cToken = ( - await ethers.getContractAt('CTokenVaultMock', await cTokenCollateral.erc20()) + cToken = ( + await ethers.getContractAt('CTokenWrapperMock', await cTokenCollateral.erc20()) ) await token.connect(owner).mint(owner.address, amt) @@ -1282,7 +1282,7 @@ describe('Collateral contracts', () => { let CTokenNonFiatFactory: ContractFactory let cTokenNonFiatCollateral: CTokenNonFiatCollateral let nonFiatToken: ERC20Mock - let cNonFiatTokenVault: CTokenVaultMock + let cNonFiatTokenVault: CTokenWrapperMock let targetUnitOracle: MockV3Aggregator let referenceUnitOracle: MockV3Aggregator @@ -1299,7 +1299,7 @@ describe('Collateral contracts', () => { ) // cToken cNonFiatTokenVault = await ( - await ethers.getContractFactory('CTokenVaultMock') + await ethers.getContractFactory('CTokenWrapperMock') ).deploy( 'cWBTC Token', 'cWBTC', @@ -1716,7 +1716,7 @@ describe('Collateral contracts', () => { let CTokenSelfReferentialFactory: ContractFactory let cTokenSelfReferentialCollateral: CTokenSelfReferentialCollateral let selfRefToken: WETH9 - let cSelfRefToken: CTokenVaultMock + let cSelfRefToken: CTokenWrapperMock let chainlinkFeed: MockV3Aggregator beforeEach(async () => { @@ -1727,7 +1727,7 @@ describe('Collateral contracts', () => { // cToken Self Ref cSelfRefToken = await ( - await ethers.getContractFactory('CTokenVaultMock') + await ethers.getContractFactory('CTokenWrapperMock') ).deploy('cETH Token', 'cETH', selfRefToken.address, compToken.address, compoundMock.address) CTokenSelfReferentialFactory = await ethers.getContractFactory( diff --git a/test/plugins/RewardableERC20.test.ts b/test/plugins/RewardableERC20.test.ts new file mode 100644 index 000000000..5e16d63c8 --- /dev/null +++ b/test/plugins/RewardableERC20.test.ts @@ -0,0 +1,617 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { expect } from 'chai' +import { Wallet, ContractFactory, ContractTransaction, BigNumber } from 'ethers' +import { ethers } from 'hardhat' +import { bn, fp } from '../../common/numbers' +import { + ERC20MockDecimals, + ERC20MockRewarding, + RewardableERC20Wrapper, + RewardableERC4626Vault, +} from '../../typechain' +import { cartesianProduct } from '../utils/cases' +import { useEnv } from '#/utils/env' +import { Implementation } from '../fixtures' +import snapshotGasCost from '../utils/snapshotGasCost' + +type Fixture = () => Promise + +interface RewardableERC20Fixture { + rewardableVault: RewardableERC4626Vault | RewardableERC20Wrapper + rewardableAsset: ERC20MockRewarding + rewardToken: ERC20MockDecimals +} + +// 18 cases: test two wrappers with 2 combinations of decimals [6, 8, 18] + +enum Wrapper { + ERC20 = 'RewardableERC20WrapperTest', + ERC4626 = 'RewardableERC4626VaultTest', +} +const wrapperNames: Wrapper[] = [Wrapper.ERC20, Wrapper.ERC4626] + +for (const wrapperName of wrapperNames) { + // this style preferred due to handling gas section correctly + + const getFixture = ( + assetDecimals: number, + rewardDecimals: number + ): Fixture => { + const fixture: Fixture = + async function (): Promise { + const rewardTokenFactory: ContractFactory = await ethers.getContractFactory( + 'ERC20MockDecimals' + ) + const rewardToken = ( + await rewardTokenFactory.deploy('Reward Token', 'REWARD', rewardDecimals) + ) + + const rewardableAssetFactory: ContractFactory = await ethers.getContractFactory( + 'ERC20MockRewarding' + ) + const rewardableAsset = ( + await rewardableAssetFactory.deploy( + 'Rewarding Test Asset', + 'rewardTEST', + assetDecimals, + rewardToken.address + ) + ) + + const rewardableVaultFactory: ContractFactory = await ethers.getContractFactory(wrapperName) + const rewardableVault = ( + await rewardableVaultFactory.deploy( + rewardableAsset.address, + 'Rewarding Test Asset Vault', + 'vrewardTEST', + rewardToken.address + ) + ) + + return { + rewardableVault, + rewardableAsset, + rewardToken, + } + } + return fixture + } + + const toShares = (assets: BigNumber, assetDecimals: number, shareDecimals: number): BigNumber => { + return assets.mul(bn(10).pow(shareDecimals - assetDecimals)) + } + + // helper to handle different withdraw() signatures for each wrapper type + const withdraw = ( + wrapper: RewardableERC4626Vault | RewardableERC20Wrapper, + amount: BigNumber, + to: string + ): Promise => { + if (wrapperName == Wrapper.ERC20) { + const wrapperERC20 = wrapper as RewardableERC20Wrapper + return wrapperERC20.withdraw(amount, to) + } else { + const wrapperERC4626 = wrapper as RewardableERC4626Vault + return wrapperERC4626.withdraw(amount, to, to) + } + } + + const runTests = (assetDecimals: number, rewardDecimals: number) => { + describe(wrapperName, () => { + // Decimals + let shareDecimals: number + + // Assets + let rewardableVault: RewardableERC20Wrapper | RewardableERC4626Vault + let rewardableAsset: ERC20MockRewarding + let rewardToken: ERC20MockDecimals + + // Main + let alice: Wallet + let bob: Wallet + + const initBalance = fp('10000').div(bn(10).pow(18 - assetDecimals)) + const rewardAmount = fp('200').div(bn(10).pow(18 - rewardDecimals)) + let oneShare: BigNumber + let initShares: BigNumber + + const fixture = getFixture(assetDecimals, rewardDecimals) + + before('load wallets', async () => { + ;[alice, bob] = (await ethers.getSigners()) as unknown as Wallet[] + }) + + beforeEach(async () => { + // Deploy fixture + ;({ rewardableVault, rewardableAsset, rewardToken } = await loadFixture(fixture)) + + await rewardableAsset.mint(alice.address, initBalance) + await rewardableAsset.connect(alice).approve(rewardableVault.address, initBalance) + await rewardableAsset.mint(bob.address, initBalance) + await rewardableAsset.connect(bob).approve(rewardableVault.address, initBalance) + + shareDecimals = await rewardableVault.decimals() + initShares = toShares(initBalance, assetDecimals, shareDecimals) + oneShare = bn('1').mul(bn(10).pow(shareDecimals)) + }) + + describe('Deployment', () => { + it('sets the rewardableVault rewardableAsset', async () => { + const seenAsset = await (wrapperName == Wrapper.ERC4626 + ? (rewardableVault as RewardableERC4626Vault).asset() + : (rewardableVault as RewardableERC20Wrapper).underlying()) + + expect(seenAsset).equal(rewardableAsset.address) + }) + + it('sets the rewardableVault reward token', async () => { + const seenRewardToken = await rewardableVault.rewardToken() + expect(seenRewardToken).equal(rewardToken.address) + }) + + it('no rewards yet', async () => { + await rewardableVault.connect(alice).claimRewards() + expect(await rewardableVault.rewardsPerShare()).to.equal(bn(0)) + expect(await rewardableVault.lastRewardsPerShare(alice.address)).to.equal(bn(0)) + }) + }) + + describe('alice deposit, accrue, alice deposit, bob deposit', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(alice).deposit(initBalance.div(8), alice.address) + + // bob deposit + await rewardableVault.connect(bob).deposit(initBalance.div(8), bob.address) + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct balance', async () => { + expect(initShares.mul(3).div(8)).equal(await rewardableVault.balanceOf(alice.address)) + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('bob shows correct balance', async () => { + expect(initShares.div(8)).equal(await rewardableVault.balanceOf(bob.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // rewards / alice's deposit + expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + }) + }) + + describe('alice deposit, accrue, alice deposit, accrue, bob deposit', () => { + let rewardsPerShare: BigNumber + let initRewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + + initRewardsPerShare = await rewardableVault.rewardsPerShare() + + // accrue + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + // bob deposit + await rewardableVault.connect(alice).deposit(initBalance.div(8), bob.address) + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct lastRewardsPerShare', async () => { + // rewards / alice's deposit + expect(initRewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + expect(initRewardsPerShare).equal( + await rewardableVault.lastRewardsPerShare(alice.address) + ) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + const expectedRewardsPerShare = rewardAmount + .mul(oneShare) + .div(initShares.div(4)) + .add(rewardAmount.mul(oneShare).div(initShares.div(2))) + expect(rewardsPerShare).equal(expectedRewardsPerShare) + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + }) + + describe('alice deposit, accrue, alice withdraw', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await withdraw(rewardableVault.connect(alice), initBalance.div(8), alice.address) + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('rewardsPerShare is correct', async () => { + // rewards / alice's deposit + expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + }) + }) + + describe('alice deposit and withdraw with 0 amount', () => { + beforeEach(async () => { + // alice deposit, accrue, and claim - 0 amount + await rewardableVault.connect(alice).deposit(bn(0), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await withdraw(rewardableVault.connect(alice), bn(0), alice.address) + }) + + it('no rewards', async () => { + expect(await rewardableVault.lastRewardsPerShare(alice.address)).to.equal(bn(0)) + expect(await rewardableVault.rewardsPerShare()).to.equal(bn(0)) + }) + }) + + describe('alice deposit, accrue, bob deposit, alice withdraw', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) + await withdraw(rewardableVault.connect(alice), initBalance.div(8), alice.address) + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // rewards / alice's deposit + expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + }) + }) + + describe('alice deposit, accrue, bob deposit, alice fully withdraw', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) + await withdraw(rewardableVault.connect(alice), initBalance.div(4), alice.address) + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct balance', async () => { + expect(0).equal(await rewardableVault.balanceOf(alice.address)) + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('bob shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // rewards / alice's deposit + expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + }) + }) + + describe('alice deposit, accrue, alice claim, bob deposit', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(alice).claimRewards() + + // bob deposit + await rewardableVault.connect(bob).deposit(initBalance.div(8), bob.address) + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + }) + + it('alice has claimed rewards', async () => { + expect(rewardAmount).equal(await rewardToken.balanceOf(alice.address)) + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('bob shows correct balance', async () => { + expect(initShares.div(8)).equal(await rewardableVault.balanceOf(bob.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // rewards / alice's deposit + expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) + }) + }) + + describe('alice deposit, accrue, bob deposit, accrue, bob claim, alice claim', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + // bob deposit + await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) + + // accrue + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + // claims + await rewardableVault.connect(bob).claimRewards() + await rewardableVault.connect(alice).claimRewards() + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + }) + + it('alice has claimed rewards', async () => { + expect(rewardAmount.add(rewardAmount.div(2))).equal( + await rewardToken.balanceOf(alice.address) + ) + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('bob shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('bob has claimed rewards', async () => { + expect(rewardAmount.div(2)).equal(await rewardToken.balanceOf(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // (rewards / alice's deposit) + (rewards / (alice's deposit + bob's deposit)) + const expectedRewardsPerShare = rewardAmount + .mul(oneShare) + .div(initShares.div(4)) + .add(rewardAmount.mul(oneShare).div(initShares.div(2))) + expect(rewardsPerShare).equal(expectedRewardsPerShare) + }) + }) + + describe('does not accure rewards for an account while it has no deposits', () => { + // alice deposit, accrue, bob deposit, alice fully withdraw, accrue, alice deposit, alice claim, bob claim + let rewardsPerShare: BigNumber + + beforeEach(async () => { + // alice deposit, accrue, and claim + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + // accrue + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + // bob deposit + await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) + // alice withdraw all + await withdraw(rewardableVault.connect(alice), initBalance.div(4), alice.address) + // accrue + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + // alice re-deposit + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + // both claim + await rewardableVault.connect(bob).claimRewards() + await rewardableVault.connect(alice).claimRewards() + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('alice has claimed rewards', async () => { + expect(rewardAmount).equal(await rewardToken.balanceOf(alice.address)) + }) + + it('bob shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('bob has claimed rewards', async () => { + expect(rewardAmount).equal(await rewardToken.balanceOf(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // (rewards / alice's deposit) + (rewards / bob's deposit) + expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4)).mul(2)) + }) + }) + + describe('correctly updates rewards on transfer', () => { + let rewardsPerShare: BigNumber + + beforeEach(async () => { + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) + await rewardableVault.connect(alice).transfer(bob.address, initShares.div(4)) + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + await rewardableVault.connect(bob).claimRewards() + await rewardableVault.connect(alice).claimRewards() + + rewardsPerShare = await rewardableVault.rewardsPerShare() + }) + + it('alice shows correct balance', async () => { + expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) + }) + + it('alice shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) + }) + + it('alice has claimed rewards', async () => { + expect(rewardAmount).equal(await rewardToken.balanceOf(alice.address)) + }) + + it('bob shows correct balance', async () => { + expect(initShares.div(2)).equal(await rewardableVault.balanceOf(bob.address)) + }) + + it('bob shows correct lastRewardsPerShare', async () => { + expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) + }) + + it('bob has claimed rewards', async () => { + expect(rewardAmount).equal(await rewardToken.balanceOf(bob.address)) + }) + + it('rewardsPerShare is correct', async () => { + // (rewards / alice's deposit) + (rewards / (alice's deposit + bob's deposit)) + expect(rewardsPerShare).equal( + rewardAmount + .mul(oneShare) + .div(initShares.div(4)) + .add(rewardAmount.mul(oneShare).div(initShares.div(2))) + ) + }) + }) + }) + } + + const decimalSeeds = [6, 8, 18] + const cases = cartesianProduct(decimalSeeds, decimalSeeds) + cases.forEach((params) => { + const wrapperStr = wrapperName.replace('Test', '') + describe(`${wrapperStr} - asset decimals: ${params[0]} / reward decimals: ${params[1]}`, () => { + runTests(params[0], params[1]) + }) + }) + + const IMPLEMENTATION: Implementation = + useEnv('PROTO_IMPL') == Implementation.P1.toString() ? Implementation.P1 : Implementation.P0 + + const describeGas = + IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip + + // This only needs to run once per Wrapper. Should not run for multiple decimal combinations + describeGas('Gas Reporting', () => { + // Assets + let rewardableVault: RewardableERC4626Vault | RewardableERC20Wrapper + let rewardableAsset: ERC20MockRewarding + + // Main + let alice: Wallet + + const initBalance = fp('10000') + const rewardAmount = fp('200') + + const fixture = getFixture(18, 18) + + before('load wallets', async () => { + ;[alice] = (await ethers.getSigners()) as unknown as Wallet[] + }) + + beforeEach(async () => { + // Deploy fixture + ;({ rewardableVault, rewardableAsset } = await loadFixture(fixture)) + + await rewardableAsset.mint(alice.address, initBalance) + await rewardableAsset.connect(alice).approve(rewardableVault.address, initBalance) + }) + + describe(wrapperName, () => { + it('deposit', async function () { + // Deposit + await snapshotGasCost( + rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + ) + + // Deposit again + await snapshotGasCost( + rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) + ) + }) + + it('withdraw', async function () { + await rewardableVault.connect(alice).deposit(initBalance, alice.address) + + await snapshotGasCost( + withdraw(rewardableVault.connect(alice), initBalance.div(2), alice.address) + ) + + await snapshotGasCost( + withdraw(rewardableVault.connect(alice), initBalance.div(2), alice.address) + ) + }) + + it('claimRewards', async function () { + await rewardableVault.connect(alice).deposit(initBalance, alice.address) + + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + await snapshotGasCost(rewardableVault.connect(alice).claimRewards()) + + await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) + + await snapshotGasCost(rewardableVault.connect(alice).claimRewards()) + }) + }) + }) +} diff --git a/test/plugins/RewardableERC20Vault.test.ts b/test/plugins/RewardableERC20Vault.test.ts deleted file mode 100644 index 47f24678b..000000000 --- a/test/plugins/RewardableERC20Vault.test.ts +++ /dev/null @@ -1,574 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import { expect } from 'chai' -import { Wallet, ContractFactory, BigNumber } from 'ethers' -import { ethers } from 'hardhat' -import { bn, fp } from '../../common/numbers' -import { - ERC20MockDecimals, - ERC20MockRewarding, - RewardableERC20Vault, - RewardableERC20VaultTest, -} from '../../typechain' -import { cartesianProduct } from '../utils/cases' -import { useEnv } from '#/utils/env' -import { Implementation } from '../fixtures' -import snapshotGasCost from '../utils/snapshotGasCost' - -type Fixture = () => Promise - -interface RewardableERC20VaultFixture { - rewardableVault: RewardableERC20Vault - rewardableAsset: ERC20MockRewarding - rewardToken: ERC20MockDecimals -} - -const getFixture = ( - assetDecimals: number, - rewardDecimals: number -): Fixture => { - const fixture: Fixture = - async function (): Promise { - const rewardTokenFactory: ContractFactory = await ethers.getContractFactory( - 'ERC20MockDecimals' - ) - const rewardToken = ( - await rewardTokenFactory.deploy('Reward Token', 'REWARD', rewardDecimals) - ) - - const rewardableAssetFactory: ContractFactory = await ethers.getContractFactory( - 'ERC20MockRewarding' - ) - const rewardableAsset = ( - await rewardableAssetFactory.deploy( - 'Rewarding Test Asset', - 'rewardTEST', - assetDecimals, - rewardToken.address - ) - ) - - const rewardableVaultFactory: ContractFactory = await ethers.getContractFactory( - 'RewardableERC20VaultTest' - ) - const rewardableVault = ( - await rewardableVaultFactory.deploy( - rewardableAsset.address, - 'Rewarding Test Asset Vault', - 'vrewardTEST', - rewardToken.address - ) - ) - - return { - rewardableVault, - rewardableAsset, - rewardToken, - } - } - return fixture -} - -const toShares = (assets: BigNumber, assetDecimals: number, shareDecimals: number): BigNumber => { - return assets.mul(bn(10).pow(shareDecimals - assetDecimals)) -} - -const runTests = (assetDecimals: number, rewardDecimals: number) => { - describe('RewardableERC20Vault', () => { - // Decimals - let shareDecimals: number - - // Assets - let rewardableVault: RewardableERC20Vault - let rewardableAsset: ERC20MockRewarding - let rewardToken: ERC20MockDecimals - - // Main - let alice: Wallet - let bob: Wallet - - const initBalance = fp('10000').div(bn(10).pow(18 - assetDecimals)) - const rewardAmount = fp('200').div(bn(10).pow(18 - rewardDecimals)) - let oneShare: BigNumber - let initShares: BigNumber - - const fixture = getFixture(assetDecimals, rewardDecimals) - - before('load wallets', async () => { - ;[alice, bob] = (await ethers.getSigners()) as unknown as Wallet[] - }) - - beforeEach(async () => { - // Deploy fixture - ;({ rewardableVault, rewardableAsset, rewardToken } = await loadFixture(fixture)) - - await rewardableAsset.mint(alice.address, initBalance) - await rewardableAsset.connect(alice).approve(rewardableVault.address, initBalance) - await rewardableAsset.mint(bob.address, initBalance) - await rewardableAsset.connect(bob).approve(rewardableVault.address, initBalance) - - shareDecimals = await rewardableVault.decimals() - initShares = toShares(initBalance, assetDecimals, shareDecimals) - oneShare = bn('1').mul(bn(10).pow(shareDecimals)) - }) - - describe('Deployment', () => { - it('sets the rewardableVault rewardableAsset', async () => { - const seenAsset = await rewardableVault.asset() - expect(seenAsset).equal(rewardableAsset.address) - }) - - it('sets the rewardableVault reward token', async () => { - const seenRewardToken = await rewardableVault.rewardToken() - expect(seenRewardToken).equal(rewardToken.address) - }) - }) - - describe('alice deposit, accrue, alice deposit, bob deposit', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(alice).deposit(initBalance.div(8), alice.address) - - // bob deposit - await rewardableVault.connect(bob).deposit(initBalance.div(8), bob.address) - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct balance', async () => { - expect(initShares.mul(3).div(8)).equal(await rewardableVault.balanceOf(alice.address)) - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('bob shows correct balance', async () => { - expect(initShares.div(8)).equal(await rewardableVault.balanceOf(bob.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) - }) - }) - - describe('alice deposit, accrue, alice deposit, accrue, bob deposit', () => { - let rewardsPerShare: BigNumber - let initRewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - - initRewardsPerShare = await rewardableVault.rewardsPerShare() - - // accrue - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - - // bob deposit - await rewardableVault.connect(alice).deposit(initBalance.div(8), bob.address) - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct lastRewardsPerShare', async () => { - // rewards / alice's deposit - expect(initRewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) - expect(initRewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - const expectedRewardsPerShare = rewardAmount - .mul(oneShare) - .div(initShares.div(4)) - .add(rewardAmount.mul(oneShare).div(initShares.div(2))) - expect(rewardsPerShare).equal(expectedRewardsPerShare) - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - }) - - describe('alice deposit, accrue, alice withdraw', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault - .connect(alice) - .withdraw(initBalance.div(8), alice.address, alice.address) - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('rewardsPerShare is correct', async () => { - // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) - }) - }) - - describe('alice deposit, accrue, bob deposit, alice withdraw', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) - await rewardableVault - .connect(alice) - .withdraw(initBalance.div(8), alice.address, alice.address) - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) - }) - }) - - describe('alice deposit, accrue, bob deposit, alice fully withdraw', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) - await rewardableVault - .connect(alice) - .withdraw(initBalance.div(4), alice.address, alice.address) - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct balance', async () => { - expect(0).equal(await rewardableVault.balanceOf(alice.address)) - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('bob shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) - }) - }) - - describe('alice deposit, accrue, alice claim, bob deposit', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(alice).claimRewards() - - // bob deposit - await rewardableVault.connect(bob).deposit(initBalance.div(8), bob.address) - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) - }) - - it('alice has claimed rewards', async () => { - expect(rewardAmount).equal(await rewardToken.balanceOf(alice.address)) - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('bob shows correct balance', async () => { - expect(initShares.div(8)).equal(await rewardableVault.balanceOf(bob.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // rewards / alice's deposit - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4))) - }) - }) - - describe('alice deposit, accrue, bob deposit, accrue, bob claim, alice claim', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - - // bob deposit - await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) - - // accrue - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - - // claims - await rewardableVault.connect(bob).claimRewards() - await rewardableVault.connect(alice).claimRewards() - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) - }) - - it('alice has claimed rewards', async () => { - expect(rewardAmount.add(rewardAmount.div(2))).equal( - await rewardToken.balanceOf(alice.address) - ) - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('bob shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('bob has claimed rewards', async () => { - expect(rewardAmount.div(2)).equal(await rewardToken.balanceOf(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // (rewards / alice's deposit) + (rewards / (alice's deposit + bob's deposit)) - const expectedRewardsPerShare = rewardAmount - .mul(oneShare) - .div(initShares.div(4)) - .add(rewardAmount.mul(oneShare).div(initShares.div(2))) - expect(rewardsPerShare).equal(expectedRewardsPerShare) - }) - }) - - describe('does not accure rewards for an account while it has no deposits', () => { - // alice deposit, accrue, bob deposit, alice fully withdraw, accrue, alice deposit, alice claim, bob claim - let rewardsPerShare: BigNumber - - beforeEach(async () => { - // alice deposit, accrue, and claim - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - // accrue - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - // bob deposit - await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) - // alice withdraw all - await rewardableVault - .connect(alice) - .withdraw(initBalance.div(4), alice.address, alice.address) - // accrue - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - // alice re-deposit - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - // both claim - await rewardableVault.connect(bob).claimRewards() - await rewardableVault.connect(alice).claimRewards() - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('alice has claimed rewards', async () => { - expect(rewardAmount).equal(await rewardToken.balanceOf(alice.address)) - }) - - it('bob shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(bob.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('bob has claimed rewards', async () => { - expect(rewardAmount).equal(await rewardToken.balanceOf(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // (rewards / alice's deposit) + (rewards / bob's deposit) - expect(rewardsPerShare).equal(rewardAmount.mul(oneShare).div(initShares.div(4)).mul(2)) - }) - }) - - describe('correctly updates rewards on transfer', () => { - let rewardsPerShare: BigNumber - - beforeEach(async () => { - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(bob).deposit(initBalance.div(4), bob.address) - await rewardableVault.connect(alice).transfer(bob.address, initShares.div(4)) - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - await rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - await rewardableVault.connect(bob).claimRewards() - await rewardableVault.connect(alice).claimRewards() - - rewardsPerShare = await rewardableVault.rewardsPerShare() - }) - - it('alice shows correct balance', async () => { - expect(initShares.div(4)).equal(await rewardableVault.balanceOf(alice.address)) - }) - - it('alice shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(alice.address)) - }) - - it('alice has claimed rewards', async () => { - expect(rewardAmount).equal(await rewardToken.balanceOf(alice.address)) - }) - - it('bob shows correct balance', async () => { - expect(initShares.div(2)).equal(await rewardableVault.balanceOf(bob.address)) - }) - - it('bob shows correct lastRewardsPerShare', async () => { - expect(rewardsPerShare).equal(await rewardableVault.lastRewardsPerShare(bob.address)) - }) - - it('bob has claimed rewards', async () => { - expect(rewardAmount).equal(await rewardToken.balanceOf(bob.address)) - }) - - it('rewardsPerShare is correct', async () => { - // (rewards / alice's deposit) + (rewards / (alice's deposit + bob's deposit)) - expect(rewardsPerShare).equal( - rewardAmount - .mul(oneShare) - .div(initShares.div(4)) - .add(rewardAmount.mul(oneShare).div(initShares.div(2))) - ) - }) - }) - }) -} - -const decimalSeeds = [6, 8, 18] -const cases = cartesianProduct(decimalSeeds, decimalSeeds) -// const cases = [[6, 6]] -cases.forEach((params) => { - describe(`rewardableAsset assetDecimals: ${params[0]} / reward assetDecimals: ${params[1]}`, () => { - runTests(params[0], params[1]) - }) -}) - -export const IMPLEMENTATION: Implementation = - useEnv('PROTO_IMPL') == Implementation.P1.toString() ? Implementation.P1 : Implementation.P0 - -const describeGas = - IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip - -describeGas('Gas Reporting', () => { - // Assets - let rewardableVault: RewardableERC20Vault - let rewardableAsset: ERC20MockRewarding - - // Main - let alice: Wallet - - const initBalance = fp('10000') - const rewardAmount = fp('200') - - const fixture = getFixture(18, 18) - - before('load wallets', async () => { - ;[alice] = (await ethers.getSigners()) as unknown as Wallet[] - }) - - beforeEach(async () => { - // Deploy fixture - ;({ rewardableVault, rewardableAsset } = await loadFixture(fixture)) - - await rewardableAsset.mint(alice.address, initBalance) - await rewardableAsset.connect(alice).approve(rewardableVault.address, initBalance) - }) - - describe('RewardableERC20Vault', () => { - it('deposit', async function () { - // Deposit - await snapshotGasCost( - rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - ) - - // Deposit again - await snapshotGasCost( - rewardableVault.connect(alice).deposit(initBalance.div(4), alice.address) - ) - }) - - it('withdraw', async function () { - await rewardableVault.connect(alice).deposit(initBalance, alice.address) - - await snapshotGasCost( - rewardableVault.connect(alice).withdraw(initBalance.div(2), alice.address, alice.address) - ) - - await snapshotGasCost( - rewardableVault.connect(alice).withdraw(initBalance.div(2), alice.address, alice.address) - ) - }) - - it('claimRewards', async function () { - await rewardableVault.connect(alice).deposit(initBalance, alice.address) - - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - - await snapshotGasCost(rewardableVault.connect(alice).claimRewards()) - - await rewardableAsset.accrueRewards(rewardAmount, rewardableVault.address) - - await snapshotGasCost(rewardableVault.connect(alice).claimRewards()) - }) - }) -}) diff --git a/test/plugins/__snapshots__/RewardableERC20Vault.test.ts.snap b/test/plugins/__snapshots__/RewardableERC20Vault.test.ts.snap index dd642750d..5d104c5e6 100644 --- a/test/plugins/__snapshots__/RewardableERC20Vault.test.ts.snap +++ b/test/plugins/__snapshots__/RewardableERC20Vault.test.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Gas Reporting RewardableERC20Vault claimRewards 1`] = `159832`; +exports[`Gas Reporting RewardableERC4626Vault claimRewards 1`] = `159832`; -exports[`Gas Reporting RewardableERC20Vault claimRewards 2`] = `83066`; +exports[`Gas Reporting RewardableERC4626Vault claimRewards 2`] = `83066`; -exports[`Gas Reporting RewardableERC20Vault deposit 1`] = `119033`; +exports[`Gas Reporting RewardableERC4626Vault deposit 1`] = `119033`; -exports[`Gas Reporting RewardableERC20Vault deposit 2`] = `85387`; +exports[`Gas Reporting RewardableERC4626Vault deposit 2`] = `85387`; -exports[`Gas Reporting RewardableERC20Vault withdraw 1`] = `98068`; +exports[`Gas Reporting RewardableERC4626Vault withdraw 1`] = `98068`; -exports[`Gas Reporting RewardableERC20Vault withdraw 2`] = `66568`; +exports[`Gas Reporting RewardableERC4626Vault withdraw 2`] = `66568`; diff --git a/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts b/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts index 5b3eeea84..38853a79c 100644 --- a/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/aave/ATokenFiatCollateral.test.ts @@ -167,7 +167,12 @@ describeFork(`ATokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi await ethers.getContractAt('ERC20Mock', networkConfig[chainId].tokens.DAI || '') ) // aDAI token - aDai = await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aDAI || '') + aDai = ( + await ethers.getContractAt( + '@aave/protocol-v2/contracts/interfaces/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aDAI || '' + ) + ) // stkAAVE stkAave = ( diff --git a/test/plugins/individual-collateral/aave/StaticATokenLM.test.ts b/test/plugins/individual-collateral/aave/StaticATokenLM.test.ts index eee440900..bb78081cd 100644 --- a/test/plugins/individual-collateral/aave/StaticATokenLM.test.ts +++ b/test/plugins/individual-collateral/aave/StaticATokenLM.test.ts @@ -20,6 +20,7 @@ import { getChainId } from '../../../../common/blockchain-utils' import { networkConfig } from '../../../../common/configuration' import { MAX_UINT256, ZERO_ADDRESS } from '../../../../common/constants' import { + ATokenNoController, ERC20Mock, IAaveIncentivesController, IAToken, @@ -207,7 +208,7 @@ describeFork('StaticATokenLM: aToken wrapper with static balances and liquidity ) incentives = ( await ethers.getContractAt( - 'IAaveIncentivesController', + 'contracts/plugins/assets/aave/vendor/IAaveIncentivesController.sol:IAaveIncentivesController', networkConfig[chainId].AAVE_INCENTIVES || '', userSigner ) @@ -217,7 +218,11 @@ describeFork('StaticATokenLM: aToken wrapper with static balances and liquidity await ethers.getContractAt('IWETH', networkConfig[chainId].tokens.WETH || '', userSigner) ) aweth = ( - await ethers.getContractAt('IAToken', networkConfig[chainId].tokens.aWETH || '', userSigner) + await ethers.getContractAt( + 'contracts/plugins/assets/aave/vendor/IAToken.sol:IAToken', + networkConfig[chainId].tokens.aWETH || '', + userSigner + ) ) stkAave = ( await ethers.getContractAt( @@ -242,6 +247,8 @@ describeFork('StaticATokenLM: aToken wrapper with static balances and liquidity networkConfig[chainId].AAVE_INCENTIVES ) + expect(await staticAToken.UNDERLYING_ASSET_ADDRESS()).to.be.eq(weth.address) + ctxtParams = { staticAToken: staticAToken, underlying: (weth), @@ -848,6 +855,50 @@ describeFork('StaticATokenLM: aToken wrapper with static balances and liquidity expect(ctxtAfterWithdrawal.userStkAaveBalance).to.be.eq(0) }) + it('Withdraw using withdrawDynamicAmount() - exceeding balance', async () => { + const amountToDeposit = utils.parseEther('5') + // Exceed available balance + const amountToWithdraw = utils.parseEther('10') + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })) + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)) + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ) + + const ctxtBeforeWithdrawal = await getContext(ctxtParams) + + // Withdraw dynamic amount + await waitForTx( + await staticAToken.withdrawDynamicAmount( + userSigner._address, + amountToWithdraw, + false, + defaultTxParams + ) + ) + + const ctxtAfterWithdrawal = await getContext(ctxtParams) + + expect(ctxtBeforeWithdrawal.userATokenBalance).to.be.eq(0) + expect(ctxtBeforeWithdrawal.staticATokenATokenBalance).to.be.closeTo(amountToDeposit, 2) + // Withdraws all balance + expect(ctxtAfterWithdrawal.userATokenBalance).to.be.closeTo( + BigNumber.from( + rayMul( + new bnjs(ctxtBeforeWithdrawal.userStaticATokenBalance.toString()), + new bnjs(ctxtAfterWithdrawal.currentRate.toString()) + ).toString() + ), + 2 + ) + expect(ctxtAfterWithdrawal.userDynamicStaticATokenBalance).to.equal(0) + expect(ctxtAfterWithdrawal.userStkAaveBalance).to.equal(0) + }) + it('Withdraw using metaWithdraw()', async () => { const amountToDeposit = utils.parseEther('5') const chainId = hre.network.config.chainId ? hre.network.config.chainId : 1 @@ -2283,4 +2334,128 @@ describeFork('StaticATokenLM: aToken wrapper with static balances and liquidity expect(await staticAToken.getClaimableRewards(user.address)).to.be.eq(0) expect(await stkAave.balanceOf(user.address)).to.be.gt(0) }) + + it('Handles AToken with no incentives controller', async () => { + const StaticATokenFactory: ContractFactory = await ethers.getContractFactory('StaticATokenLM') + + const aWETHNoController: ATokenNoController = ( + await ( + await ethers.getContractFactory('ATokenNoController') + ).deploy( + networkConfig[chainId].AAVE_LENDING_POOL, + weth.address, + networkConfig[chainId].AAVE_RESERVE_TREASURY, + 'aWETH-NC', + 'aWETH-NC', + ZERO_ADDRESS + ) + ) + + const staticATokenNoController: StaticATokenLM = ( + await StaticATokenFactory.connect(userSigner).deploy( + networkConfig[chainId].AAVE_LENDING_POOL, + aWETHNoController.address, + 'Static Aave Interest Bearing WETH - No controller', + 'stataWETH-NC' + ) + ) + + expect(await staticATokenNoController.getIncentivesController()).to.be.eq(ZERO_ADDRESS) + + expect(await staticATokenNoController.UNDERLYING_ASSET_ADDRESS()).to.be.eq(weth.address) + + // Deposit + const amountToDeposit = utils.parseEther('5') + const amountToWithdraw = MAX_UINT256 + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })) + await waitForTx( + await weth.approve(staticATokenNoController.address, amountToDeposit.mul(2), defaultTxParams) + ) + + // Depositing + await waitForTx( + await staticATokenNoController.deposit( + userSigner._address, + amountToDeposit, + 0, + true, + defaultTxParams + ) + ) + + const pendingRewards1 = await staticATokenNoController.getClaimableRewards(userSigner._address) + + expect(pendingRewards1).to.equal(0) + + // Depositing + await waitForTx( + await staticATokenNoController.deposit( + userSigner._address, + amountToDeposit, + 0, + true, + defaultTxParams + ) + ) + + const pendingRewards2 = await staticATokenNoController.getClaimableRewards(userSigner._address) + + await waitForTx(await staticATokenNoController.collectAndUpdateRewards()) + await waitForTx(await staticATokenNoController.connect(userSigner)['claimRewards()']()) + + const pendingRewards3 = await staticATokenNoController.getClaimableRewards(userSigner._address) + + expect(pendingRewards2).to.equal(0) + expect(pendingRewards3).to.equal(0) + + // Withdrawing all. + await waitForTx( + await staticATokenNoController.withdraw( + userSigner._address, + amountToWithdraw, + true, + defaultTxParams + ) + ) + + const pendingRewards4 = await staticATokenNoController.getClaimableRewards(userSigner._address) + const totPendingRewards4 = await staticATokenNoController.getTotalClaimableRewards() + const claimedRewards4 = await stkAave.balanceOf(userSigner._address) + const stkAaveStatic4 = await stkAave.balanceOf(staticATokenNoController.address) + + await waitForTx(await staticATokenNoController.connect(userSigner).claimRewardsToSelf(false)) + await waitForTx( + await staticATokenNoController + .connect(userSigner) + ['claimRewards(address,bool)'](userSigner._address, true) + ) + await waitForTx( + await staticATokenNoController + .connect(userSigner) + .claimRewardsOnBehalf(userSigner._address, userSigner._address, true) + ) + + const pendingRewards5 = await staticATokenNoController.getClaimableRewards(userSigner._address) + const totPendingRewards5 = await staticATokenNoController.getTotalClaimableRewards() + const claimedRewards5 = await stkAave.balanceOf(userSigner._address) + const stkAaveStatic5 = await stkAave.balanceOf(staticATokenNoController.address) + + await waitForTx(await staticATokenNoController.collectAndUpdateRewards()) + const pendingRewards6 = await staticATokenNoController.getClaimableRewards(userSigner._address) + + // Checks + expect(pendingRewards2).to.equal(0) + expect(pendingRewards3).to.equal(0) + expect(pendingRewards4).to.equal(0) + expect(totPendingRewards4).to.eq(0) + expect(pendingRewards5).to.be.eq(0) + expect(pendingRewards6).to.be.eq(0) + expect(claimedRewards4).to.be.eq(0) + expect(claimedRewards5).to.be.eq(0) + expect(totPendingRewards5).to.be.eq(0) + expect(stkAaveStatic4).to.equal(0) + expect(stkAaveStatic5).to.equal(0) + }) }) diff --git a/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts new file mode 100644 index 000000000..60473286f --- /dev/null +++ b/test/plugins/individual-collateral/cbeth/CBETHCollateral.test.ts @@ -0,0 +1,233 @@ +import collateralTests from '../collateralTests' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { + CBETH_ETH_PRICE_FEED, + CB_ETH, + CB_ETH_ORACLE, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + ETH_USD_PRICE_FEED, + MAX_TRADE_VOL, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, +} from './constants' +import { BigNumber, BigNumberish, ContractFactory } from 'ethers' +import { bn, fp } from '#/common/numbers' +import { TestICollateral } from '@typechain/TestICollateral' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { MockV3Aggregator } from '@typechain/MockV3Aggregator' +import { CBEth, ERC20Mock, MockV3Aggregator__factory } from '@typechain/index' +import { mintCBETH, resetFork } from './helpers' +import { whileImpersonating } from '#/utils/impersonation' +import hre from 'hardhat' + +interface CbEthCollateralFixtureContext extends CollateralFixtureContext { + cbETH: CBEth + refPerTokChainlinkFeed: MockV3Aggregator +} + +interface CbEthCollateralOpts extends CollateralOpts { + refPerTokChainlinkFeed?: string + refPerTokChainlinkTimeout?: BigNumberish +} + +export const deployCollateral = async ( + opts: CbEthCollateralOpts = {} +): Promise => { + opts = { ...defaultCBEthCollateralOpts, ...opts } + + const CBETHCollateralFactory: ContractFactory = await ethers.getContractFactory('CBEthCollateral') + + const collateral = await CBETHCollateralFactory.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 ?? CBETH_ETH_PRICE_FEED, + opts.refPerTokChainlinkTimeout ?? ORACLE_TIMEOUT, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + + await expect(collateral.refresh()) + + return collateral +} + +const chainlinkDefaultAnswer = bn('1600e8') +const refPerTokChainlinkDefaultAnswer = fp('1') + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CbEthCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCBEthCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + const refPerTokChainlinkFeed = ( + await MockV3AggregatorFactory.deploy(18, refPerTokChainlinkDefaultAnswer) + ) + + collateralOpts.refPerTokChainlinkFeed = refPerTokChainlinkFeed.address + collateralOpts.refPerTokChainlinkTimeout = PRICE_TIMEOUT + + const cbETH = (await ethers.getContractAt('CBEth', CB_ETH)) as unknown as CBEth + const collateral = await deployCollateral(collateralOpts) + + return { + alice, + collateral, + chainlinkFeed, + refPerTokChainlinkFeed, + cbETH, + tok: cbETH as unknown as ERC20Mock, + } + } + + return makeCollateralFixtureContext +} +/* + Define helper functions +*/ + +const mintCollateralTo: MintCollateralFunc = async ( + ctx: CbEthCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintCBETH(amount, recipient) +} + +// 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: CbEthCollateralFixtureContext, percentChange: BigNumber) => { + await whileImpersonating(hre, CB_ETH_ORACLE, async (oracleSigner) => { + const rate = await ctx.cbETH.exchangeRate() + await ctx.cbETH + .connect(oracleSigner) + .updateExchangeRate(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: CbEthCollateralFixtureContext, + pctDecrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctDecrease).mul(-1) + ) +} +// prettier-ignore +const increaseRefPerTok = async ( + ctx: CbEthCollateralFixtureContext, + pctIncrease: BigNumberish +) => { + await changeRefPerTok( + ctx, + bn(pctIncrease) + ) +} +const getExpectedPrice = async (ctx: CbEthCollateralFixtureContext): 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() + + return clData.answer + .mul(bn(10).pow(18 - clDecimals)) + .mul(clRptData.answer.mul(bn(10).pow(18 - clRptDecimals))) + .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 defaultCBEthCollateralOpts: CollateralOpts = { + erc20: CB_ETH, + targetName: ethers.utils.formatBytes32String('ETH'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ETH_USD_PRICE_FEED, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), +} + +/* + 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: resetFork, + collateralName: 'CBEthCollateral', + chainlinkDefaultAnswer, +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/cbeth/constants.ts b/test/plugins/individual-collateral/cbeth/constants.ts new file mode 100644 index 000000000..887a8db61 --- /dev/null +++ b/test/plugins/individual-collateral/cbeth/constants.ts @@ -0,0 +1,19 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const ETH_USD_PRICE_FEED = networkConfig['31337'].chainlinkFeeds.ETH as string +export const CBETH_ETH_PRICE_FEED = networkConfig['31337'].chainlinkFeeds.cbETH as string +export const CB_ETH = networkConfig['31337'].tokens.cbETH as string +export const WETH = networkConfig['31337'].tokens.WETH as string +export const CB_ETH_MINTER = '0xd0F73E06E7b88c8e1da291bB744c4eEBAf9Af59f' +export const CB_ETH_ORACLE = '0x9b37180d847B27ADC13C2277299045C1237Ae281' + +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 MAX_TRADE_VOL = bn(1000) + +export const FORK_BLOCK = 17479312 diff --git a/test/plugins/individual-collateral/cbeth/helpers.ts b/test/plugins/individual-collateral/cbeth/helpers.ts new file mode 100644 index 000000000..0b90ef428 --- /dev/null +++ b/test/plugins/individual-collateral/cbeth/helpers.ts @@ -0,0 +1,17 @@ +import { ethers } from 'hardhat' +import { CBEth } from '../../../../typechain' +import { BigNumberish } from 'ethers' +import { CB_ETH_MINTER, CB_ETH, FORK_BLOCK } from './constants' +import { getResetFork } from '../helpers' +import { whileImpersonating } from '#/utils/impersonation' +import hre from 'hardhat' +export const resetFork = getResetFork(FORK_BLOCK) + +export const mintCBETH = async (amount: BigNumberish, recipient: string) => { + const cbETH: CBEth = await ethers.getContractAt('CBEth', CB_ETH) + + await whileImpersonating(hre, CB_ETH_MINTER, async (minter) => { + await cbETH.connect(minter).configureMinter(CB_ETH_MINTER, amount) + await cbETH.connect(minter).mint(recipient, amount) + }) +} diff --git a/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts index c40542d33..17dffd882 100644 --- a/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/compoundv2/CTokenFiatCollateral.test.ts @@ -29,10 +29,12 @@ import { import { advanceBlocks, advanceTime, getLatestBlockTimestamp } from '../../../utils/time' import { Asset, + BadERC20, ComptrollerMock, CTokenFiatCollateral, CTokenMock, - CTokenVault, + CTokenWrapper, + CTokenWrapperMock, ERC20Mock, FacadeRead, FacadeTest, @@ -40,7 +42,6 @@ import { IAssetRegistry, InvalidMockV3Aggregator, MockV3Aggregator, - RewardableERC20Vault, RTokenAsset, TestIBackingManager, TestIBasketHandler, @@ -81,7 +82,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi // Tokens/Assets let dai: ERC20Mock let cDai: CTokenMock - let cDaiVault: RewardableERC20Vault + let cDaiVault: CTokenWrapper let cDaiCollateral: CTokenFiatCollateral let compToken: ERC20Mock let compAsset: Asset @@ -195,8 +196,8 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi ) ) - const cDaiVaultFactory: ContractFactory = await ethers.getContractFactory('CTokenVault') - cDaiVault = ( + const cDaiVaultFactory: ContractFactory = await ethers.getContractFactory('CTokenWrapper') + cDaiVault = ( await cDaiVaultFactory.deploy( cDai.address, 'cDAI RToken Vault', @@ -317,7 +318,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi expect(await cDaiCollateral.referenceERC20Decimals()).to.equal(await dai.decimals()) expect(await cDaiCollateral.erc20()).to.equal(cDaiVault.address) expect(await cDai.decimals()).to.equal(8) - expect(await cDaiVault.decimals()).to.equal(17) + expect(await cDaiVault.decimals()).to.equal(8) expect(await cDaiCollateral.targetName()).to.equal(ethers.utils.formatBytes32String('USD')) expect(await cDaiCollateral.refPerTok()).to.be.closeTo(fp('0.022'), fp('0.001')) expect(await cDaiCollateral.targetPerRef()).to.equal(fp('1')) @@ -342,6 +343,10 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi .withArgs(compToken.address, anyValue) expect(await cDaiCollateral.maxTradeVolume()).to.equal(config.rTokenMaxTradeVolume) + // Exchange rate + await cDaiVault.exchangeRateCurrent() + expect(await cDaiVault.exchangeRateStored()).to.equal(await cDaiVault.exchangeRateStored()) + // Should setup contracts expect(main.address).to.not.equal(ZERO_ADDRESS) }) @@ -382,7 +387,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi // Check RToken price const issueAmount: BigNumber = bn('10000e18') - await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) await advanceTime(3600) await expect(rToken.connect(addr1).issue(issueAmount)).to.emit(rToken, 'Issuance') await expectRTokenPrice( @@ -414,6 +419,42 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi REVENUE_HIDING ) ).to.be.revertedWith('missing erc20') + + // Check cToken with decimals = 0 in underlying + const token0decimals: BadERC20 = await ( + await ethers.getContractFactory('BadERC20') + ).deploy('Bad ERC20', 'BERC20') + await token0decimals.setDecimals(0) + + const CTokenWrapperMockFactory: ContractFactory = await ethers.getContractFactory( + 'CTokenWrapperMock' + ) + const vault: CTokenWrapperMock = ( + await CTokenWrapperMockFactory.deploy( + '0 Decimal Token', + '0 Decimal Token', + token0decimals.address, + compToken.address, + comptroller.address + ) + ) + + await expect( + CTokenCollateralFactory.deploy( + { + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.DAI as string, + oracleError: ORACLE_ERROR, + erc20: vault.address, + maxTradeVolume: config.rTokenMaxTradeVolume, + oracleTimeout: ORACLE_TIMEOUT, + targetName: ethers.utils.formatBytes32String('USD'), + defaultThreshold, + delayUntilDefault, + }, + REVENUE_HIDING + ) + ).to.be.revertedWith('referenceERC20Decimals missing') }) }) @@ -425,7 +466,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi const issueAmount: BigNumber = MIN_ISSUANCE_PER_BLOCK // instant issuance // Provide approvals for issuances - await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) await advanceTime(3600) @@ -533,12 +574,12 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi const newBalanceAddr1cDai: BigNumber = await cDaiVault.balanceOf(addr1.address) // Check received tokens represent ~10K in value at current prices - expect(newBalanceAddr1cDai.sub(balanceAddr1cDai)).to.be.closeTo(bn('303570e17'), bn('8e16')) // ~0.03294 * 303571 ~= 10K (100% of basket) + expect(newBalanceAddr1cDai.sub(balanceAddr1cDai)).to.be.closeTo(bn('303570e8'), bn('8e7')) // ~0.03294 * 303571 ~= 10K (100% of basket) // Check remainders in Backing Manager expect(await cDaiVault.balanceOf(backingManager.address)).to.be.closeTo( - bn('150663e17'), - bn('5e16') + bn('150663e8'), + bn('5e7') ) // ~= 4962.8 usd in value // Check total asset value (remainder) @@ -572,7 +613,7 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi expect(await compToken.balanceOf(backingManager.address)).to.equal(0) // Provide approvals for issuances - await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 17).mul(100)) + await cDaiVault.connect(addr1).approve(rToken.address, toBNDecimals(issueAmount, 8).mul(100)) await advanceTime(3600) @@ -743,8 +784,8 @@ describeFork(`CTokenFiatCollateral - Mainnet Forking P${IMPLEMENTATION}`, functi // Set initial exchange rate to the new cDai Mock await cDaiMock.setExchangeRate(fp('0.02')) - const cDaiVaultFactory: ContractFactory = await ethers.getContractFactory('CTokenVault') - const cDaiMockVault = ( + const cDaiVaultFactory: ContractFactory = await ethers.getContractFactory('CTokenWrapper') + const cDaiMockVault = ( await cDaiVaultFactory.deploy( cDaiMock.address, 'cDAI Mock RToken Vault', diff --git a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts b/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts index a09f88fbb..8a44e447a 100644 --- a/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts +++ b/test/plugins/individual-collateral/compoundv3/CusdcV3Wrapper.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import hre, { ethers, network } from 'hardhat' import { useEnv } from '#/utils/env' +import { whileImpersonating } from '../../../utils/impersonation' import { advanceTime, advanceBlocks } from '../../../utils/time' import { allocateUSDC, enableRewardsAccrual, mintWcUSDC, makewCSUDC, resetFork } from './helpers' import { COMP, REWARDS } from './constants' @@ -15,7 +16,7 @@ import { bn } from '../../../../common/numbers' import { getChainId } from '../../../../common/blockchain-utils' import { networkConfig } from '../../../../common/configuration' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { ZERO_ADDRESS } from '../../../../common/constants' +import { MAX_UINT256, ZERO_ADDRESS } from '../../../../common/constants' const describeFork = useEnv('FORK') ? describe : describe.skip @@ -52,6 +53,15 @@ describeFork('Wrapped CUSDCv3', () => { await expect(CusdcV3WrapperFactory.deploy(ZERO_ADDRESS, REWARDS, COMP)).to.be.reverted }) + it('configuration/state', async () => { + expect(await wcusdcV3.symbol()).to.equal('wcUSDCv3') + expect(await wcusdcV3.name()).to.equal('Wrapped cUSDCv3') + expect(await wcusdcV3.totalSupply()).to.equal(bn(0)) + + expect(await wcusdcV3.underlyingComet()).to.equal(cusdcV3.address) + expect(await wcusdcV3.rewardERC20()).to.equal(COMP) + }) + describe('deposit', () => { const amount = bn('20000e6') @@ -163,6 +173,12 @@ describeFork('Wrapped CUSDCv3', () => { wcusdcV3.connect(bob).deposit(ethers.constants.MaxUint256) ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') }) + + it('desposit to zero address reverts', async () => { + await expect( + wcusdcV3.connect(bob).depositTo(ZERO_ADDRESS, ethers.constants.MaxUint256) + ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') + }) }) describe('withdraw', () => { @@ -292,10 +308,92 @@ describeFork('Wrapped CUSDCv3', () => { await mintWcUSDC(usdc, cusdcV3, wcusdcV3, bob, bn('20000e6'), bob.address) }) + it('sets max allowance with approval', async () => { + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) + + // set approve + await wcusdcV3.connect(bob).allow(don.address, true) + + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) + + // rollback approve + await wcusdcV3.connect(bob).allow(don.address, false) + + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) + }) + it('does not transfer without approval', async () => { await expect( wcusdcV3.connect(bob).transferFrom(don.address, bob.address, bn('10000e6')) ).to.be.revertedWithCustomError(wcusdcV3, 'Unauthorized') + + // Perform approval + await wcusdcV3.connect(bob).allow(don.address, true) + + await expect( + wcusdcV3.connect(don).transferFrom(bob.address, don.address, bn('10000e6')) + ).to.emit(wcusdcV3, 'Transfer') + }) + + it('transfer from/to zero address revert', async () => { + await expect( + wcusdcV3.connect(bob).transfer(ZERO_ADDRESS, bn('100e6')) + ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') + + await whileImpersonating(ZERO_ADDRESS, async (signer) => { + await expect( + wcusdcV3.connect(signer).transfer(don.address, bn('100e6')) + ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') + }) + }) + + it('performs validation on transfer amount', async () => { + await expect( + wcusdcV3.connect(bob).transfer(don.address, bn('40000e6')) + ).to.be.revertedWithCustomError(wcusdcV3, 'ExceedsBalance') + }) + + it('supports IERC20.approve and performs validations', async () => { + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) + + // Cannot set approve to the zero address + await expect( + wcusdcV3.connect(bob).approve(ZERO_ADDRESS, bn('10000e6')) + ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') + + // Can set full allowance with max uint256 + await expect(wcusdcV3.connect(bob).approve(don.address, MAX_UINT256)).to.emit( + wcusdcV3, + 'Approval' + ) + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(MAX_UINT256) + expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(true) + + // Can revert allowance with zero amount + await expect(wcusdcV3.connect(bob).approve(don.address, bn(0))).to.emit(wcusdcV3, 'Approval') + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) + + // Any other amount reverts + await expect( + wcusdcV3.connect(bob).approve(don.address, bn('10000e6')) + ).to.be.revertedWithCustomError(wcusdcV3, 'BadAmount') + expect(await wcusdcV3.allowance(bob.address, don.address)).to.equal(bn(0)) + expect(await wcusdcV3.hasPermission(bob.address, don.address)).to.equal(false) + }) + + it('perform validations on allow', async () => { + await expect(wcusdcV3.connect(bob).allow(ZERO_ADDRESS, true)).to.be.revertedWithCustomError( + wcusdcV3, + 'ZeroAddress' + ) + + await whileImpersonating(ZERO_ADDRESS, async (signer) => { + await expect( + wcusdcV3.connect(signer).allow(don.address, true) + ).to.be.revertedWithCustomError(wcusdcV3, 'ZeroAddress') + }) }) it('updates balances and rewards in sender and receiver', async () => { diff --git a/test/plugins/individual-collateral/convex/CvxStableMetapoolSuite.test.ts b/test/plugins/individual-collateral/convex/CvxStableMetapoolSuite.test.ts deleted file mode 100644 index e73d306f9..000000000 --- a/test/plugins/individual-collateral/convex/CvxStableMetapoolSuite.test.ts +++ /dev/null @@ -1,768 +0,0 @@ -import { - CollateralFixtureContext, - CollateralOpts, - CollateralStatus, - MintCollateralFunc, -} from '../pluginTestTypes' -import { makeWMIM3Pool, mintWMIM3Pool, WrappedMIM3PoolFixture, resetFork } from './helpers' -import hre, { ethers } from 'hardhat' -import { BigNumberish } from 'ethers' -import { - CvxStableMetapoolCollateral, - ERC20Mock, - InvalidMockV3Aggregator, - MockV3Aggregator, - MockV3Aggregator__factory, - TestICollateral, -} from '../../../../typechain' -import { bn, fp } from '../../../../common/numbers' -import { MAX_UINT192, MAX_UINT48, ZERO_ADDRESS } from '../../../../common/constants' -import { expect } from 'chai' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import { - PRICE_TIMEOUT, - THREE_POOL, - THREE_POOL_TOKEN, - THREE_POOL_DEFAULT_THRESHOLD, - CVX, - DAI_USD_FEED, - DAI_ORACLE_TIMEOUT, - DAI_ORACLE_ERROR, - MIM_DEFAULT_THRESHOLD, - MIM_USD_FEED, - MIM_ORACLE_TIMEOUT, - MIM_ORACLE_ERROR, - MIM_THREE_POOL, - MIM_THREE_POOL_HOLDER, - USDC_USD_FEED, - USDC_ORACLE_TIMEOUT, - USDC_ORACLE_ERROR, - USDT_USD_FEED, - USDT_ORACLE_TIMEOUT, - USDT_ORACLE_ERROR, - MAX_TRADE_VOL, - DELAY_UNTIL_DEFAULT, - CurvePoolType, - CRV, -} from './constants' -import { useEnv } from '#/utils/env' -import { getChainId } from '#/common/blockchain-utils' -import { networkConfig } from '#/common/configuration' -import { - advanceBlocks, - advanceTime, - getLatestBlockTimestamp, - setNextBlockTimestamp, -} from '#/test/utils/time' - -type Fixture = () => Promise - -/* - Define interfaces -*/ - -interface CvxStableMetapoolCollateralFixtureContext - extends CollateralFixtureContext, - WrappedMIM3PoolFixture { - usdcFeed: MockV3Aggregator - daiFeed: MockV3Aggregator - usdtFeed: MockV3Aggregator - cvx: ERC20Mock - crv: ERC20Mock -} - -interface CvxStableCollateralOpts extends CollateralOpts { - revenueHiding?: BigNumberish - nTokens?: BigNumberish - curvePool?: string - poolType?: CurvePoolType - feeds?: string[][] - oracleTimeouts?: BigNumberish[][] - oracleErrors?: BigNumberish[][] - lpToken?: string - pairedTokenDefaultThreshold?: BigNumberish - metapoolToken?: string -} - -/* - Define deployment functions -*/ - -export const defaultCvxStableCollateralOpts: CvxStableCollateralOpts = { - erc20: ZERO_ADDRESS, - targetName: ethers.utils.formatBytes32String('USD'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: MIM_USD_FEED, - oracleTimeout: MIM_ORACLE_TIMEOUT, - oracleError: MIM_ORACLE_ERROR, - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: THREE_POOL_DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - revenueHiding: bn('0'), // TODO - nTokens: bn('3'), - curvePool: THREE_POOL, - lpToken: THREE_POOL_TOKEN, - poolType: CurvePoolType.Plain, - feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], - oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], - metapoolToken: MIM_THREE_POOL, - pairedTokenDefaultThreshold: MIM_DEFAULT_THRESHOLD, -} - -export const deployCollateral = async ( - opts: CvxStableCollateralOpts = {} -): Promise => { - if (!opts.erc20 && !opts.feeds && !opts.chainlinkFeed) { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all 3 feeds: DAI, USDC, USDT - const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const mimFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const fix = await makeWMIM3Pool() - - opts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] - opts.erc20 = fix.wPool.address - opts.chainlinkFeed = mimFeed.address - } - - opts = { ...defaultCvxStableCollateralOpts, ...opts } - - const CvxStableCollateralFactory = await ethers.getContractFactory('CvxStableMetapoolCollateral') - - const collateral = await CvxStableCollateralFactory.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!, - { - nTokens: opts.nTokens!, - curvePool: opts.curvePool!, - poolType: opts.poolType!, - feeds: opts.feeds!, - oracleTimeouts: opts.oracleTimeouts!, - oracleErrors: opts.oracleErrors!, - lpToken: opts.lpToken!, - }, - opts.metapoolToken!, - opts.pairedTokenDefaultThreshold! - ) - await collateral.deployed() - - // sometimes we are trying to test a negative test case and we want this to fail silently - // fortunately this syntax fails silently because our tools are terrible - await expect(collateral.refresh()) - - return collateral -} - -const makeCollateralFixtureContext = ( - alice: SignerWithAddress, - opts: CvxStableCollateralOpts = {} -): Fixture => { - const collateralOpts = { ...defaultCvxStableCollateralOpts, ...opts } - - const makeCollateralFixtureContext = async () => { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all 3 feeds: DAI, USDC, USDT - const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const mimFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const fix = await makeWMIM3Pool() - - collateralOpts.erc20 = fix.wPool.address - collateralOpts.chainlinkFeed = mimFeed.address - collateralOpts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] - collateralOpts.curvePool = fix.curvePool.address - collateralOpts.metapoolToken = fix.metapoolToken.address - - const collateral = ((await deployCollateral(collateralOpts)) as unknown) - const rewardToken = await ethers.getContractAt('ERC20Mock', CVX) // use CVX - - const cvx = await ethers.getContractAt('ERC20Mock', CVX) - const crv = await ethers.getContractAt('ERC20Mock', CRV) - - return { - alice, - collateral, - chainlinkFeed: mimFeed, - metapoolToken: fix.metapoolToken, - realMetapool: fix.realMetapool, - curvePool: fix.curvePool, - wPool: fix.wPool, - dai: fix.dai, - usdc: fix.usdc, - usdt: fix.usdt, - mim: fix.mim, - tok: fix.wPool, - rewardToken, - daiFeed, - usdcFeed, - usdtFeed, - cvx, - crv, - } - } - - return makeCollateralFixtureContext -} - -/* - Define helper functions -*/ - -const mintCollateralTo: MintCollateralFunc = async ( - ctx: CvxStableMetapoolCollateralFixtureContext, - amount: BigNumberish, - user: SignerWithAddress, - recipient: string -) => { - await mintWMIM3Pool(ctx, amount, user, recipient, MIM_THREE_POOL_HOLDER) -} - -/* - Define collateral-specific tests -*/ - -const collateralSpecificConstructorTests = () => { - it('does not allow 0 defaultThreshold', async () => { - await expect(deployCollateral({ defaultThreshold: bn('0') })).to.be.revertedWith( - 'defaultThreshold zero' - ) - }) - - it('does not allow more than 4 tokens', async () => { - await expect(deployCollateral({ nTokens: 5 })).to.be.revertedWith('up to 4 tokens max') - }) - - it('does not allow empty curvePool', async () => { - await expect(deployCollateral({ curvePool: ZERO_ADDRESS })).to.be.revertedWith( - 'curvePool address is zero' - ) - }) - - it('does not allow more than 2 price feeds', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED, DAI_USD_FEED, DAI_USD_FEED], [], []], - }) - ).to.be.revertedWith('price feeds limited to 2') - }) - - it('requires at least 1 price feed per token', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED, DAI_USD_FEED], [USDC_USD_FEED], []], - }) - ).to.be.revertedWith('each token needs at least 1 price feed') - }) - - it('requires non-zero-address feeds', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[ZERO_ADDRESS], [USDC_USD_FEED], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t0feed0 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED, ZERO_ADDRESS], [USDC_USD_FEED], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t0feed1 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[USDC_USD_FEED], [ZERO_ADDRESS], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t1feed0 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED], [USDC_USD_FEED, ZERO_ADDRESS], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t1feed1 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t2feed0 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED, ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t2feed1 empty') - }) - - it('requires non-zero oracleTimeouts', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - oracleTimeouts: [[bn('0')], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t0timeout0 zero') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - oracleTimeouts: [[USDC_ORACLE_TIMEOUT], [bn('0')], [USDT_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t1timeout0 zero') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [bn('0')]], - }) - ).to.be.revertedWith('t2timeout0 zero') - }) - - it('requires non-zero oracleErrors', async () => { - await expect( - deployCollateral({ - oracleErrors: [[fp('1')], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t0error0 too large') - await expect( - deployCollateral({ - oracleErrors: [[USDC_ORACLE_ERROR], [fp('1')], [USDT_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t1error0 too large') - await expect( - deployCollateral({ oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [fp('1')]] }) - ).to.be.revertedWith('t2error0 too large') - }) -} - -/* - Run the test suite -*/ - -const describeFork = useEnv('FORK') ? describe : describe.skip - -describeFork(`Collateral: Convex - Stable Metapool (MIM+3Pool)`, () => { - before(resetFork) - describe('constructor validation', () => { - it('validates targetName', async () => { - await expect(deployCollateral({ targetName: ethers.constants.HashZero })).to.be.revertedWith( - 'targetName missing' - ) - }) - - it('does not allow missing ERC20', async () => { - await expect(deployCollateral({ erc20: ethers.constants.AddressZero })).to.be.revertedWith( - 'missing erc20' - ) - }) - - it('does not allow missing chainlink feed', async () => { - await expect( - deployCollateral({ chainlinkFeed: ethers.constants.AddressZero }) - ).to.be.revertedWith('missing chainlink feed') - }) - - it('max trade volume must be greater than zero', async () => { - await expect(deployCollateral({ maxTradeVolume: 0 })).to.be.revertedWith( - 'invalid max trade volume' - ) - }) - - it('does not allow oracle timeout at 0', async () => { - await expect(deployCollateral({ oracleTimeout: 0 })).to.be.revertedWith('oracleTimeout zero') - }) - - it('does not allow missing delayUntilDefault if defaultThreshold > 0', async () => { - await expect(deployCollateral({ delayUntilDefault: 0 })).to.be.revertedWith( - 'delayUntilDefault zero' - ) - }) - - describe('collateral-specific tests', collateralSpecificConstructorTests) - }) - - describe('collateral functionality', () => { - let ctx: CvxStableMetapoolCollateralFixtureContext - let alice: SignerWithAddress - - let wallet: SignerWithAddress - let chainId: number - - let collateral: TestICollateral - let chainlinkFeed: MockV3Aggregator - let usdcFeed: MockV3Aggregator - let daiFeed: MockV3Aggregator - let usdtFeed: MockV3Aggregator - - let crv: ERC20Mock - let cvx: ERC20Mock - - before(async () => { - ;[wallet] = (await ethers.getSigners()) as unknown as SignerWithAddress[] - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - await resetFork() - }) - - beforeEach(async () => { - ;[, alice] = await ethers.getSigners() - ctx = await loadFixture(makeCollateralFixtureContext(alice, {})) - ;({ chainlinkFeed, collateral, usdcFeed, daiFeed, usdtFeed, crv, cvx } = ctx) - await mintCollateralTo(ctx, bn('100e18'), wallet, wallet.address) - }) - - describe('functions', () => { - it('returns the correct bal (18 decimals)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - const aliceBal = await collateral.bal(alice.address) - expect(aliceBal).to.closeTo( - amount.mul(bn(10).pow(18 - (await ctx.tok.decimals()))), - bn('100').mul(bn(10).pow(18 - (await ctx.tok.decimals()))) - ) - }) - }) - - describe('rewards', () => { - it('does not revert', async () => { - await expect(collateral.claimRewards()).to.not.be.reverted - }) - - it('claims rewards (plugin)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, collateral.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(collateral.address) - const cvxBefore = await cvx.balanceOf(collateral.address) - await expect(collateral.claimRewards()).to.emit(ctx.wPool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(collateral.address) - const cvxAfter = await cvx.balanceOf(collateral.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - - it('claims rewards (wrapper)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(alice.address) - const cvxBefore = await cvx.balanceOf(alice.address) - await expect(ctx.wPool.connect(alice).claimRewards()).to.emit(ctx.wPool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(alice.address) - const cvxAfter = await cvx.balanceOf(alice.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - }) - - describe('prices', () => { - it('prices change as feed price changes', async () => { - const feedData = await usdcFeed.latestRoundData() - const initialRefPerTok = await collateral.refPerTok() - - const [low, high] = await collateral.price() - - // Update values in Oracles increase by 10% - const newPrice = feedData.answer.mul(110).div(100) - - await Promise.all([ - usdcFeed.updateAnswer(newPrice).then((e) => e.wait()), - daiFeed.updateAnswer(newPrice).then((e) => e.wait()), - usdtFeed.updateAnswer(newPrice).then((e) => e.wait()), - chainlinkFeed.updateAnswer(newPrice).then((e) => e.wait()), - ]) - - const [newLow, newHigh] = await collateral.price() - - expect(newLow).to.be.closeTo(low.mul(110).div(100), bn('1e3')) // rounding - expect(newHigh).to.be.closeTo(high.mul(110).div(100), bn('1e3')) - - // Check refPerTok remains the same (because we have not refreshed) - const finalRefPerTok = await collateral.refPerTok() - expect(finalRefPerTok).to.equal(initialRefPerTok) - }) - - it('prices change as refPerTok changes', async () => { - const initRefPerTok = await collateral.refPerTok() - const curveVirtualPrice = await ctx.metapoolToken.get_virtual_price() - await ctx.metapoolToken.setVirtualPrice(curveVirtualPrice.add(1e4)) - await collateral.refresh() - expect(await collateral.refPerTok()).to.be.gt(initRefPerTok) - }) - - it('returns a 0 price', async () => { - await Promise.all([ - usdcFeed.updateAnswer(0).then((e) => e.wait()), - daiFeed.updateAnswer(0).then((e) => e.wait()), - usdtFeed.updateAnswer(0).then((e) => e.wait()), - chainlinkFeed.updateAnswer(0).then((e) => e.wait()), - ]) - - // (0, FIX_MAX) is returned - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(0) - - // When refreshed, sets status to Unpriced - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('reverts in case of invalid timestamp', async () => { - await usdcFeed.setInvalidTimestamp() - - // Check price of token - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(MAX_UINT192) - - // When refreshed, sets status to Unpriced - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('decays lotPrice over priceTimeout period', async () => { - // Prices should start out equal - const p = await collateral.price() - let lotP = await collateral.lotPrice() - expect(p.length).to.equal(lotP.length) - expect(p[0]).to.equal(lotP[0]) - expect(p[1]).to.equal(lotP[1]) - - // Should be roughly half, after half of priceTimeout - const priceTimeout = await collateral.priceTimeout() - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand - expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand - - // Should be 0 after full priceTimeout - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) - }) - }) - - describe('status', () => { - before(resetFork) - it('maintains status in normal situations', async () => { - // Check initial state - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Force updates (with no changes) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - }) - - it('enters IFFY state when reference unit depegs below low threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg MIM:USD slightly, should remain SOUND - let updateAnswerTx = await chainlinkFeed.updateAnswer(bn('9.7e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - let nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - let expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - - // Depeg MIM:USD - Reducing price by 20% from 1 to 0.94 - updateAnswerTx = await chainlinkFeed.updateAnswer(bn('9.4e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters IFFY state when reference unit depegs above high threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg MIM:USD - Raising price by 20% from 1 to 1.2 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('1.2e8')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters DISABLED state when reference unit depegs for too long', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg MIM:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - - // Move time forward past delayUntilDefault - await advanceTime(delayUntilDefault) - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - - // Nothing changes if attempt to refresh after default - const prevWhenDefault: bigint = (await collateral.whenDefault()).toBigInt() - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(prevWhenDefault) - }) - - it('enters DISABLED state when refPerTok() decreases', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - const currentExchangeRate = await ctx.metapoolToken.get_virtual_price() - await ctx.metapoolToken.setVirtualPrice(currentExchangeRate.sub(1e3)).then((e) => e.wait()) - - // Collateral defaults due to refPerTok() going down - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('enters IFFY state when price becomes stale', async () => { - const oracleTimeout = DAI_ORACLE_TIMEOUT.toNumber() - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('does revenue hiding correctly', async () => { - ctx = await loadFixture(makeCollateralFixtureContext(alice, { revenueHiding: fp('1e-6') })) - ;({ collateral } = ctx) - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Decrease refPerTok by 1 part in a million - const currentExchangeRate = await ctx.metapoolToken.get_virtual_price() - const newVirtualPrice = currentExchangeRate.sub(currentExchangeRate.div(bn('1e6'))) - await ctx.metapoolToken.setVirtualPrice(newVirtualPrice) - - // Collateral remains SOUND - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // One quanta more of decrease results in default - await ctx.metapoolToken.setVirtualPrice(newVirtualPrice.sub(1)) - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { - const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( - 'InvalidMockV3Aggregator' - ) - const invalidChainlinkFeed = ( - await InvalidMockV3AggregatorFactory.deploy(6, bn('1e6')) - ) - - const fix = await makeWMIM3Pool() - - const invalidCollateral = await deployCollateral({ - erc20: fix.wPool.address, - chainlinkFeed: invalidChainlinkFeed.address, // shouldn't be necessary - feeds: [ - [invalidChainlinkFeed.address], - [invalidChainlinkFeed.address], - [invalidChainlinkFeed.address], - ], - }) - - // Reverting with no reason - await invalidChainlinkFeed.setSimplyRevert(true) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - - // Runnning out of gas (same error) - await invalidChainlinkFeed.setSimplyRevert(false) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - }) - }) - }) -}) diff --git a/test/plugins/individual-collateral/convex/CvxStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/convex/CvxStableRTokenMetapoolTestSuite.test.ts deleted file mode 100644 index 856c13bbc..000000000 --- a/test/plugins/individual-collateral/convex/CvxStableRTokenMetapoolTestSuite.test.ts +++ /dev/null @@ -1,771 +0,0 @@ -import { - CollateralFixtureContext, - CollateralOpts, - CollateralStatus, - MintCollateralFunc, -} from '../pluginTestTypes' -import { makeWeUSDFraxBP, mintWeUSDFraxBP, WrappedEUSDFraxBPFixture, resetFork } from './helpers' -import hre, { ethers } from 'hardhat' -import { ContractFactory, BigNumberish } from 'ethers' -import { - CvxStableRTokenMetapoolCollateral, - ERC20Mock, - InvalidMockV3Aggregator, - MockV3Aggregator, - MockV3Aggregator__factory, - TestICollateral, -} from '../../../../typechain' -import { bn, fp } from '../../../../common/numbers' -import { - MAX_UINT256, - MAX_UINT192, - MAX_UINT48, - ZERO_ADDRESS, - ONE_ADDRESS, -} from '../../../../common/constants' -import { expect } from 'chai' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import { - PRICE_TIMEOUT, - eUSD_FRAX_BP, - FRAX_BP, - FRAX_BP_TOKEN, - CVX, - USDC_USD_FEED, - USDC_ORACLE_TIMEOUT, - USDC_ORACLE_ERROR, - FRAX_USD_FEED, - FRAX_ORACLE_TIMEOUT, - FRAX_ORACLE_ERROR, - MAX_TRADE_VOL, - DEFAULT_THRESHOLD, - RTOKEN_DELAY_UNTIL_DEFAULT, - CurvePoolType, - CRV, - eUSD_FRAX_HOLDER, -} from './constants' -import { useEnv } from '#/utils/env' -import { getChainId } from '#/common/blockchain-utils' -import { networkConfig } from '#/common/configuration' -import { - advanceBlocks, - advanceTime, - getLatestBlockTimestamp, - setNextBlockTimestamp, -} from '#/test/utils/time' - -type Fixture = () => Promise - -/* - Define interfaces - */ - -interface CvxStableRTokenMetapoolCollateralFixtureContext - extends CollateralFixtureContext, - WrappedEUSDFraxBPFixture { - fraxFeed: MockV3Aggregator - usdcFeed: MockV3Aggregator - eusdFeed: MockV3Aggregator - cvx: ERC20Mock - crv: ERC20Mock -} - -// interface CometCollateralFixtureContextMockComet extends CollateralFixtureContext { -// cusdcV3: CometMock -// wcusdcV3: ICusdcV3Wrapper -// usdc: ERC20Mock -// wcusdcV3Mock: CusdcV3WrapperMock -// } - -interface CvxStableRTokenMetapoolCollateralOpts extends CollateralOpts { - revenueHiding?: BigNumberish - nTokens?: BigNumberish - curvePool?: string - poolType?: CurvePoolType // for underlying fraxBP pool - feeds?: string[][] - oracleTimeouts?: BigNumberish[][] - oracleErrors?: BigNumberish[][] - lpToken?: string - metapoolToken?: string -} - -/* - Define deployment functions - */ - -export const defaultCvxStableCollateralOpts: CvxStableRTokenMetapoolCollateralOpts = { - erc20: ZERO_ADDRESS, - targetName: ethers.utils.formatBytes32String('USD'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero - oracleTimeout: bn('1'), // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, - revenueHiding: bn('0'), // TODO - nTokens: bn('2'), - curvePool: FRAX_BP, - lpToken: FRAX_BP_TOKEN, - poolType: CurvePoolType.Plain, // for fraxBP, not the top-level pool - feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], - oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], - oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], - metapoolToken: eUSD_FRAX_BP, -} - -export const deployCollateral = async ( - opts: CvxStableRTokenMetapoolCollateralOpts = {} -): Promise => { - if (!opts.erc20 && !opts.feeds) { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all 3 feeds: FRAX, USDC, eUSD - const fraxFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const eusdFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const fix = await makeWeUSDFraxBP(eusdFeed) - - opts.feeds = [[fraxFeed.address], [usdcFeed.address]] - opts.erc20 = fix.wPool.address - } - - opts = { ...defaultCvxStableCollateralOpts, ...opts } - - const CvxStableRTokenMetapoolCollateralFactory: ContractFactory = await ethers.getContractFactory( - 'CvxStableRTokenMetapoolCollateral' - ) - - const collateral = ( - await CvxStableRTokenMetapoolCollateralFactory.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, - { - nTokens: opts.nTokens, - curvePool: opts.curvePool, - poolType: opts.poolType, - feeds: opts.feeds, - oracleTimeouts: opts.oracleTimeouts, - oracleErrors: opts.oracleErrors, - lpToken: opts.lpToken, - }, - opts.metapoolToken, - opts.defaultThreshold // use same 2% value - ) - ) - await collateral.deployed() - - // sometimes we are trying to test a negative test case and we want this to fail silently - // fortunately this syntax fails silently because our tools are terrible - await expect(collateral.refresh()) - - return collateral -} - -const makeCollateralFixtureContext = ( - alice: SignerWithAddress, - opts: CvxStableRTokenMetapoolCollateralOpts = {} -): Fixture => { - const collateralOpts = { ...defaultCvxStableCollateralOpts, ...opts } - - const makeCollateralFixtureContext = async () => { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all feeds: FRAX, USDC, RToken - const fraxFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const eusdFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - - const fix = await makeWeUSDFraxBP(eusdFeed) - collateralOpts.feeds = [[fraxFeed.address], [usdcFeed.address]] - - collateralOpts.erc20 = fix.wPool.address - collateralOpts.curvePool = fix.curvePool.address - collateralOpts.metapoolToken = fix.metapoolToken.address - - const collateral = ((await deployCollateral(collateralOpts)) as unknown) - const rewardToken = await ethers.getContractAt('ERC20Mock', CVX) // use CVX - - const cvx = await ethers.getContractAt('ERC20Mock', CVX) - const crv = await ethers.getContractAt('ERC20Mock', CRV) - - return { - alice, - collateral, - chainlinkFeed: usdcFeed, - metapoolToken: fix.metapoolToken, - realMetapool: fix.realMetapool, - curvePool: fix.curvePool, - wPool: fix.wPool, - frax: fix.frax, - usdc: fix.usdc, - eusd: fix.eusd, - tok: fix.wPool, - rewardToken, - fraxFeed, - usdcFeed, - eusdFeed, - cvx, - crv, - } - } - - return makeCollateralFixtureContext -} - -/* - Define helper functions - */ - -const mintCollateralTo: MintCollateralFunc< - CvxStableRTokenMetapoolCollateralFixtureContext -> = async ( - ctx: CvxStableRTokenMetapoolCollateralFixtureContext, - amount: BigNumberish, - user: SignerWithAddress, - recipient: string -) => { - await mintWeUSDFraxBP(ctx, amount, user, recipient, eUSD_FRAX_HOLDER) -} - -/* - Define collateral-specific tests - */ - -const collateralSpecificConstructorTests = () => { - it('does not allow 0 defaultThreshold', async () => { - await expect(deployCollateral({ defaultThreshold: bn('0') })).to.be.revertedWith( - 'defaultThreshold zero' - ) - }) - - it('does not allow more than 2 tokens', async () => { - await expect(deployCollateral({ nTokens: 1 })).to.be.reverted - await expect(deployCollateral({ nTokens: 3 })).to.be.reverted - }) - - it('does not allow empty curvePool', async () => { - await expect(deployCollateral({ curvePool: ZERO_ADDRESS })).to.be.revertedWith( - 'curvePool address is zero' - ) - }) - - it('does not allow more than 2 price feeds', async () => { - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - feeds: [[FRAX_USD_FEED, FRAX_USD_FEED, FRAX_USD_FEED], [], []], - }) - ).to.be.revertedWith('price feeds limited to 2') - }) - - it('requires at least 1 price feed per token', async () => { - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - feeds: [[FRAX_USD_FEED, FRAX_USD_FEED], [USDC_USD_FEED], []], - }) - ).to.be.revertedWith('each token needs at least 1 price feed') - }) - - it('requires non-zero-address feeds', async () => { - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - feeds: [[ZERO_ADDRESS], [FRAX_USD_FEED]], - }) - ).to.be.revertedWith('t0feed0 empty') - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - feeds: [[FRAX_USD_FEED, ZERO_ADDRESS], [USDC_USD_FEED]], - }) - ).to.be.revertedWith('t0feed1 empty') - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - feeds: [[FRAX_USD_FEED], [ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t1feed0 empty') - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - feeds: [[FRAX_USD_FEED], [USDC_USD_FEED, ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t1feed1 empty') - }) - - it('requires non-zero oracleTimeouts', async () => { - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - oracleTimeouts: [[bn('0')], [FRAX_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t0timeout0 zero') - await expect( - deployCollateral({ - erc20: eUSD_FRAX_BP, // can be anything. - oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [bn('0')]], - }) - ).to.be.revertedWith('t1timeout0 zero') - }) - - it('requires non-zero oracleErrors', async () => { - await expect( - deployCollateral({ - oracleErrors: [[fp('1')], [USDC_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t0error0 too large') - await expect( - deployCollateral({ - oracleErrors: [[FRAX_ORACLE_ERROR], [fp('1')]], - }) - ).to.be.revertedWith('t1error0 too large') - }) -} - -/* - Run the test suite - */ - -const describeFork = useEnv('FORK') ? describe : describe.skip - -describeFork(`Collateral: Convex - RToken Metapool (eUSD/fraxBP)`, () => { - before(resetFork) - describe('constructor validation', () => { - it('validates targetName', async () => { - await expect(deployCollateral({ targetName: ethers.constants.HashZero })).to.be.revertedWith( - 'targetName missing' - ) - }) - - it('does not allow missing ERC20', async () => { - await expect(deployCollateral({ erc20: ethers.constants.AddressZero })).to.be.revertedWith( - 'missing erc20' - ) - }) - - it('does not allow missing chainlink feed', async () => { - await expect( - deployCollateral({ chainlinkFeed: ethers.constants.AddressZero }) - ).to.be.revertedWith('missing chainlink feed') - }) - - it('max trade volume must be greater than zero', async () => { - await expect(deployCollateral({ maxTradeVolume: 0 })).to.be.revertedWith( - 'invalid max trade volume' - ) - }) - - it('does not allow oracle timeout at 0', async () => { - await expect(deployCollateral({ oracleTimeout: 0 })).to.be.revertedWith('oracleTimeout zero') - }) - - it('does not allow missing delayUntilDefault if defaultThreshold > 0', async () => { - await expect(deployCollateral({ delayUntilDefault: 0 })).to.be.revertedWith( - 'delayUntilDefault zero' - ) - }) - - describe('collateral-specific tests', collateralSpecificConstructorTests) - }) - - describe('collateral functionality', () => { - let ctx: CvxStableRTokenMetapoolCollateralFixtureContext - let alice: SignerWithAddress - - let wallet: SignerWithAddress - let chainId: number - - let collateral: TestICollateral - let fraxFeed: MockV3Aggregator - let usdcFeed: MockV3Aggregator - let eusdFeed: MockV3Aggregator - - let crv: ERC20Mock - let cvx: ERC20Mock - - before(async () => { - ;[wallet] = (await ethers.getSigners()) as unknown as SignerWithAddress[] - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - }) - - beforeEach(async () => { - ;[, alice] = await ethers.getSigners() - ctx = await loadFixture(makeCollateralFixtureContext(alice, {})) - ;({ collateral, fraxFeed, usdcFeed, eusdFeed, crv, cvx } = ctx) - - await mintCollateralTo(ctx, bn('100e18'), wallet, wallet.address) - }) - - describe('functions', () => { - it('returns the correct bal (18 decimals)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - const aliceBal = await collateral.bal(alice.address) - expect(aliceBal).to.closeTo( - amount.mul(bn(10).pow(18 - (await ctx.tok.decimals()))), - bn('100').mul(bn(10).pow(18 - (await ctx.tok.decimals()))) - ) - }) - }) - - describe('rewards', () => { - it('does not revert', async () => { - await expect(collateral.claimRewards()).to.not.be.reverted - }) - - it('claims rewards (plugin)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, collateral.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(collateral.address) - const cvxBefore = await cvx.balanceOf(collateral.address) - await expect(collateral.claimRewards()).to.emit(ctx.wPool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(collateral.address) - const cvxAfter = await cvx.balanceOf(collateral.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - - it('claims rewards (wrapper)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(alice.address) - const cvxBefore = await cvx.balanceOf(alice.address) - await expect(ctx.wPool.connect(alice).claimRewards()).to.emit(ctx.wPool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(alice.address) - const cvxAfter = await cvx.balanceOf(alice.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - }) - - describe('prices', () => { - it('prices change as feed price changes', async () => { - const feedData = await usdcFeed.latestRoundData() - const initialRefPerTok = await collateral.refPerTok() - - const [low, high] = await collateral.price() - - // Update values in Oracles increase by 10% - const newPrice = feedData.answer.mul(110).div(100) - - await Promise.all([ - fraxFeed.updateAnswer(newPrice).then((e) => e.wait()), - usdcFeed.updateAnswer(newPrice).then((e) => e.wait()), - eusdFeed.updateAnswer(newPrice).then((e) => e.wait()), - ]) - - // Appreciated 10% - const [newLow, newHigh] = await collateral.price() - expect(newLow).to.be.closeTo(low.mul(110).div(100), 1) - expect(newHigh).to.be.closeTo(high.mul(110).div(100), 1) - - // Check refPerTok remains the same - const finalRefPerTok = await collateral.refPerTok() - expect(finalRefPerTok).to.equal(initialRefPerTok) - }) - - it('prices change as refPerTok changes', async () => { - const initRefPerTok = await collateral.refPerTok() - const curveVirtualPrice = await ctx.metapoolToken.get_virtual_price() - await ctx.metapoolToken.setVirtualPrice(curveVirtualPrice.add(1e4)) - await collateral.refresh() - expect(await collateral.refPerTok()).to.be.gt(initRefPerTok) - }) - - it('returns a 0 price', async () => { - await Promise.all([ - fraxFeed.updateAnswer(0).then((e) => e.wait()), - usdcFeed.updateAnswer(0).then((e) => e.wait()), - eusdFeed.updateAnswer(0).then((e) => e.wait()), - ]) - - // (0, 0) is returned - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(0) - - // When refreshed, sets status to IFFY - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('still reads out pool token price when paired price is broken', async () => { - await eusdFeed.updateAnswer(MAX_UINT256.div(2).sub(1)) - - // (>0.5, +inf) is returned - const [low, high] = await collateral.price() - expect(low).to.be.gt(fp('0.5')) - expect(high).to.be.gt(fp('1e27')) // won't quite be FIX_MAX always - - // When refreshed, sets status to IFFY - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('reverts in case of invalid timestamp', async () => { - await usdcFeed.setInvalidTimestamp() - - // Check price of token - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(MAX_UINT192) - - // When refreshed, sets status to IFFY - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('decays lotPrice over priceTimeout period', async () => { - // Prices should start out equal - const p = await collateral.price() - let lotP = await collateral.lotPrice() - expect(p.length).to.equal(lotP.length) - expect(p[0]).to.equal(lotP[0]) - expect(p[1]).to.equal(lotP[1]) - - // Should be roughly half, after half of priceTimeout - const priceTimeout = await collateral.priceTimeout() - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand - expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand - - // Should be 0 after full priceTimeout - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) - }) - }) - - describe('status', () => { - before(resetFork) - - it('maintains status in normal situations', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Force updates (with no changes) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - }) - - it('enters IFFY state when reference unit depegs below low threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await usdcFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters IFFY state when reference unit depegs above high threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Raising price by 20% from 1 to 1.2 - const updateAnswerTx = await usdcFeed.updateAnswer(bn('1.2e8')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters DISABLED state when reference unit depegs for too long', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await usdcFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - - // Move time forward past delayUntilDefault - await advanceTime(delayUntilDefault) - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - - // Nothing changes if attempt to refresh after default - const prevWhenDefault: bigint = (await collateral.whenDefault()).toBigInt() - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(prevWhenDefault) - }) - - // handy trick for dealing with expiring oracles - it('resets fork', async () => { - await resetFork() - }) - - it('enters DISABLED state when refPerTok() decreases', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - const currentExchangeRate = await ctx.metapoolToken.get_virtual_price() - await ctx.metapoolToken.setVirtualPrice(currentExchangeRate.sub(1e3)) - - // Collateral defaults due to refPerTok() going down - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('does revenue hiding correctly', async () => { - ctx = await loadFixture(makeCollateralFixtureContext(alice, { revenueHiding: fp('1e-6') })) - ;({ collateral } = ctx) - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Decrease refPerTok by 1 part in a million - const currentExchangeRate = await ctx.metapoolToken.get_virtual_price() - const newVirtualPrice = currentExchangeRate.sub(currentExchangeRate.div(bn('1e6'))) - await ctx.metapoolToken.setVirtualPrice(newVirtualPrice) - - // Collateral remains SOUND - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // One quanta more of decrease results in default - await ctx.metapoolToken.setVirtualPrice(newVirtualPrice.sub(1)) - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('enters IFFY state when price becomes stale', async () => { - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - const oracleTimeout = FRAX_ORACLE_TIMEOUT.toNumber() - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('enters IFFY state when _only_ the RToken de-pegs for 72h', async () => { - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - - // De-peg RToken to just below threshold of $0.98 - await eusdFeed.updateAnswer(fp('0.9799999')) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - - // Advance 72h - await setNextBlockTimestamp( - RTOKEN_DELAY_UNTIL_DEFAULT.add(await getLatestBlockTimestamp()).toNumber() - ) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - }) - - it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { - const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( - 'InvalidMockV3Aggregator' - ) - const invalidChainlinkFeed = ( - await InvalidMockV3AggregatorFactory.deploy(6, bn('1e6')) - ) - - const fix = await makeWeUSDFraxBP(eusdFeed) - - const invalidCollateral = await deployCollateral({ - erc20: fix.wPool.address, - feeds: [[invalidChainlinkFeed.address], [invalidChainlinkFeed.address]], - }) - - // Reverting with no reason - await invalidChainlinkFeed.setSimplyRevert(true) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - - // Runnning out of gas (same error) - await invalidChainlinkFeed.setSimplyRevert(false) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - }) - }) - }) -}) diff --git a/test/plugins/individual-collateral/convex/CvxStableTestSuite.test.ts b/test/plugins/individual-collateral/convex/CvxStableTestSuite.test.ts deleted file mode 100644 index 9cb9f7acb..000000000 --- a/test/plugins/individual-collateral/convex/CvxStableTestSuite.test.ts +++ /dev/null @@ -1,756 +0,0 @@ -import { - CollateralFixtureContext, - CollateralOpts, - CollateralStatus, - MintCollateralFunc, -} from '../pluginTestTypes' -import { mintW3Pool, makeW3PoolStable, Wrapped3PoolFixtureStable, resetFork } from './helpers' -import hre, { ethers } from 'hardhat' -import { ContractFactory, BigNumberish } from 'ethers' -import { - CvxStableCollateral, - ERC20Mock, - InvalidMockV3Aggregator, - MockV3Aggregator, - MockV3Aggregator__factory, - TestICollateral, -} from '../../../../typechain' -import { bn, fp } from '../../../../common/numbers' -import { MAX_UINT192, MAX_UINT48, ZERO_ADDRESS } from '../../../../common/constants' -import { expect } from 'chai' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import { - PRICE_TIMEOUT, - THREE_POOL, - THREE_POOL_TOKEN, - CVX, - DAI_USD_FEED, - DAI_ORACLE_TIMEOUT, - DAI_ORACLE_ERROR, - USDC_USD_FEED, - USDC_ORACLE_TIMEOUT, - USDC_ORACLE_ERROR, - USDT_USD_FEED, - USDT_ORACLE_TIMEOUT, - USDT_ORACLE_ERROR, - MAX_TRADE_VOL, - DEFAULT_THRESHOLD, - DELAY_UNTIL_DEFAULT, - CurvePoolType, - CRV, - THREE_POOL_HOLDER, -} from './constants' -import { useEnv } from '#/utils/env' -import { getChainId } from '#/common/blockchain-utils' -import { networkConfig } from '#/common/configuration' -import { - advanceBlocks, - advanceTime, - getLatestBlockTimestamp, - setNextBlockTimestamp, -} from '#/test/utils/time' - -type Fixture = () => Promise - -/* - Define interfaces -*/ - -interface CvxStableCollateralFixtureContext - extends CollateralFixtureContext, - Wrapped3PoolFixtureStable { - usdcFeed: MockV3Aggregator - daiFeed: MockV3Aggregator - usdtFeed: MockV3Aggregator - cvx: ERC20Mock - crv: ERC20Mock -} - -// interface CometCollateralFixtureContextMockComet extends CollateralFixtureContext { -// cusdcV3: CometMock -// wcusdcV3: ICusdcV3Wrapper -// usdc: ERC20Mock -// wcusdcV3Mock: CusdcV3WrapperMock -// } - -interface CvxStableCollateralOpts extends CollateralOpts { - revenueHiding?: BigNumberish - nTokens?: BigNumberish - curvePool?: string - poolType?: CurvePoolType - feeds?: string[][] - oracleTimeouts?: BigNumberish[][] - oracleErrors?: BigNumberish[][] - lpToken?: string -} - -/* - Define deployment functions -*/ - -export const defaultCvxStableCollateralOpts: CvxStableCollateralOpts = { - erc20: ZERO_ADDRESS, - targetName: ethers.utils.formatBytes32String('USD'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: DAI_USD_FEED, // unused but cannot be zero - oracleTimeout: bn('1'), // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - revenueHiding: bn('0'), // TODO - nTokens: bn('3'), - curvePool: THREE_POOL, - lpToken: THREE_POOL_TOKEN, - poolType: CurvePoolType.Plain, - feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], - oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], - oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], -} - -export const deployCollateral = async ( - opts: CvxStableCollateralOpts = {} -): Promise => { - if (!opts.erc20 && !opts.feeds) { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all 3 feeds: DAI, USDC, USDT - const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const fix = await makeW3PoolStable() - - opts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] - opts.erc20 = fix.w3Pool.address - } - - opts = { ...defaultCvxStableCollateralOpts, ...opts } - - const CvxStableCollateralFactory: ContractFactory = await ethers.getContractFactory( - 'CvxStableCollateral' - ) - - const collateral = await CvxStableCollateralFactory.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, - { - nTokens: opts.nTokens, - curvePool: opts.curvePool, - poolType: opts.poolType, - feeds: opts.feeds, - oracleTimeouts: opts.oracleTimeouts, - oracleErrors: opts.oracleErrors, - lpToken: opts.lpToken, - } - ) - await collateral.deployed() - - // sometimes we are trying to test a negative test case and we want this to fail silently - // fortunately this syntax fails silently because our tools are terrible - await expect(collateral.refresh()) - - return collateral -} - -const makeCollateralFixtureContext = ( - alice: SignerWithAddress, - opts: CvxStableCollateralOpts = {} -): Fixture => { - const collateralOpts = { ...defaultCvxStableCollateralOpts, ...opts } - - const makeCollateralFixtureContext = async () => { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all 3 feeds: DAI, USDC, USDT - const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - collateralOpts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] - - const fix = await makeW3PoolStable() - - collateralOpts.erc20 = fix.w3Pool.address - collateralOpts.curvePool = fix.curvePool.address - const collateral = ((await deployCollateral(collateralOpts)) as unknown) - const rewardToken = await ethers.getContractAt('ERC20Mock', CVX) // use CVX - - const cvx = await ethers.getContractAt('ERC20Mock', CVX) - const crv = await ethers.getContractAt('ERC20Mock', CRV) - - return { - alice, - collateral, - chainlinkFeed: usdcFeed, - curvePool: fix.curvePool, - crv3Pool: fix.crv3Pool, - w3Pool: fix.w3Pool, - dai: fix.dai, - usdc: fix.usdc, - usdt: fix.usdt, - tok: fix.w3Pool, - rewardToken, - usdcFeed, - daiFeed, - usdtFeed, - cvx, - crv, - } - } - - return makeCollateralFixtureContext -} - -/* - Define helper functions -*/ - -const mintCollateralTo: MintCollateralFunc = async ( - ctx: CvxStableCollateralFixtureContext, - amount: BigNumberish, - user: SignerWithAddress, - recipient: string -) => { - await mintW3Pool(ctx, amount, user, recipient, THREE_POOL_HOLDER) -} - -/* - Define collateral-specific tests -*/ - -const collateralSpecificConstructorTests = () => { - it('does not allow 0 defaultThreshold', async () => { - await expect(deployCollateral({ defaultThreshold: bn('0') })).to.be.revertedWith( - 'defaultThreshold zero' - ) - }) - - it('does not allow more than 4 tokens', async () => { - await expect(deployCollateral({ nTokens: 5 })).to.be.revertedWith('up to 4 tokens max') - }) - - it('does not allow empty curvePool', async () => { - await expect(deployCollateral({ curvePool: ZERO_ADDRESS })).to.be.revertedWith( - 'curvePool address is zero' - ) - }) - - it('does not allow more than 2 price feeds', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED, DAI_USD_FEED, DAI_USD_FEED], [], []], - }) - ).to.be.revertedWith('price feeds limited to 2') - }) - - it('requires at least 1 price feed per token', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED, DAI_USD_FEED], [USDC_USD_FEED], []], - }) - ).to.be.revertedWith('each token needs at least 1 price feed') - }) - - it('requires non-zero-address feeds', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[ZERO_ADDRESS], [USDC_USD_FEED], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t0feed0 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED, ZERO_ADDRESS], [USDC_USD_FEED], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t0feed1 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[USDC_USD_FEED], [ZERO_ADDRESS], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t1feed0 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED], [USDC_USD_FEED, ZERO_ADDRESS], [USDT_USD_FEED]], - }) - ).to.be.revertedWith('t1feed1 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t2feed0 empty') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED, ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t2feed1 empty') - }) - - it('requires non-zero oracleTimeouts', async () => { - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - oracleTimeouts: [[bn('0')], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t0timeout0 zero') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - oracleTimeouts: [[USDC_ORACLE_TIMEOUT], [bn('0')], [USDT_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t1timeout0 zero') - await expect( - deployCollateral({ - erc20: THREE_POOL_TOKEN, // can be anything. - oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [bn('0')]], - }) - ).to.be.revertedWith('t2timeout0 zero') - }) - - it('requires non-zero oracleErrors', async () => { - await expect( - deployCollateral({ - oracleErrors: [[fp('1')], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t0error0 too large') - await expect( - deployCollateral({ - oracleErrors: [[USDC_ORACLE_ERROR], [fp('1')], [USDT_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t1error0 too large') - await expect( - deployCollateral({ oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [fp('1')]] }) - ).to.be.revertedWith('t2error0 too large') - }) -} - -/* - Run the test suite -*/ - -const describeFork = useEnv('FORK') ? describe : describe.skip - -describeFork(`Collateral: Convex - Stable (3Pool)`, () => { - before(resetFork) - describe('constructor validation', () => { - it('validates targetName', async () => { - await expect(deployCollateral({ targetName: ethers.constants.HashZero })).to.be.revertedWith( - 'targetName missing' - ) - }) - - it('does not allow missing ERC20', async () => { - await expect(deployCollateral({ erc20: ethers.constants.AddressZero })).to.be.revertedWith( - 'missing erc20' - ) - }) - - it('does not allow missing chainlink feed', async () => { - await expect( - deployCollateral({ chainlinkFeed: ethers.constants.AddressZero }) - ).to.be.revertedWith('missing chainlink feed') - }) - - it('max trade volume must be greater than zero', async () => { - await expect(deployCollateral({ maxTradeVolume: 0 })).to.be.revertedWith( - 'invalid max trade volume' - ) - }) - - it('does not allow oracle timeout at 0', async () => { - await expect(deployCollateral({ oracleTimeout: 0 })).to.be.revertedWith('oracleTimeout zero') - }) - - it('does not allow missing delayUntilDefault if defaultThreshold > 0', async () => { - await expect(deployCollateral({ delayUntilDefault: 0 })).to.be.revertedWith( - 'delayUntilDefault zero' - ) - }) - - describe('collateral-specific tests', collateralSpecificConstructorTests) - }) - - describe('collateral functionality', () => { - let ctx: CvxStableCollateralFixtureContext - let alice: SignerWithAddress - - let wallet: SignerWithAddress - let chainId: number - - let collateral: TestICollateral - let chainlinkFeed: MockV3Aggregator - let usdcFeed: MockV3Aggregator - let daiFeed: MockV3Aggregator - let usdtFeed: MockV3Aggregator - - let crv: ERC20Mock - let cvx: ERC20Mock - - before(async () => { - ;[wallet] = (await ethers.getSigners()) as unknown as SignerWithAddress[] - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - await resetFork() - }) - - beforeEach(async () => { - ;[, alice] = await ethers.getSigners() - ctx = await loadFixture(makeCollateralFixtureContext(alice, {})) - ;({ chainlinkFeed, collateral, usdcFeed, daiFeed, usdtFeed, crv, cvx } = ctx) - - await mintCollateralTo(ctx, bn('100e18'), wallet, wallet.address) - }) - - describe('functions', () => { - it('returns the correct bal (18 decimals)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - const aliceBal = await collateral.bal(alice.address) - expect(aliceBal).to.closeTo( - amount.mul(bn(10).pow(18 - (await ctx.tok.decimals()))), - bn('100').mul(bn(10).pow(18 - (await ctx.tok.decimals()))) - ) - }) - }) - - describe('rewards', () => { - it('does not revert', async () => { - await expect(collateral.claimRewards()).to.not.be.reverted - }) - - it('claims rewards (plugin)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, collateral.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(collateral.address) - const cvxBefore = await cvx.balanceOf(collateral.address) - await expect(collateral.claimRewards()).to.emit(ctx.w3Pool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(collateral.address) - const cvxAfter = await cvx.balanceOf(collateral.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - - it('claims rewards (wrapper)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(alice.address) - const cvxBefore = await cvx.balanceOf(alice.address) - await expect(ctx.w3Pool.connect(alice).claimRewards()).to.emit(ctx.w3Pool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(alice.address) - const cvxAfter = await cvx.balanceOf(alice.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - }) - - describe('prices', () => { - before(resetFork) - it('prices change as feed price changes', async () => { - const feedData = await usdcFeed.latestRoundData() - const initialRefPerTok = await collateral.refPerTok() - - const [low, high] = await collateral.price() - - // Update values in Oracles increase by 10% - const newPrice = feedData.answer.mul(110).div(100) - - await Promise.all([ - usdcFeed.updateAnswer(newPrice).then((e) => e.wait()), - daiFeed.updateAnswer(newPrice).then((e) => e.wait()), - usdtFeed.updateAnswer(newPrice).then((e) => e.wait()), - ]) - - const [newLow, newHigh] = await collateral.price() - - expect(newLow).to.be.closeTo(low.mul(110).div(100), 1) - expect(newHigh).to.be.closeTo(high.mul(110).div(100), 1) - - // Check refPerTok remains the same (because we have not refreshed) - const finalRefPerTok = await collateral.refPerTok() - expect(finalRefPerTok).to.equal(initialRefPerTok) - }) - - it('prices change as refPerTok changes', async () => { - const initRefPerTok = await collateral.refPerTok() - const [initLow, initHigh] = await collateral.price() - - const curveVirtualPrice = await ctx.curvePool.get_virtual_price() - await ctx.curvePool.setVirtualPrice(curveVirtualPrice.add(1e4)) - await ctx.curvePool.setBalances([ - await ctx.curvePool.balances(0).then((e) => e.add(1e4)), - await ctx.curvePool.balances(1).then((e) => e.add(2e4)), - await ctx.curvePool.balances(2).then((e) => e.add(3e4)), - ]) - - await collateral.refresh() - expect(await collateral.refPerTok()).to.be.gt(initRefPerTok) - - const [newLow, newHigh] = await collateral.price() - expect(newLow).to.be.gt(initLow) - expect(newHigh).to.be.gt(initHigh) - }) - - it('returns a 0 price', async () => { - await Promise.all([ - usdcFeed.updateAnswer(0).then((e) => e.wait()), - daiFeed.updateAnswer(0).then((e) => e.wait()), - usdtFeed.updateAnswer(0).then((e) => e.wait()), - ]) - - // (0, FIX_MAX) is returned - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(0) - - // When refreshed, sets status to Unpriced - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('reverts in case of invalid timestamp', async () => { - await usdcFeed.setInvalidTimestamp() - - // Check price of token - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(MAX_UINT192) - - // When refreshed, sets status to Unpriced - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('decays lotPrice over priceTimeout period', async () => { - // Prices should start out equal - const p = await collateral.price() - let lotP = await collateral.lotPrice() - expect(p.length).to.equal(lotP.length) - expect(p[0]).to.equal(lotP[0]) - expect(p[1]).to.equal(lotP[1]) - - // Should be roughly half, after half of priceTimeout - const priceTimeout = await collateral.priceTimeout() - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand - expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand - - // Should be 0 after full priceTimeout - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) - }) - }) - - describe('status', () => { - it('maintains status in normal situations', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Force updates (with no changes) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - }) - - it('enters IFFY state when reference unit depegs below low threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters IFFY state when reference unit depegs above high threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Raising price by 20% from 1 to 1.2 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('1.2e8')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters DISABLED state when reference unit depegs for too long', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - - // Move time forward past delayUntilDefault - await advanceTime(delayUntilDefault) - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - - // Nothing changes if attempt to refresh after default - const prevWhenDefault: bigint = (await collateral.whenDefault()).toBigInt() - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(prevWhenDefault) - }) - - it('enters DISABLED state when refPerTok() decreases', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - const currentExchangeRate = await ctx.curvePool.get_virtual_price() - await ctx.curvePool.setVirtualPrice(currentExchangeRate.sub(1e3)).then((e) => e.wait()) - - // Collateral defaults due to refPerTok() going down - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('enters IFFY state when price becomes stale', async () => { - const oracleTimeout = DAI_ORACLE_TIMEOUT.toNumber() - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('does revenue hiding correctly', async () => { - ctx = await loadFixture(makeCollateralFixtureContext(alice, { revenueHiding: fp('1e-6') })) - ;({ collateral } = ctx) - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Decrease refPerTok by 1 part in a million - const currentExchangeRate = await ctx.curvePool.get_virtual_price() - const newVirtualPrice = currentExchangeRate.sub(currentExchangeRate.div(bn('1e6'))) - await ctx.curvePool.setVirtualPrice(newVirtualPrice) - - // Collateral remains SOUND - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // One quanta more of decrease results in default - await ctx.curvePool.setVirtualPrice(newVirtualPrice.sub(1)) - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { - const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( - 'InvalidMockV3Aggregator' - ) - const invalidChainlinkFeed = ( - await InvalidMockV3AggregatorFactory.deploy(6, bn('1e6')) - ) - - const fix = await makeW3PoolStable() - - const invalidCollateral = await deployCollateral({ - erc20: fix.w3Pool.address, - feeds: [ - [invalidChainlinkFeed.address], - [invalidChainlinkFeed.address], - [invalidChainlinkFeed.address], - ], - }) - - // Reverting with no reason - await invalidChainlinkFeed.setSimplyRevert(true) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - - // Runnning out of gas (same error) - await invalidChainlinkFeed.setSimplyRevert(false) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - }) - }) - }) -}) diff --git a/test/plugins/individual-collateral/convex/CvxVolatileTestSuite.test.ts b/test/plugins/individual-collateral/convex/CvxVolatileTestSuite.test.ts deleted file mode 100644 index cbbd8780a..000000000 --- a/test/plugins/individual-collateral/convex/CvxVolatileTestSuite.test.ts +++ /dev/null @@ -1,799 +0,0 @@ -import { - CollateralFixtureContext, - CollateralOpts, - CollateralStatus, - MintCollateralFunc, -} from '../pluginTestTypes' -import { mintW3Pool, makeW3PoolVolatile, Wrapped3PoolFixtureVolatile, resetFork } from './helpers' -import hre, { ethers } from 'hardhat' -import { ContractFactory, BigNumberish } from 'ethers' -import { - CvxVolatileCollateral, - ERC20Mock, - InvalidMockV3Aggregator, - MockV3Aggregator, - MockV3Aggregator__factory, - TestICollateral, -} from '../../../../typechain' -import { bn, fp } from '../../../../common/numbers' -import { MAX_UINT192, MAX_UINT48, ZERO_ADDRESS } from '../../../../common/constants' -import { expect } from 'chai' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import { - PRICE_TIMEOUT, - CVX, - USDT_ORACLE_TIMEOUT, - USDT_ORACLE_ERROR, - MAX_TRADE_VOL, - DEFAULT_THRESHOLD, - DELAY_UNTIL_DEFAULT, - CurvePoolType, - CRV, - TRI_CRYPTO_HOLDER, - TRI_CRYPTO, - TRI_CRYPTO_TOKEN, - WBTC_BTC_FEED, - BTC_USD_FEED, - BTC_ORACLE_TIMEOUT, - WETH_USD_FEED, - WBTC_BTC_ORACLE_ERROR, - WBTC_ORACLE_TIMEOUT, - WETH_ORACLE_TIMEOUT, - USDT_USD_FEED, - BTC_USD_ORACLE_ERROR, - WETH_ORACLE_ERROR, -} from './constants' -import { useEnv } from '#/utils/env' -import { getChainId } from '#/common/blockchain-utils' -import { networkConfig } from '#/common/configuration' -import { - advanceBlocks, - advanceTime, - getLatestBlockTimestamp, - setNextBlockTimestamp, -} from '#/test/utils/time' - -type Fixture = () => Promise - -/* - Define interfaces -*/ - -interface CvxVolatileCollateralFixtureContext - extends CollateralFixtureContext, - Wrapped3PoolFixtureVolatile { - wethFeed: MockV3Aggregator - wbtcFeed: MockV3Aggregator - btcFeed: MockV3Aggregator - usdtFeed: MockV3Aggregator - cvx: ERC20Mock - crv: ERC20Mock -} - -// interface CometCollateralFixtureContextMockComet extends CollateralFixtureContext { -// cusdcV3: CometMock -// wcusdcV3: ICusdcV3Wrapper -// usdc: ERC20Mock -// wcusdcV3Mock: CusdcV3WrapperMock -// } - -interface CvxVolatileCollateralOpts extends CollateralOpts { - revenueHiding?: BigNumberish - nTokens?: BigNumberish - curvePool?: string - poolType?: CurvePoolType - feeds?: string[][] - oracleTimeouts?: BigNumberish[][] - oracleErrors?: BigNumberish[][] - lpToken?: string -} - -/* - Define deployment functions -*/ - -export const defaultCvxVolatileCollateralOpts: CvxVolatileCollateralOpts = { - erc20: ZERO_ADDRESS, - targetName: ethers.utils.formatBytes32String('TRICRYPTO'), - priceTimeout: PRICE_TIMEOUT, - chainlinkFeed: USDT_USD_FEED, // unused but cannot be zero - oracleTimeout: bn('1'), // unused but cannot be zero - oracleError: bn('1'), // unused but cannot be zero - maxTradeVolume: MAX_TRADE_VOL, - defaultThreshold: DEFAULT_THRESHOLD, - delayUntilDefault: DELAY_UNTIL_DEFAULT, - revenueHiding: bn('0'), // TODO - nTokens: bn('3'), - curvePool: TRI_CRYPTO, - lpToken: TRI_CRYPTO_TOKEN, - poolType: CurvePoolType.Plain, - feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - oracleTimeouts: [ - [USDT_ORACLE_TIMEOUT], - [WBTC_ORACLE_TIMEOUT, BTC_ORACLE_TIMEOUT], - [WETH_ORACLE_TIMEOUT], - ], - oracleErrors: [ - [USDT_ORACLE_ERROR], - [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], - [WETH_ORACLE_ERROR], - ], -} - -const makeFeeds = async () => { - const MockV3AggregatorFactory = ( - await ethers.getContractFactory('MockV3Aggregator') - ) - - // Substitute all 3 feeds: DAI, USDC, USDT - const wethFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const wbtcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const btcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) - - const wethFeedOrg = MockV3AggregatorFactory.attach(WETH_USD_FEED) - const wbtcFeedOrg = MockV3AggregatorFactory.attach(WBTC_BTC_FEED) - const btcFeedOrg = MockV3AggregatorFactory.attach(BTC_USD_FEED) - const usdtFeedOrg = MockV3AggregatorFactory.attach(USDT_USD_FEED) - - await wethFeed.updateAnswer(await wethFeedOrg.latestAnswer()) - await wbtcFeed.updateAnswer(await wbtcFeedOrg.latestAnswer()) - await btcFeed.updateAnswer(await btcFeedOrg.latestAnswer()) - await usdtFeed.updateAnswer(await usdtFeedOrg.latestAnswer()) - - return { wethFeed, wbtcFeed, btcFeed, usdtFeed } -} - -export const deployCollateral = async ( - opts: CvxVolatileCollateralOpts = {} -): Promise => { - if (!opts.erc20 && !opts.feeds) { - const { wethFeed, wbtcFeed, btcFeed, usdtFeed } = await makeFeeds() - - const fix = await makeW3PoolVolatile() - - opts.feeds = [[wethFeed.address], [wbtcFeed.address, btcFeed.address], [usdtFeed.address]] - opts.erc20 = fix.w3Pool.address - } - - opts = { ...defaultCvxVolatileCollateralOpts, ...opts } - - const CvxVolatileCollateralFactory: ContractFactory = await ethers.getContractFactory( - 'CvxVolatileCollateral' - ) - - const collateral = await CvxVolatileCollateralFactory.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, - { - nTokens: opts.nTokens, - curvePool: opts.curvePool, - poolType: opts.poolType, - feeds: opts.feeds, - oracleTimeouts: opts.oracleTimeouts, - oracleErrors: opts.oracleErrors, - lpToken: opts.lpToken, - } - ) - await collateral.deployed() - - return collateral -} - -const makeCollateralFixtureContext = ( - alice: SignerWithAddress, - opts: CvxVolatileCollateralOpts = {} -): Fixture => { - const collateralOpts = { ...defaultCvxVolatileCollateralOpts, ...opts } - - const makeCollateralFixtureContext = async () => { - const { wethFeed, wbtcFeed, btcFeed, usdtFeed } = await makeFeeds() - - collateralOpts.feeds = [ - [usdtFeed.address], - [wbtcFeed.address, btcFeed.address], - [wethFeed.address], - ] - - const fix = await makeW3PoolVolatile() - - collateralOpts.erc20 = fix.w3Pool.address - collateralOpts.curvePool = fix.curvePool.address - const collateral = ((await deployCollateral(collateralOpts)) as unknown) - const rewardToken = await ethers.getContractAt('ERC20Mock', CVX) // use CVX - - const cvx = await ethers.getContractAt('ERC20Mock', CVX) - const crv = await ethers.getContractAt('ERC20Mock', CRV) - - return { - alice, - collateral, - chainlinkFeed: usdtFeed, - curvePool: fix.curvePool, - crv3Pool: fix.crv3Pool, - w3Pool: fix.w3Pool, - usdt: fix.usdt, - wbtc: fix.wbtc, - weth: fix.weth, - tok: fix.w3Pool, - rewardToken, - wbtcFeed, - btcFeed, - wethFeed, - usdtFeed, - cvx, - crv, - } - } - - return makeCollateralFixtureContext -} - -/* - Define helper functions -*/ - -const mintCollateralTo: MintCollateralFunc = async ( - ctx: CvxVolatileCollateralFixtureContext, - amount: BigNumberish, - user: SignerWithAddress, - recipient: string -) => { - await mintW3Pool(ctx, amount, user, recipient, TRI_CRYPTO_HOLDER) -} - -/* - Define collateral-specific tests -*/ - -const collateralSpecificConstructorTests = () => { - it('does not allow 0 defaultThreshold', async () => { - await expect(deployCollateral({ defaultThreshold: bn('0') })).to.be.revertedWith( - 'defaultThreshold zero' - ) - }) - - it('does not allow more than 4 tokens', async () => { - await expect(deployCollateral({ nTokens: 5 })).to.be.revertedWith('up to 4 tokens max') - }) - - it('does not allow empty curvePool', async () => { - await expect(deployCollateral({ curvePool: ZERO_ADDRESS })).to.be.revertedWith( - 'curvePool address is zero' - ) - }) - - it('does not allow more than 2 price feeds', async () => { - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[WETH_USD_FEED, WBTC_BTC_FEED, WETH_USD_FEED], [], []], - }) - ).to.be.revertedWith('price feeds limited to 2') - }) - - it('requires at least 1 price feed per token', async () => { - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[WETH_USD_FEED], [WETH_USD_FEED], []], - }) - ).to.be.revertedWith('each token needs at least 1 price feed') - }) - - it('requires non-zero-address feeds', async () => { - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[ZERO_ADDRESS], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - }) - ).to.be.revertedWith('t0feed0 empty') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[WETH_USD_FEED, ZERO_ADDRESS], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], - }) - ).to.be.revertedWith('t0feed1 empty') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[USDT_USD_FEED], [ZERO_ADDRESS], [WETH_USD_FEED]], - }) - ).to.be.revertedWith('t1feed0 empty') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[USDT_USD_FEED], [USDT_USD_FEED, ZERO_ADDRESS], [WETH_USD_FEED]], - }) - ).to.be.revertedWith('t1feed1 empty') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[USDT_USD_FEED], [USDT_USD_FEED], [ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t2feed0 empty') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - feeds: [[USDT_USD_FEED], [USDT_USD_FEED], [WETH_USD_FEED, ZERO_ADDRESS]], - }) - ).to.be.revertedWith('t2feed1 empty') - }) - - it('requires non-zero oracleTimeouts', async () => { - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - oracleTimeouts: [[bn('0')], [USDT_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t0timeout0 zero') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - oracleTimeouts: [[USDT_ORACLE_TIMEOUT], [bn('0')], [USDT_ORACLE_TIMEOUT]], - }) - ).to.be.revertedWith('t1timeout0 zero') - await expect( - deployCollateral({ - erc20: TRI_CRYPTO_TOKEN, // can be anything. - oracleTimeouts: [ - [USDT_ORACLE_TIMEOUT], - [USDT_ORACLE_TIMEOUT, USDT_ORACLE_TIMEOUT], - [bn('0')], - ], - }) - ).to.be.revertedWith('t2timeout0 zero') - }) - - it('requires non-zero oracleErrors', async () => { - await expect( - deployCollateral({ - oracleErrors: [[fp('1')], [USDT_ORACLE_ERROR], [USDT_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t0error0 too large') - await expect( - deployCollateral({ - oracleErrors: [[USDT_ORACLE_ERROR], [fp('1')], [USDT_ORACLE_ERROR]], - }) - ).to.be.revertedWith('t1error0 too large') - await expect( - deployCollateral({ oracleErrors: [[USDT_ORACLE_ERROR], [USDT_ORACLE_ERROR], [fp('1')]] }) - ).to.be.revertedWith('t2error0 too large') - }) -} - -/* - Run the test suite -*/ - -const describeFork = useEnv('FORK') ? describe : describe.skip - -describeFork(`Collateral: Convex - Volatile`, () => { - before(resetFork) - - describe('constructor validation', () => { - it('validates targetName', async () => { - await expect(deployCollateral({ targetName: ethers.constants.HashZero })).to.be.revertedWith( - 'targetName missing' - ) - }) - - it('does not allow missing ERC20', async () => { - await expect(deployCollateral({ erc20: ethers.constants.AddressZero })).to.be.revertedWith( - 'missing erc20' - ) - }) - - it('does not allow missing chainlink feed', async () => { - await expect( - deployCollateral({ chainlinkFeed: ethers.constants.AddressZero }) - ).to.be.revertedWith('missing chainlink feed') - }) - - it('max trade volume must be greater than zero', async () => { - await expect(deployCollateral({ maxTradeVolume: 0 })).to.be.revertedWith( - 'invalid max trade volume' - ) - }) - - it('does not allow oracle timeout at 0', async () => { - await expect(deployCollateral({ oracleTimeout: 0 })).to.be.revertedWith('oracleTimeout zero') - }) - - it('does not allow missing delayUntilDefault if defaultThreshold > 0', async () => { - await expect(deployCollateral({ delayUntilDefault: 0 })).to.be.revertedWith( - 'delayUntilDefault zero' - ) - }) - - describe('collateral-specific tests', collateralSpecificConstructorTests) - }) - - describe('collateral functionality', () => { - let ctx: CvxVolatileCollateralFixtureContext - let alice: SignerWithAddress - - let wallet: SignerWithAddress - let chainId: number - - let collateral: TestICollateral - let chainlinkFeed: MockV3Aggregator - let wbtcFeed: MockV3Aggregator - let btcFeed: MockV3Aggregator - let wethFeed: MockV3Aggregator - let usdtFeed: MockV3Aggregator - - let crv: ERC20Mock - let cvx: ERC20Mock - - before(async () => { - ;[wallet] = (await ethers.getSigners()) as unknown as SignerWithAddress[] - - chainId = await getChainId(hre) - if (!networkConfig[chainId]) { - throw new Error(`Missing network configuration for ${hre.network.name}`) - } - }) - - beforeEach(async () => { - ;[, alice] = await ethers.getSigners() - ctx = await loadFixture(makeCollateralFixtureContext(alice, {})) - ;({ chainlinkFeed, collateral, wbtcFeed, btcFeed, wethFeed, usdtFeed, crv, cvx } = ctx) - - await mintCollateralTo(ctx, bn('100e18'), wallet, wallet.address) - }) - - describe('functions', () => { - it('returns the correct bal (18 decimals)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - const aliceBal = await collateral.bal(alice.address) - expect(aliceBal).to.closeTo( - amount.mul(bn(10).pow(18 - (await ctx.tok.decimals()))), - bn('100').mul(bn(10).pow(18 - (await ctx.tok.decimals()))) - ) - }) - }) - - describe('rewards', () => { - it('does not revert', async () => { - await expect(collateral.claimRewards()).to.not.be.reverted - }) - - it('claims rewards (plugin)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, collateral.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(collateral.address) - const cvxBefore = await cvx.balanceOf(collateral.address) - await expect(collateral.claimRewards()).to.emit(ctx.w3Pool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(collateral.address) - const cvxAfter = await cvx.balanceOf(collateral.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - - it('claims rewards (wrapper)', async () => { - const amount = bn('20000').mul(bn(10).pow(await ctx.tok.decimals())) - await mintCollateralTo(ctx, amount, alice, alice.address) - - await advanceBlocks(1000) - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) - - const crvBefore = await crv.balanceOf(alice.address) - const cvxBefore = await cvx.balanceOf(alice.address) - await expect(ctx.w3Pool.connect(alice).claimRewards()).to.emit(ctx.w3Pool, 'RewardsClaimed') - const crvAfter = await crv.balanceOf(alice.address) - const cvxAfter = await cvx.balanceOf(alice.address) - expect(crvAfter).gt(crvBefore) - expect(cvxAfter).gt(cvxBefore) - }) - }) - - describe('prices', () => { - it('prices change as feed price changes', async () => { - await collateral.refresh() - - const initialRefPerTok = await collateral.refPerTok() - const [low, high] = await collateral.price() - - await Promise.all([ - btcFeed - .updateAnswer(await btcFeed.latestRoundData().then((e) => e.answer.mul(110).div(100))) - .then((e) => e.wait()), - wethFeed - .updateAnswer(await wethFeed.latestRoundData().then((e) => e.answer.mul(110).div(100))) - .then((e) => e.wait()), - usdtFeed - .updateAnswer(await usdtFeed.latestRoundData().then((e) => e.answer.mul(110).div(100))) - .then((e) => e.wait()), - ]) - - const [newLow, newHigh] = await collateral.price() - const expectedNewLow = low.mul(110).div(100) - const expectedNewHigh = high.mul(110).div(100) - - // Expect price to be correct within 1 part in 100 million - // The rounding comes from the oracle .mul(110).div(100) calculations truncating - expect(newLow).to.be.closeTo(expectedNewLow, expectedNewLow.div(bn('1e8'))) - expect(newHigh).to.be.closeTo(expectedNewHigh, expectedNewHigh.div(bn('1e8'))) - expect(newLow).to.be.lt(expectedNewLow) - expect(newHigh).to.be.lt(expectedNewHigh) - - // Check refPerTok remains the same (because we have not refreshed) - const finalRefPerTok = await collateral.refPerTok() - expect(finalRefPerTok).to.equal(initialRefPerTok) - }) - - it('prices change as refPerTok changes', async () => { - await collateral.refresh() - - const initRefPerTok = await collateral.refPerTok() - const [initLow, initHigh] = await collateral.price() - - const curveVirtualPrice = await ctx.curvePool.get_virtual_price() - await ctx.curvePool.setVirtualPrice(curveVirtualPrice.add(1e4)) - await ctx.curvePool.setBalances([ - await ctx.curvePool.balances(0).then((e) => e.add(1e4)), - await ctx.curvePool.balances(1).then((e) => e.add(2e4)), - await ctx.curvePool.balances(2).then((e) => e.add(3e4)), - ]) - - await collateral.refresh() - expect(await collateral.refPerTok()).to.be.gt(initRefPerTok) - - const [newLow, newHigh] = await collateral.price() - expect(newLow).to.be.gt(initLow) - expect(newHigh).to.be.gt(initHigh) - }) - - it('returns a 0 price', async () => { - await collateral.refresh() - - await Promise.all([ - wbtcFeed.updateAnswer(0).then((e) => e.wait()), - wethFeed.updateAnswer(0).then((e) => e.wait()), - usdtFeed.updateAnswer(0).then((e) => e.wait()), - ]) - - // (0, FIX_MAX) is returned - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(0) - - // When refreshed, sets status to Unpriced - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('reverts in case of invalid timestamp', async () => { - await wbtcFeed.setInvalidTimestamp() - - // Check price of token - const [low, high] = await collateral.price() - expect(low).to.equal(0) - expect(high).to.equal(MAX_UINT192) - - // When refreshed, sets status to Unpriced - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('decays lotPrice over priceTimeout period', async () => { - // Prices should start out equal - await collateral.refresh() - const p = await collateral.price() - let lotP = await collateral.lotPrice() - expect(p.length).to.equal(lotP.length) - expect(p[0]).to.equal(lotP[0]) - expect(p[1]).to.equal(lotP[1]) - - // Should be roughly half, after half of priceTimeout - const priceTimeout = await collateral.priceTimeout() - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand - expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand - - // Should be 0 after full priceTimeout - await advanceTime(priceTimeout / 2) - lotP = await collateral.lotPrice() - expect(lotP[0]).to.equal(0) - expect(lotP[1]).to.equal(0) - }) - }) - - describe('status', () => { - it('maintains status in normal situations', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Force updates (with no changes) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - }) - - it('enters IFFY state when reference unit depegs below low threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters IFFY state when reference unit depegs above high threshold', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Raising price by 20% from 1 to 1.2 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('1.2e8')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault - - await expect(collateral.refresh()) - .to.emit(collateral, 'CollateralStatusChanged') - .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - expect(await collateral.whenDefault()).to.equal(expectedDefaultTimestamp) - }) - - it('enters DISABLED state when reference unit depegs for too long', async () => { - const delayUntilDefault = await collateral.delayUntilDefault() - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Depeg USDC:USD - Reducing price by 20% from 1 to 0.8 - const updateAnswerTx = await chainlinkFeed.updateAnswer(bn('8e7')) - await updateAnswerTx.wait() - - // Set next block timestamp - for deterministic result - const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 - await setNextBlockTimestamp(nextBlockTimestamp) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - - // Move time forward past delayUntilDefault - await advanceTime(delayUntilDefault) - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - - // Nothing changes if attempt to refresh after default - const prevWhenDefault: bigint = (await collateral.whenDefault()).toBigInt() - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(prevWhenDefault) - }) - - it('enters DISABLED state when refPerTok() decreases', async () => { - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - const currentExchangeRate = await ctx.curvePool.get_virtual_price() - await ctx.curvePool.setVirtualPrice(currentExchangeRate.sub(1e3)).then((e) => e.wait()) - - // Collateral defaults due to refPerTok() going down - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('enters IFFY state when price becomes stale', async () => { - const oracleTimeout = USDT_ORACLE_TIMEOUT.toNumber() - await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout) - await collateral.refresh() - expect(await collateral.status()).to.equal(CollateralStatus.IFFY) - }) - - it('does revenue hiding correctly', async () => { - ctx = await loadFixture(makeCollateralFixtureContext(alice, { revenueHiding: fp('1e-6') })) - ;({ collateral } = ctx) - - // Check initial state - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - await mintCollateralTo(ctx, bn('20000e6'), alice, alice.address) - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - - // State remains the same - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Decrease refPerTok by 1 part in a million - const currentExchangeRate = await ctx.curvePool.get_virtual_price() - const newVirtualPrice = currentExchangeRate.sub(currentExchangeRate.div(bn('1e6'))) - await ctx.curvePool.setVirtualPrice(newVirtualPrice) - - // Collateral remains SOUND - await expect(collateral.refresh()).to.not.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.SOUND) - expect(await collateral.whenDefault()).to.equal(MAX_UINT48) - - // Two more quanta of decrease results in default - await ctx.curvePool.setVirtualPrice(newVirtualPrice.sub(2)) - await expect(collateral.refresh()).to.emit(collateral, 'CollateralStatusChanged') - expect(await collateral.status()).to.equal(CollateralStatus.DISABLED) - expect(await collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) - }) - - it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { - const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( - 'InvalidMockV3Aggregator' - ) - const invalidChainlinkFeed = ( - await InvalidMockV3AggregatorFactory.deploy(6, bn('1e6')) - ) - - const fix = await makeW3PoolVolatile() - - const invalidCollateral = await deployCollateral({ - erc20: fix.w3Pool.address, - feeds: [ - [invalidChainlinkFeed.address], - [invalidChainlinkFeed.address], - [invalidChainlinkFeed.address], - ], - }) - - // Reverting with no reason - await invalidChainlinkFeed.setSimplyRevert(true) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - - // Runnning out of gas (same error) - await invalidChainlinkFeed.setSimplyRevert(false) - await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() - expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) - }) - }) - }) -}) diff --git a/test/plugins/individual-collateral/curve/collateralTests.ts b/test/plugins/individual-collateral/curve/collateralTests.ts new file mode 100644 index 000000000..eeff7bdf4 --- /dev/null +++ b/test/plugins/individual-collateral/curve/collateralTests.ts @@ -0,0 +1,639 @@ +import { + CurveCollateralFixtureContext, + CurveCollateralOpts, + CurveCollateralTestSuiteFixtures, +} from './pluginTestTypes' +import { CollateralStatus } from '../pluginTestTypes' +import { ethers } from 'hardhat' +import { ERC20Mock, InvalidMockV3Aggregator } from '../../../../typechain' +import { BigNumber } from 'ethers' +import { bn, fp } from '../../../../common/numbers' +import { MAX_UINT48, ZERO_ADDRESS, ONE_ADDRESS } from '../../../../common/constants' +import { expect } from 'chai' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { useEnv } from '#/utils/env' +import { expectUnpriced } from '../../../utils/oracles' +import { + advanceBlocks, + advanceTime, + getLatestBlockTimestamp, + setNextBlockTimestamp, +} from '#/test/utils/time' + +const describeFork = useEnv('FORK') ? describe : describe.skip + +export default function fn( + fixtures: CurveCollateralTestSuiteFixtures +) { + const { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + isMetapool, + resetFork, + collateralName, + } = fixtures + + describeFork(`Collateral: ${collateralName}`, () => { + let defaultOpts: CurveCollateralOpts + let mockERC20: ERC20Mock + + before(async () => { + ;[, defaultOpts] = await deployCollateral({}) + const ERC20Factory = await ethers.getContractFactory('ERC20Mock') + mockERC20 = await ERC20Factory.deploy('Mock ERC20', 'ERC20') + }) + + describe('constructor validation', () => { + it('does not allow 0 defaultThreshold', async () => { + await expect(deployCollateral({ defaultThreshold: bn('0') })).to.be.revertedWith( + 'defaultThreshold zero' + ) + }) + + it('does not allow more than 4 tokens', async () => { + await expect(deployCollateral({ nTokens: 5 })).to.be.revertedWith('up to 4 tokens max') + }) + + it('does not allow empty curvePool', async () => { + await expect(deployCollateral({ curvePool: ZERO_ADDRESS })).to.be.revertedWith( + 'curvePool address is zero' + ) + }) + + it('does not allow invalid Pool Type', async () => { + await expect(deployCollateral({ poolType: 1 })).to.be.revertedWith('invalid poolType') + }) + + it('does not allow more than 2 price feeds', async () => { + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds: [[ONE_ADDRESS, ONE_ADDRESS, ONE_ADDRESS], [], []], + }) + ).to.be.revertedWith('price feeds limited to 2') + }) + + it('supports up to 2 price feeds per token', async () => { + const nonzeroError = fp('0.01') // 1% + const nTokens = defaultOpts.nTokens || 0 + + const feeds: string[][] = [] + for (let i = 0; i < nTokens; i++) { + feeds.push([ONE_ADDRESS, ONE_ADDRESS]) + } + + const oracleTimeouts: BigNumber[][] = [] + for (let i = 0; i < nTokens; i++) { + oracleTimeouts.push([bn('1'), bn('1')]) + } + + const oracleErrors: BigNumber[][] = [] + for (let i = 0; i < nTokens; i++) { + oracleErrors.push([nonzeroError, bn(0)]) + } + + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds, + oracleTimeouts, + oracleErrors, + }) + ).to.not.be.reverted + }) + + it('requires at least 1 price feed per token', async () => { + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds: [[ONE_ADDRESS, ONE_ADDRESS], [ONE_ADDRESS], []], + }) + ).to.be.revertedWith('each token needs at least 1 price feed') + + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds: [[], [ONE_ADDRESS, ONE_ADDRESS], [ONE_ADDRESS]], + }) + ).to.be.revertedWith('each token needs at least 1 price feed') + + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds: [[ONE_ADDRESS], [], [ONE_ADDRESS, ONE_ADDRESS]], + }) + ).to.be.revertedWith('each token needs at least 1 price feed') + }) + + it('requires non-zero-address feeds', async () => { + const nonzeroTimeout = bn(defaultOpts.oracleTimeouts![0][0]) + const nonzeroError = bn(defaultOpts.oracleErrors![0][0]) + + // Complete all possible feeds + const allFeeds: string[][] = [] + const allOracleTimeouts: BigNumber[][] = [] + const allOracleErrors: BigNumber[][] = [] + + for (let i = 0; i < defaultOpts.nTokens!; i++) { + allFeeds[i] = [ONE_ADDRESS, ONE_ADDRESS] + allOracleTimeouts[i] = [nonzeroTimeout, nonzeroTimeout] + allOracleErrors[i] = [nonzeroError, nonzeroError] + } + + for (let i = 0; i < allFeeds.length; i++) { + for (let j = 0; j < allFeeds[i].length; j++) { + const feeds = allFeeds.map((f) => f.map(() => ONE_ADDRESS)) + feeds[i][j] = ZERO_ADDRESS + + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds, + oracleTimeouts: allOracleTimeouts, + oracleErrors: allOracleErrors, + }) + ).to.be.revertedWith(`t${i}feed${j} empty`) + } + } + }) + + it('requires non-zero oracleTimeouts', async () => { + const nonzeroError = bn(defaultOpts.oracleErrors![0][0]) + + // Complete all possible feeds + const allFeeds: string[][] = [] + const allOracleTimeouts: BigNumber[][] = [] + const allOracleErrors: BigNumber[][] = [] + + for (let i = 0; i < defaultOpts.nTokens!; i++) { + allFeeds[i] = [ONE_ADDRESS, ONE_ADDRESS] + allOracleTimeouts[i] = [bn('1'), bn('1')] + allOracleErrors[i] = [nonzeroError, nonzeroError] + } + + for (let i = 0; i < allFeeds.length; i++) { + for (let j = 0; j < allFeeds[i].length; j++) { + const oracleTimeouts = allOracleTimeouts.map((f) => f.map(() => bn('1'))) + oracleTimeouts[i][j] = bn('0') + + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds: allFeeds, + oracleTimeouts, + oracleErrors: allOracleErrors, + }) + ).to.be.revertedWith(`t${i}timeout${j} zero`) + } + } + }) + + it('requires non-large oracleErrors', async () => { + const nonlargeError = fp('0.01') // 1% + + // Complete all possible feeds + const allFeeds: string[][] = [] + const allOracleTimeouts: BigNumber[][] = [] + const allOracleErrors: BigNumber[][] = [] + + for (let i = 0; i < defaultOpts.nTokens!; i++) { + allFeeds[i] = [ONE_ADDRESS, ONE_ADDRESS] + allOracleTimeouts[i] = [bn('1'), bn('1')] + allOracleErrors[i] = [nonlargeError, nonlargeError] + } + + for (let i = 0; i < allFeeds.length; i++) { + for (let j = 0; j < allFeeds[i].length; j++) { + const oracleErrors = allOracleErrors.map((f) => f.map(() => nonlargeError)) + oracleErrors[i][j] = fp('1') + + await expect( + deployCollateral({ + erc20: mockERC20.address, // can be anything. + feeds: allFeeds, + oracleTimeouts: allOracleTimeouts, + oracleErrors, + }) + ).to.be.revertedWith(`t${i}error${j} too large`) + } + } + }) + + it('validates targetName', async () => { + await expect( + deployCollateral({ targetName: ethers.constants.HashZero }) + ).to.be.revertedWith('targetName missing') + }) + + it('does not allow missing ERC20', async () => { + await expect(deployCollateral({ erc20: ethers.constants.AddressZero })).to.be.revertedWith( + 'missing erc20' + ) + }) + + it('does not allow missing chainlink feed', async () => { + await expect( + deployCollateral({ chainlinkFeed: ethers.constants.AddressZero }) + ).to.be.revertedWith('missing chainlink feed') + }) + + it('max trade volume must be greater than zero', async () => { + await expect(deployCollateral({ maxTradeVolume: 0 })).to.be.revertedWith( + 'invalid max trade volume' + ) + }) + + it('does not allow oracle timeout at 0', async () => { + await expect(deployCollateral({ oracleTimeout: 0 })).to.be.revertedWith( + 'oracleTimeout zero' + ) + }) + + it('does not allow missing delayUntilDefault if defaultThreshold > 0', async () => { + await expect(deployCollateral({ delayUntilDefault: 0 })).to.be.revertedWith( + 'delayUntilDefault zero' + ) + }) + describe('collateral-specific constructor tests', collateralSpecificConstructorTests) + }) + + describe('collateral functionality', () => { + before(resetFork) + + let ctx: CurveCollateralFixtureContext + + beforeEach(async () => { + const [alice] = await ethers.getSigners() + ctx = await loadFixture(makeCollateralFixtureContext(alice, {})) + }) + + describe('functions', () => { + it('returns the correct bal (18 decimals)', async () => { + const amount = bn('20000').mul(bn(10).pow(await ctx.wrapper.decimals())) + await mintCollateralTo(ctx, amount, ctx.alice, ctx.alice.address) + + const aliceBal = await ctx.collateral.bal(ctx.alice.address) + expect(aliceBal).to.closeTo( + amount.mul(bn(10).pow(18 - (await ctx.wrapper.decimals()))), + bn('100').mul(bn(10).pow(18 - (await ctx.wrapper.decimals()))) + ) + }) + }) + + describe('rewards', () => { + it('does not revert', async () => { + await expect(ctx.collateral.claimRewards()).to.not.be.reverted + }) + + it('claims rewards (plugin)', async () => { + const amount = bn('20000').mul(bn(10).pow(await ctx.wrapper.decimals())) + await mintCollateralTo(ctx, amount, ctx.alice, ctx.collateral.address) + + await advanceBlocks(1000) + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) + + const before = await Promise.all( + ctx.rewardTokens.map((t) => t.balanceOf(ctx.wrapper.address)) + ) + await expect(ctx.wrapper.claimRewards()).to.emit(ctx.wrapper, 'RewardsClaimed') + const after = await Promise.all( + ctx.rewardTokens.map((t) => t.balanceOf(ctx.wrapper.address)) + ) + + // Each reward token should have grew + for (let i = 0; i < ctx.rewardTokens.length; i++) { + expect(after[i]).gt(before[i]) + } + }) + + it('claims rewards (wrapper)', async () => { + const amount = bn('20000').mul(bn(10).pow(await ctx.wrapper.decimals())) + await mintCollateralTo(ctx, amount, ctx.alice, ctx.alice.address) + + await advanceBlocks(1000) + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + 12000) + + const before = await Promise.all( + ctx.rewardTokens.map((t) => t.balanceOf(ctx.alice.address)) + ) + await expect(ctx.wrapper.connect(ctx.alice).claimRewards()).to.emit( + ctx.wrapper, + 'RewardsClaimed' + ) + const after = await Promise.all( + ctx.rewardTokens.map((t) => t.balanceOf(ctx.alice.address)) + ) + + // Each reward token should have grew + for (let i = 0; i < ctx.rewardTokens.length; i++) { + expect(after[i]).gt(before[i]) + } + }) + }) + + describe('prices', () => { + before(resetFork) + it('prices change as feed price changes', async () => { + const initialRefPerTok = await ctx.collateral.refPerTok() + const [low, high] = await ctx.collateral.price() + + // Update values in Oracles increase by 10% + const initialPrices = await Promise.all(ctx.feeds.map((f) => f.latestRoundData())) + for (const [i, feed] of ctx.feeds.entries()) { + await feed.updateAnswer(initialPrices[i].answer.mul(110).div(100)).then((e) => e.wait()) + } + + const [newLow, newHigh] = await ctx.collateral.price() + + // with 18 decimals of price precision a 1e-9 tolerance seems fine for a 10% change + // and without this kind of tolerance the Volatile pool tests fail due to small movements + expect(newLow).to.be.closeTo(low.mul(110).div(100), fp('1e-9')) + expect(newHigh).to.be.closeTo(high.mul(110).div(100), fp('1e-9')) + + // Check refPerTok remains the same (because we have not refreshed) + const finalRefPerTok = await ctx.collateral.refPerTok() + expect(finalRefPerTok).to.equal(initialRefPerTok) + }) + + it('prices change as refPerTok changes', async () => { + const initRefPerTok = await ctx.collateral.refPerTok() + const [initLow, initHigh] = await ctx.collateral.price() + + const curveVirtualPrice = await ctx.curvePool.get_virtual_price() + await ctx.collateral.refresh() + expect(await ctx.collateral.refPerTok()).to.equal(curveVirtualPrice) + + await ctx.curvePool.setVirtualPrice(curveVirtualPrice.add(1e4)) + + const newBalances = [ + await ctx.curvePool.balances(0).then((e) => e.add(1e4)), + await ctx.curvePool.balances(1).then((e) => e.add(2e4)), + ] + if (!isMetapool) { + newBalances.push(await ctx.curvePool.balances(2).then((e) => e.add(3e4))) + } + await ctx.curvePool.setBalances(newBalances) + + await ctx.collateral.refresh() + expect(await ctx.collateral.refPerTok()).to.be.gt(initRefPerTok) + + // if it's a metapool, then price may not be hooked up to the mock + if (!isMetapool) { + const [newLow, newHigh] = await ctx.collateral.price() + expect(newLow).to.be.gt(initLow) + expect(newHigh).to.be.gt(initHigh) + } + }) + + it('returns a 0 price', async () => { + for (const feed of ctx.feeds) { + await feed.updateAnswer(0).then((e) => e.wait()) + } + + // (0, 0) is returned + const [low, high] = await ctx.collateral.price() + expect(low).to.equal(0) + expect(high).to.equal(0) + + // When refreshed, sets status to Unpriced + await ctx.collateral.refresh() + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + }) + + it('does not revert in case of invalid timestamp', async () => { + await ctx.feeds[0].setInvalidTimestamp() + + // When refreshed, sets status to Unpriced + await ctx.collateral.refresh() + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + }) + + it('Handles stale price', async () => { + await advanceTime(await ctx.collateral.priceTimeout()) + + // (0, FIX_MAX) is returned + await expectUnpriced(ctx.collateral.address) + + // Refresh should mark status IFFY + await ctx.collateral.refresh() + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + }) + + it('decays lotPrice over priceTimeout period', async () => { + // Prices should start out equal + const p = await ctx.collateral.price() + let lotP = await ctx.collateral.lotPrice() + expect(p.length).to.equal(lotP.length) + expect(p[0]).to.equal(lotP[0]) + expect(p[1]).to.equal(lotP[1]) + + // Should be roughly half, after half of priceTimeout + const priceTimeout = await ctx.collateral.priceTimeout() + await advanceTime(priceTimeout / 2) + lotP = await ctx.collateral.lotPrice() + expect(lotP[0]).to.be.closeTo(p[0].div(2), p[0].div(2).div(10000)) // 1 part in 10 thousand + expect(lotP[1]).to.be.closeTo(p[1].div(2), p[1].div(2).div(10000)) // 1 part in 10 thousand + + // Should be 0 after full priceTimeout + await advanceTime(priceTimeout / 2) + lotP = await ctx.collateral.lotPrice() + expect(lotP[0]).to.equal(0) + expect(lotP[1]).to.equal(0) + }) + }) + + describe('status', () => { + it('maintains status in normal situations', async () => { + // Check initial state + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + // Force updates (with no changes) + await expect(ctx.collateral.refresh()).to.not.emit( + ctx.collateral, + 'CollateralStatusChanged' + ) + // State remains the same + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + }) + + it('enters IFFY state when reference unit depegs below low threshold', async () => { + const delayUntilDefault = await ctx.collateral.delayUntilDefault() + + // Check initial state + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + // Depeg first feed - Reducing price by 20% from 1 to 0.8 + const updateAnswerTx = await ctx.feeds[0].updateAnswer(bn('8e7')) + await updateAnswerTx.wait() + + // Set next block timestamp - for deterministic result + const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 + await setNextBlockTimestamp(nextBlockTimestamp) + const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault + + await expect(ctx.collateral.refresh()) + .to.emit(ctx.collateral, 'CollateralStatusChanged') + .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + expect(await ctx.collateral.whenDefault()).to.equal(expectedDefaultTimestamp) + }) + + it('enters IFFY state when reference unit depegs above high threshold', async () => { + const delayUntilDefault = await ctx.collateral.delayUntilDefault() + + // Check initial state + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + // Depeg first feed - Raising price by 20% from 1 to 1.2 + const updateAnswerTx = await ctx.feeds[0].updateAnswer(bn('1.2e8')) + await updateAnswerTx.wait() + + // Set next block timestamp - for deterministic result + const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 + await setNextBlockTimestamp(nextBlockTimestamp) + const expectedDefaultTimestamp = nextBlockTimestamp + delayUntilDefault + + await expect(ctx.collateral.refresh()) + .to.emit(ctx.collateral, 'CollateralStatusChanged') + .withArgs(CollateralStatus.SOUND, CollateralStatus.IFFY) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + expect(await ctx.collateral.whenDefault()).to.equal(expectedDefaultTimestamp) + }) + + it('enters DISABLED state when reference unit depegs for too long', async () => { + const delayUntilDefault = await ctx.collateral.delayUntilDefault() + + // Check initial state + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + // Depeg first feed - Reducing price by 20% from 1 to 0.8 + const updateAnswerTx = await ctx.feeds[0].updateAnswer(bn('8e7')) + await updateAnswerTx.wait() + + // Set next block timestamp - for deterministic result + const nextBlockTimestamp = (await getLatestBlockTimestamp()) + 1 + await setNextBlockTimestamp(nextBlockTimestamp) + await ctx.collateral.refresh() + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + + // Move time forward past delayUntilDefault + await advanceTime(delayUntilDefault) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) + + // Nothing changes if attempt to refresh after default + const prevWhenDefault: bigint = (await ctx.collateral.whenDefault()).toBigInt() + await expect(ctx.collateral.refresh()).to.not.emit( + ctx.collateral, + 'CollateralStatusChanged' + ) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) + expect(await ctx.collateral.whenDefault()).to.equal(prevWhenDefault) + }) + + it('enters DISABLED state when refPerTok() decreases', async () => { + // Check initial state + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + await mintCollateralTo(ctx, bn('20000e6'), ctx.alice, ctx.alice.address) + + await expect(ctx.collateral.refresh()).to.not.emit( + ctx.collateral, + 'CollateralStatusChanged' + ) + // State remains the same + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + const currentExchangeRate = await ctx.curvePool.get_virtual_price() + await ctx.curvePool.setVirtualPrice(currentExchangeRate.sub(1e3)).then((e) => e.wait()) + + // Collateral defaults due to refPerTok() going down + await expect(ctx.collateral.refresh()).to.emit(ctx.collateral, 'CollateralStatusChanged') + expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) + expect(await ctx.collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) + }) + + it('enters IFFY state when price becomes stale', async () => { + const oracleTimeout = bn(defaultOpts.oracleTimeouts![0][0]) + await setNextBlockTimestamp((await getLatestBlockTimestamp()) + oracleTimeout.toNumber()) + await ctx.collateral.refresh() + expect(await ctx.collateral.status()).to.equal(CollateralStatus.IFFY) + }) + + it('does revenue hiding correctly', async () => { + ctx = await loadFixture( + makeCollateralFixtureContext(ctx.alice, { revenueHiding: fp('1e-6') }) + ) + + // Check initial state + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + await mintCollateralTo(ctx, bn('20000e6'), ctx.alice, ctx.alice.address) + await expect(ctx.collateral.refresh()).to.not.emit( + ctx.collateral, + 'CollateralStatusChanged' + ) + + // State remains the same + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + // Decrease refPerTok by 1 part in a million + const currentExchangeRate = await ctx.curvePool.get_virtual_price() + const newVirtualPrice = currentExchangeRate.sub(currentExchangeRate.div(bn('1e6'))) + await ctx.curvePool.setVirtualPrice(newVirtualPrice) + + // Collateral remains SOUND + await expect(ctx.collateral.refresh()).to.not.emit( + ctx.collateral, + 'CollateralStatusChanged' + ) + expect(await ctx.collateral.status()).to.equal(CollateralStatus.SOUND) + expect(await ctx.collateral.whenDefault()).to.equal(MAX_UINT48) + + // One quanta more of decrease results in default + await ctx.curvePool.setVirtualPrice(newVirtualPrice.sub(2)) // sub 2 to compenstate for rounding + await expect(ctx.collateral.refresh()).to.emit(ctx.collateral, 'CollateralStatusChanged') + expect(await ctx.collateral.status()).to.equal(CollateralStatus.DISABLED) + expect(await ctx.collateral.whenDefault()).to.equal(await getLatestBlockTimestamp()) + }) + + it('reverts if Chainlink feed reverts or runs out of gas, maintains status', async () => { + const InvalidMockV3AggregatorFactory = await ethers.getContractFactory( + 'InvalidMockV3Aggregator' + ) + const invalidChainlinkFeed = ( + await InvalidMockV3AggregatorFactory.deploy(6, bn('1e6')) + ) + + ctx = await loadFixture(makeCollateralFixtureContext(ctx.alice, {})) + const [invalidCollateral] = await deployCollateral({ + erc20: ctx.wrapper.address, + feeds: defaultOpts.feeds!.map((f) => f.map(() => invalidChainlinkFeed.address)), + }) + + // Reverting with no reason + await invalidChainlinkFeed.setSimplyRevert(true) + await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() + expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) + + // Runnning out of gas (same error) + await invalidChainlinkFeed.setSimplyRevert(false) + await expect(invalidCollateral.refresh()).to.be.revertedWithoutReason() + expect(await invalidCollateral.status()).to.equal(CollateralStatus.SOUND) + }) + + describe('collateral-specific tests', collateralSpecificStatusTests) + }) + }) + }) +} diff --git a/test/plugins/individual-collateral/convex/constants.ts b/test/plugins/individual-collateral/curve/constants.ts similarity index 79% rename from test/plugins/individual-collateral/convex/constants.ts rename to test/plugins/individual-collateral/curve/constants.ts index 98c0df065..9e68a63d9 100644 --- a/test/plugins/individual-collateral/convex/constants.ts +++ b/test/plugins/individual-collateral/curve/constants.ts @@ -18,6 +18,11 @@ export const USDT_USD_FEED = networkConfig['1'].chainlinkFeeds.USDT as string export const USDT_ORACLE_TIMEOUT = bn('86400') export const USDT_ORACLE_ERROR = fp('0.0025') +// SUSD +export const SUSD_USD_FEED = networkConfig['1'].chainlinkFeeds.sUSD as string +export const SUSD_ORACLE_TIMEOUT = bn('86400') +export const SUSD_ORACLE_ERROR = fp('0.0025') + // FRAX export const FRAX_USD_FEED = networkConfig['1'].chainlinkFeeds.FRAX as string export const FRAX_ORACLE_TIMEOUT = bn('3600') @@ -46,6 +51,7 @@ export const MIM_DEFAULT_THRESHOLD = fp('0.055') // 5.5% export const DAI = networkConfig['1'].tokens.DAI as string export const USDC = networkConfig['1'].tokens.USDC as string export const USDT = networkConfig['1'].tokens.USDT as string +export const SUSD = networkConfig['1'].tokens.sUSD as string export const FRAX = networkConfig['1'].tokens.FRAX as string export const MIM = networkConfig['1'].tokens.MIM as string export const eUSD = networkConfig['1'].tokens.eUSD as string @@ -62,12 +68,21 @@ export const THREE_POOL_TOKEN = '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490' export const THREE_POOL_CVX_POOL_ID = 9 export const THREE_POOL_HOLDER = '0xd632f22692fac7611d2aa1c0d552930d43caed3b' export const THREE_POOL_DEFAULT_THRESHOLD = fp('0.0125') // 1.25% +export const THREE_POOL_GAUGE = '0xbfcf63294ad7105dea65aa58f8ae5be2d9d0952a' // tricrypto2 - USDT, WBTC, ETH export const TRI_CRYPTO = '0xd51a44d3fae010294c616388b506acda1bfaae46' export const TRI_CRYPTO_TOKEN = '0xc4ad29ba4b3c580e6d59105fff484999997675ff' export const TRI_CRYPTO_CVX_POOL_ID = 38 export const TRI_CRYPTO_HOLDER = '0xDeFd8FdD20e0f34115C7018CCfb655796F6B2168' +export const TRI_CRYPTO_GAUGE = '0xdefd8fdd20e0f34115c7018ccfb655796f6b2168' + +// SUSD Pool - USDC, USDT, DAI, SUSD (used for tests only) +export const SUSD_POOL = '0xa5407eae9ba41422680e2e00537571bcc53efbfd' +export const SUSD_POOL_TOKEN = '0xC25a3A3b969415c80451098fa907EC722572917F' +export const SUSD_POOL_CVX_POOL_ID = 4 +export const SUSD_POOL_HOLDER = '0xDCB6A51eA3CA5d3Fd898Fd6564757c7aAeC3ca92' +export const SUSD_POOL_DEFAULT_THRESHOLD = fp('0.0125') // 1.25% // fraxBP export const FRAX_BP = '0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2' @@ -77,11 +92,16 @@ export const FRAX_BP_TOKEN = '0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC' export const eUSD_FRAX_BP = '0xAEda92e6A3B1028edc139A4ae56Ec881f3064D4F' export const eUSD_FRAX_BP_POOL_ID = 156 export const eUSD_FRAX_HOLDER = '0x8605dc0C339a2e7e85EEA043bD29d42DA2c6D784' +export const eUSD_GAUGE = '0x8605dc0c339a2e7e85eea043bd29d42da2c6d784' // MIM + 3pool export const MIM_THREE_POOL = '0x5a6A4D54456819380173272A5E8E9B9904BdF41B' export const MIM_THREE_POOL_POOL_ID = 40 export const MIM_THREE_POOL_HOLDER = '0x66C90baCE2B68955C875FdA89Ba2c5A94e672440' +export const MIM_THREE_POOL_GAUGE = '0xd8b712d29381748db89c36bca0138d7c75866ddf' + +// Curve-specific +export const CURVE_MINTER = '0xd061d61a4d941c39e5453435b6345dc261c2fce0' // RTokenMetapool-specific export const RTOKEN_DELAY_UNTIL_DEFAULT = bn('259200') // 72h @@ -99,5 +119,4 @@ export const FORK_BLOCK = 16915576 export enum CurvePoolType { Plain, Lending, - Metapool, } diff --git a/test/plugins/individual-collateral/curve/crv/CrvStableMetapoolSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvStableMetapoolSuite.test.ts new file mode 100644 index 000000000..a7993bb36 --- /dev/null +++ b/test/plugins/individual-collateral/curve/crv/CrvStableMetapoolSuite.test.ts @@ -0,0 +1,229 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + MintCurveCollateralFunc, + CurveMetapoolCollateralOpts, +} from '../pluginTestTypes' +import { makeWMIM3Pool, mintWMIM3Pool, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { BigNumberish } from 'ethers' +import { + CurveStableMetapoolCollateral, + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + THREE_POOL, + THREE_POOL_TOKEN, + THREE_POOL_DEFAULT_THRESHOLD, + DAI_USD_FEED, + DAI_ORACLE_TIMEOUT, + DAI_ORACLE_ERROR, + MIM_DEFAULT_THRESHOLD, + MIM_USD_FEED, + MIM_ORACLE_TIMEOUT, + MIM_ORACLE_ERROR, + MIM_THREE_POOL, + MIM_THREE_POOL_HOLDER, + USDC_USD_FEED, + USDC_ORACLE_TIMEOUT, + USDC_ORACLE_ERROR, + USDT_USD_FEED, + USDT_ORACLE_TIMEOUT, + USDT_ORACLE_ERROR, + MAX_TRADE_VOL, + DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCrvStableCollateralOpts: CurveMetapoolCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: MIM_USD_FEED, + oracleTimeout: MIM_ORACLE_TIMEOUT, + oracleError: MIM_ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: THREE_POOL_DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), // TODO + nTokens: bn('3'), + curvePool: THREE_POOL, + lpToken: THREE_POOL_TOKEN, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], + metapoolToken: MIM_THREE_POOL, + pairedTokenDefaultThreshold: MIM_DEFAULT_THRESHOLD, +} + +export const deployCollateral = async ( + opts: CurveMetapoolCollateralOpts = {} +): Promise<[TestICollateral, CurveMetapoolCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds && !opts.chainlinkFeed) { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const mimFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeWMIM3Pool() + + opts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + opts.erc20 = fix.wPool.address + opts.chainlinkFeed = mimFeed.address + } + + opts = { ...defaultCrvStableCollateralOpts, ...opts } + + const CrvStableCollateralFactory = await ethers.getContractFactory( + 'CurveStableMetapoolCollateral' + ) + + const collateral = await CrvStableCollateralFactory.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!, + { + nTokens: opts.nTokens!, + curvePool: opts.curvePool!, + poolType: opts.poolType!, + feeds: opts.feeds!, + oracleTimeouts: opts.oracleTimeouts!, + oracleErrors: opts.oracleErrors!, + lpToken: opts.lpToken!, + }, + opts.metapoolToken!, + opts.pairedTokenDefaultThreshold! + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral as unknown as TestICollateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveMetapoolCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCrvStableCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const mimFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeWMIM3Pool() + + collateralOpts.erc20 = fix.wPool.address + collateralOpts.chainlinkFeed = mimFeed.address + collateralOpts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + collateralOpts.curvePool = fix.curvePool.address + collateralOpts.metapoolToken = fix.metapoolToken.address + + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.metapoolToken, + wrapper: fix.wPool, + rewardTokens: [crv], + poolTokens: [fix.dai, fix.usdc, fix.usdt], + feeds: [mimFeed, daiFeed, usdcFeed, usdtFeed], + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWMIM3Pool(ctx, amount, user, recipient, MIM_THREE_POOL_HOLDER) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => { + it('does not allow empty metaPoolToken', async () => { + await expect(deployCollateral({ metapoolToken: ZERO_ADDRESS })).to.be.revertedWith( + 'metapoolToken address is zero' + ) + }) + + it('does not allow invalid pairedTokenDefaultThreshold', async () => { + await expect(deployCollateral({ pairedTokenDefaultThreshold: bn(0) })).to.be.revertedWith( + 'pairedTokenDefaultThreshold out of bounds' + ) + + await expect( + deployCollateral({ pairedTokenDefaultThreshold: bn('1.1e18') }) + ).to.be.revertedWith('pairedTokenDefaultThreshold out of bounds') + }) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: true, + resetFork, + collateralName: 'CurveStableMetapoolCollateral - CurveGaugeWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts new file mode 100644 index 000000000..86eed375a --- /dev/null +++ b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts @@ -0,0 +1,219 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + CurveMetapoolCollateralOpts, + MintCurveCollateralFunc, +} from '../pluginTestTypes' +import { makeWeUSDFraxBP, mintWeUSDFraxBP, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS, ONE_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + eUSD_FRAX_BP, + FRAX_BP, + FRAX_BP_TOKEN, + USDC_USD_FEED, + USDC_ORACLE_TIMEOUT, + USDC_ORACLE_ERROR, + FRAX_USD_FEED, + FRAX_ORACLE_TIMEOUT, + FRAX_ORACLE_ERROR, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + RTOKEN_DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, + eUSD_FRAX_HOLDER, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCrvStableCollateralOpts: CurveMetapoolCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), // TODO + nTokens: bn('2'), + curvePool: FRAX_BP, + lpToken: FRAX_BP_TOKEN, + poolType: CurvePoolType.Plain, // for fraxBP, not the top-level pool + feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], + oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], + oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], + metapoolToken: eUSD_FRAX_BP, + pairedTokenDefaultThreshold: DEFAULT_THRESHOLD, +} + +export const deployCollateral = async ( + opts: CurveMetapoolCollateralOpts = {} +): Promise<[TestICollateral, CurveMetapoolCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds) { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: FRAX, USDC, eUSD + const fraxFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const eusdFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeWeUSDFraxBP(eusdFeed) + + opts.feeds = [[fraxFeed.address], [usdcFeed.address]] + opts.erc20 = fix.wPool.address + } + + opts = { ...defaultCrvStableCollateralOpts, ...opts } + + const CrvStableRTokenMetapoolCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveStableRTokenMetapoolCollateral' + ) + + const collateral = await CrvStableRTokenMetapoolCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + }, + opts.metapoolToken, + opts.pairedTokenDefaultThreshold + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral as unknown as TestICollateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveMetapoolCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCrvStableCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all feeds: FRAX, USDC, RToken + const fraxFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const eusdFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const fix = await makeWeUSDFraxBP(eusdFeed) + collateralOpts.feeds = [[fraxFeed.address], [usdcFeed.address]] + + collateralOpts.erc20 = fix.wPool.address + collateralOpts.curvePool = fix.curvePool.address + collateralOpts.metapoolToken = fix.metapoolToken.address + + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.metapoolToken, + wrapper: fix.wPool, + rewardTokens: [crv], + chainlinkFeed: usdcFeed, + poolTokens: [fix.frax, fix.usdc], + feeds: [fraxFeed, usdcFeed, eusdFeed], + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWeUSDFraxBP(ctx, amount, user, recipient, eUSD_FRAX_HOLDER) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => { + it('does not allow empty metaPoolToken', async () => { + await expect(deployCollateral({ metapoolToken: ZERO_ADDRESS })).to.be.revertedWith( + 'metapoolToken address is zero' + ) + }) + + it('does not allow invalid pairedTokenDefaultThreshold', async () => { + await expect(deployCollateral({ pairedTokenDefaultThreshold: bn(0) })).to.be.revertedWith( + 'pairedTokenDefaultThreshold out of bounds' + ) + + await expect( + deployCollateral({ pairedTokenDefaultThreshold: bn('1.1e18') }) + ).to.be.revertedWith('pairedTokenDefaultThreshold out of bounds') + }) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: true, + resetFork, + collateralName: 'CurveStableRTokenMetapoolCollateral - CurveGaugeWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/crv/CrvStableTestSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvStableTestSuite.test.ts new file mode 100644 index 000000000..7cac9495b --- /dev/null +++ b/test/plugins/individual-collateral/curve/crv/CrvStableTestSuite.test.ts @@ -0,0 +1,238 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + CurveCollateralOpts, + MintCurveCollateralFunc, +} from '../pluginTestTypes' +import { mintWPool, makeW3PoolStable, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../../common/constants' +import { whileImpersonating } from '../../../../utils/impersonation' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + THREE_POOL, + THREE_POOL_TOKEN, + DAI_USD_FEED, + DAI_ORACLE_TIMEOUT, + DAI_ORACLE_ERROR, + USDC_USD_FEED, + USDC_ORACLE_TIMEOUT, + USDC_ORACLE_ERROR, + USDT_USD_FEED, + USDT_ORACLE_TIMEOUT, + USDT_ORACLE_ERROR, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, + THREE_POOL_HOLDER, + TRI_CRYPTO_TOKEN, + TRI_CRYPTO_GAUGE, + TRI_CRYPTO_HOLDER, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCrvStableCollateralOpts: CurveCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: DAI_USD_FEED, // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), + nTokens: bn('3'), + curvePool: THREE_POOL, + lpToken: THREE_POOL_TOKEN, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], +} + +export const deployCollateral = async ( + opts: CurveCollateralOpts = {} +): Promise<[TestICollateral, CurveCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds) { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeW3PoolStable() + + opts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + opts.erc20 = fix.wrapper.address + } + + opts = { ...defaultCrvStableCollateralOpts, ...opts } + + const CrvStableCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveStableCollateral' + ) + + const collateral = await CrvStableCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + } + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCrvStableCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateralOpts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + + const fix = await makeW3PoolStable() + + collateralOpts.erc20 = fix.wrapper.address + collateralOpts.curvePool = fix.curvePool.address + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.curvePool, + wrapper: fix.wrapper, + rewardTokens: [crv], + poolTokens: [fix.dai, fix.usdc, fix.usdt], + feeds: [daiFeed, usdcFeed, usdtFeed], + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWPool(ctx, amount, user, recipient, THREE_POOL_HOLDER) +} + +/* + 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 = () => { + it('wrapper allows to deposit and withdraw', async () => { + const [alice] = await ethers.getSigners() + + // Deploy Wrapper + const wrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const wrapper = await wrapperFactory.deploy( + TRI_CRYPTO_TOKEN, + 'Wrapped Curve.fi USD-BTC-ETH', + 'wcrv3crypto', + CRV, + TRI_CRYPTO_GAUGE + ) + + const amount = bn('20000').mul(bn(10).pow(await wrapper.decimals())) + + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await wrapper.underlying() + ) + await whileImpersonating(TRI_CRYPTO_HOLDER, async (signer) => { + await lpToken.connect(signer).transfer(alice.address, amount) + }) + + // Initial Balance + expect(await lpToken.balanceOf(alice.address)).to.equal(amount) + + // Deposit + await lpToken.connect(alice).approve(wrapper.address, amount) + await wrapper.connect(alice).deposit(amount, alice.address) + expect(await lpToken.balanceOf(alice.address)).to.equal(0) + + // Withdraw + await wrapper.connect(alice).withdraw(amount, alice.address) + expect(await lpToken.balanceOf(alice.address)).to.equal(amount) + }) +} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: false, + resetFork, + collateralName: 'CurveStableCollateral - CurveGaugeWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/crv/CrvVolatileTestSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvVolatileTestSuite.test.ts new file mode 100644 index 000000000..70a7905ca --- /dev/null +++ b/test/plugins/individual-collateral/curve/crv/CrvVolatileTestSuite.test.ts @@ -0,0 +1,225 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + CurveCollateralOpts, + MintCurveCollateralFunc, +} from '../pluginTestTypes' +import { mintWPool, makeWTricryptoPoolVolatile, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + USDT_ORACLE_TIMEOUT, + USDT_ORACLE_ERROR, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, + TRI_CRYPTO_HOLDER, + TRI_CRYPTO, + TRI_CRYPTO_TOKEN, + WBTC_BTC_FEED, + BTC_USD_FEED, + BTC_ORACLE_TIMEOUT, + WETH_USD_FEED, + WBTC_BTC_ORACLE_ERROR, + WBTC_ORACLE_TIMEOUT, + WETH_ORACLE_TIMEOUT, + USDT_USD_FEED, + BTC_USD_ORACLE_ERROR, + WETH_ORACLE_ERROR, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCrvVolatileCollateralOpts: CurveCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('TRICRYPTO'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: USDT_USD_FEED, // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), + nTokens: bn('3'), + curvePool: TRI_CRYPTO, + lpToken: TRI_CRYPTO_TOKEN, + poolType: CurvePoolType.Plain, + feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], + oracleTimeouts: [ + [USDT_ORACLE_TIMEOUT], + [WBTC_ORACLE_TIMEOUT, BTC_ORACLE_TIMEOUT], + [WETH_ORACLE_TIMEOUT], + ], + oracleErrors: [ + [USDT_ORACLE_ERROR], + [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], + [WETH_ORACLE_ERROR], + ], +} + +const makeFeeds = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const wethFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const wbtcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const btcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const wethFeedOrg = MockV3AggregatorFactory.attach(WETH_USD_FEED) + const wbtcFeedOrg = MockV3AggregatorFactory.attach(WBTC_BTC_FEED) + const btcFeedOrg = MockV3AggregatorFactory.attach(BTC_USD_FEED) + const usdtFeedOrg = MockV3AggregatorFactory.attach(USDT_USD_FEED) + + await wethFeed.updateAnswer(await wethFeedOrg.latestAnswer()) + await wbtcFeed.updateAnswer(await wbtcFeedOrg.latestAnswer()) + await btcFeed.updateAnswer(await btcFeedOrg.latestAnswer()) + await usdtFeed.updateAnswer(await usdtFeedOrg.latestAnswer()) + + return { wethFeed, wbtcFeed, btcFeed, usdtFeed } +} + +export const deployCollateral = async ( + opts: CurveCollateralOpts = {} +): Promise<[TestICollateral, CurveCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds) { + const { wethFeed, wbtcFeed, btcFeed, usdtFeed } = await makeFeeds() + + const fix = await makeWTricryptoPoolVolatile() + + opts.feeds = [[wethFeed.address], [wbtcFeed.address, btcFeed.address], [usdtFeed.address]] + opts.erc20 = fix.wrapper.address + } + + opts = { ...defaultCrvVolatileCollateralOpts, ...opts } + + const CrvVolatileCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveVolatileCollateral' + ) + + const collateral = await CrvVolatileCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + } + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCrvVolatileCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const { wethFeed, wbtcFeed, btcFeed, usdtFeed } = await makeFeeds() + + collateralOpts.feeds = [ + [usdtFeed.address], + [wbtcFeed.address, btcFeed.address], + [wethFeed.address], + ] + + const fix = await makeWTricryptoPoolVolatile() + + collateralOpts.erc20 = fix.wrapper.address + collateralOpts.curvePool = fix.curvePool.address + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.curvePool, + wrapper: fix.wrapper, + rewardTokens: [crv], + poolTokens: [fix.usdt, fix.wbtc, fix.weth], + feeds: [usdtFeed, btcFeed, wethFeed], // exclude wbtcFeed + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWPool(ctx, amount, user, recipient, TRI_CRYPTO_HOLDER) +} + +/* + 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 = () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: false, + resetFork, + collateralName: 'CurveVolatileCollateral - CurveGaugeWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/crv/helpers.ts b/test/plugins/individual-collateral/curve/crv/helpers.ts new file mode 100644 index 000000000..e3105d88a --- /dev/null +++ b/test/plugins/individual-collateral/curve/crv/helpers.ts @@ -0,0 +1,320 @@ +import { ethers } from 'hardhat' +import { BigNumberish } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { whileImpersonating } from '../../../../utils/impersonation' +import { bn, fp } from '../../../../../common/numbers' +import { + CurveGaugeWrapper, + CurvePoolMock, + CurveMetapoolMock, + ERC20Mock, + ICurvePool, + MockV3Aggregator, +} from '../../../../../typechain' +import { getResetFork } from '../../helpers' +import { + CRV, + DAI, + USDC, + USDT, + MIM, + THREE_POOL, + THREE_POOL_GAUGE, + THREE_POOL_TOKEN, + FORK_BLOCK, + WBTC, + WETH, + TRI_CRYPTO, + TRI_CRYPTO_GAUGE, + TRI_CRYPTO_TOKEN, + FRAX, + FRAX_BP, + eUSD, + eUSD_FRAX_BP, + eUSD_GAUGE, + eUSD_FRAX_HOLDER, + MIM_THREE_POOL, + MIM_THREE_POOL_GAUGE, + MIM_THREE_POOL_HOLDER, +} from '../constants' +import { CurveBase } from '../pluginTestTypes' + +export interface Wrapped3PoolFixtureStable extends CurveBase { + dai: ERC20Mock + usdc: ERC20Mock + usdt: ERC20Mock +} + +export const makeW3PoolStable = async (): Promise => { + // Use real reference ERC20s + const dai = await ethers.getContractAt('ERC20Mock', DAI) + const usdc = await ethers.getContractAt('ERC20Mock', USDC) + const usdt = await ethers.getContractAt('ERC20Mock', USDT) + + // Use mock curvePool seeded with initial balances + const CurvePoolMockFactory = await ethers.getContractFactory('CurvePoolMock') + const realCurvePool = await ethers.getContractAt('CurvePoolMock', THREE_POOL) + const curvePool = ( + await CurvePoolMockFactory.deploy( + [ + await realCurvePool.balances(0), + await realCurvePool.balances(1), + await realCurvePool.balances(2), + ], + [await realCurvePool.coins(0), await realCurvePool.coins(1), await realCurvePool.coins(2)] + ) + ) + await curvePool.setVirtualPrice(await realCurvePool.get_virtual_price()) + + // Deploy Wrapper + const wrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const wrapper = await wrapperFactory.deploy( + THREE_POOL_TOKEN, + 'Wrapped Staked Curve.fi DAI/USDC/USDT', + 'ws3Crv', + CRV, + THREE_POOL_GAUGE + ) + + return { + dai, + usdc, + usdt, + curvePool, + wrapper, + } +} + +export interface Wrapped3PoolFixtureVolatile extends CurveBase { + usdt: ERC20Mock + wbtc: ERC20Mock + weth: ERC20Mock +} + +export const makeWTricryptoPoolVolatile = async (): Promise => { + // Use real reference ERC20s + const usdt = await ethers.getContractAt('ERC20Mock', USDT) + const wbtc = await ethers.getContractAt('ERC20Mock', WBTC) + const weth = await ethers.getContractAt('ERC20Mock', WETH) + + // Use mock curvePool seeded with initial balances + const CurvePoolMockFactory = await ethers.getContractFactory('CurvePoolMock') + const realCurvePool = await ethers.getContractAt('CurvePoolMock', TRI_CRYPTO) + const curvePool = ( + await CurvePoolMockFactory.deploy( + [ + await realCurvePool.balances(0), + await realCurvePool.balances(1), + await realCurvePool.balances(2), + ], + [await realCurvePool.coins(0), await realCurvePool.coins(1), await realCurvePool.coins(2)] + ) + ) + await curvePool.setVirtualPrice(await realCurvePool.get_virtual_price()) + + // Deploy Wrapper + const wrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const wrapper = await wrapperFactory.deploy( + TRI_CRYPTO_TOKEN, + 'Wrapped Curve.fi USD-BTC-ETH', + 'wcrv3crypto', + CRV, + TRI_CRYPTO_GAUGE + ) + + return { + usdt, + wbtc, + weth, + curvePool, + wrapper, + } +} + +export const mintWPool = async ( + ctx: CurveBase, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string, + holder: string +) => { + const wrapper = ctx.wrapper as CurveGaugeWrapper + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await wrapper.underlying() + ) + await whileImpersonating(holder, async (signer) => { + await lpToken.connect(signer).transfer(user.address, amount) + }) + + await lpToken.connect(user).approve(wrapper.address, amount) + await wrapper.connect(user).deposit(amount, recipient) +} + +export const resetFork = getResetFork(FORK_BLOCK) + +export type Numeric = number | bigint + +export const exp = (i: Numeric, d: Numeric = 0): bigint => { + return BigInt(i) * 10n ** BigInt(d) +} + +// ===== eUSD / fraxBP + +export interface WrappedEUSDFraxBPFixture { + usdc: ERC20Mock + frax: ERC20Mock + eusd: ERC20Mock + metapoolToken: CurveMetapoolMock + realMetapool: CurveMetapoolMock + curvePool: ICurvePool + wPool: CurveGaugeWrapper +} + +export const makeWeUSDFraxBP = async ( + eusdFeed: MockV3Aggregator +): Promise => { + // Make a fake RTokenAsset and register it with eUSD's assetRegistry + const AssetFactory = await ethers.getContractFactory('Asset') + const mockRTokenAsset = await AssetFactory.deploy( + bn('604800'), + eusdFeed.address, + fp('0.01'), + eUSD, + fp('1e6'), + bn('604800') + ) + const eUSDAssetRegistry = await ethers.getContractAt( + 'IAssetRegistry', + '0x9B85aC04A09c8C813c37de9B3d563C2D3F936162' + ) + await whileImpersonating('0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c', async (signer) => { + await eUSDAssetRegistry.connect(signer).swapRegistered(mockRTokenAsset.address) + }) + + // Use real reference ERC20s + const usdc = await ethers.getContractAt('ERC20Mock', USDC) + const frax = await ethers.getContractAt('ERC20Mock', FRAX) + const eusd = await ethers.getContractAt('ERC20Mock', eUSD) + + // Use real fraxBP pool + const curvePool = await ethers.getContractAt('ICurvePool', FRAX_BP) + + // Use mock curvePool seeded with initial balances + const CurveMetapoolMockFactory = await ethers.getContractFactory('CurveMetapoolMock') + const realMetapool = ( + await ethers.getContractAt('CurveMetapoolMock', eUSD_FRAX_BP) + ) + const metapoolToken = ( + await CurveMetapoolMockFactory.deploy( + [await realMetapool.balances(0), await realMetapool.balances(1)], + [await realMetapool.coins(0), await realMetapool.coins(1)] + ) + ) + await metapoolToken.setVirtualPrice(await realMetapool.get_virtual_price()) + await metapoolToken.mint(eUSD_FRAX_HOLDER, await realMetapool.balanceOf(eUSD_FRAX_HOLDER)) + + // Deploy Wrapper + const wrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const wrapper = await wrapperFactory.deploy( + eUSD_FRAX_BP, + 'Wrapped Curve eUSD+FRAX/USDC', + 'weUSDFRAXBP', + CRV, + eUSD_GAUGE + ) + + return { usdc, frax, eusd, metapoolToken, realMetapool, curvePool, wPool: wrapper } +} + +export const mintWeUSDFraxBP = async ( + ctx: CurveBase, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string, + holder: string +) => { + const wrapper = ctx.wrapper as CurveGaugeWrapper + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await wrapper.underlying() + ) + await whileImpersonating(holder, async (signer) => { + await lpToken.connect(signer).transfer(user.address, amount) + }) + + await lpToken.connect(user).approve(wrapper.address, amount) + await wrapper.connect(user).deposit(amount, recipient) +} + +// === MIM + 3Pool + +export interface WrappedMIM3PoolFixture { + dai: ERC20Mock + usdc: ERC20Mock + usdt: ERC20Mock + mim: ERC20Mock + metapoolToken: CurveMetapoolMock + realMetapool: CurveMetapoolMock + curvePool: ICurvePool + wPool: CurveGaugeWrapper +} + +export const makeWMIM3Pool = async (): Promise => { + // Use real reference ERC20s + const dai = await ethers.getContractAt('ERC20Mock', DAI) + const usdc = await ethers.getContractAt('ERC20Mock', USDC) + const usdt = await ethers.getContractAt('ERC20Mock', USDT) + const mim = await ethers.getContractAt('ERC20Mock', MIM) + + // Use real MIM pool + const curvePool = await ethers.getContractAt('ICurvePool', THREE_POOL) + + // Use mock curvePool seeded with initial balances + const CurveMetapoolMockFactory = await ethers.getContractFactory('CurveMetapoolMock') + const realMetapool = ( + await ethers.getContractAt('CurveMetapoolMock', MIM_THREE_POOL) + ) + const metapoolToken = ( + await CurveMetapoolMockFactory.deploy( + [await realMetapool.balances(0), await realMetapool.balances(1)], + [await realMetapool.coins(0), await realMetapool.coins(1)] + ) + ) + await metapoolToken.setVirtualPrice(await realMetapool.get_virtual_price()) + await metapoolToken.mint( + MIM_THREE_POOL_HOLDER, + await realMetapool.balanceOf(MIM_THREE_POOL_HOLDER) + ) + // Deploy Wrapper + const wrapperFactory = await ethers.getContractFactory('CurveGaugeWrapper') + const wrapper = await wrapperFactory.deploy( + MIM_THREE_POOL, + 'Wrapped Curve MIM+3Pool', + 'wMIM3CRV', + CRV, + MIM_THREE_POOL_GAUGE + ) + + return { dai, usdc, usdt, mim, metapoolToken, realMetapool, curvePool, wPool: wrapper } +} + +export const mintWMIM3Pool = async ( + ctx: CurveBase, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string, + holder: string +) => { + const wrapper = ctx.wrapper as CurveGaugeWrapper + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await wrapper.underlying() + ) + await whileImpersonating(holder, async (signer) => { + await lpToken.connect(signer).transfer(user.address, amount) + }) + + await lpToken.connect(user).approve(ctx.wrapper.address, amount) + await wrapper.connect(user).deposit(amount, recipient) +} diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableMetapoolSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableMetapoolSuite.test.ts new file mode 100644 index 000000000..b92ba7c1b --- /dev/null +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableMetapoolSuite.test.ts @@ -0,0 +1,237 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + MintCurveCollateralFunc, + CurveMetapoolCollateralOpts, +} from '../pluginTestTypes' +import { makeWMIM3Pool, mintWMIM3Pool, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { BigNumberish } from 'ethers' +import { + CurveStableMetapoolCollateral, + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + THREE_POOL, + THREE_POOL_TOKEN, + THREE_POOL_DEFAULT_THRESHOLD, + CVX, + DAI_USD_FEED, + DAI_ORACLE_TIMEOUT, + DAI_ORACLE_ERROR, + MIM_DEFAULT_THRESHOLD, + MIM_USD_FEED, + MIM_ORACLE_TIMEOUT, + MIM_ORACLE_ERROR, + MIM_THREE_POOL, + MIM_THREE_POOL_HOLDER, + USDC_USD_FEED, + USDC_ORACLE_TIMEOUT, + USDC_ORACLE_ERROR, + USDT_USD_FEED, + USDT_ORACLE_TIMEOUT, + USDT_ORACLE_ERROR, + MAX_TRADE_VOL, + DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCvxStableCollateralOpts: CurveMetapoolCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: MIM_USD_FEED, + oracleTimeout: MIM_ORACLE_TIMEOUT, + oracleError: MIM_ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: THREE_POOL_DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), // TODO + nTokens: bn('3'), + curvePool: THREE_POOL, + lpToken: THREE_POOL_TOKEN, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], + metapoolToken: MIM_THREE_POOL, + pairedTokenDefaultThreshold: MIM_DEFAULT_THRESHOLD, +} + +export const deployCollateral = async ( + opts: CurveMetapoolCollateralOpts = {} +): Promise<[TestICollateral, CurveMetapoolCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds && !opts.chainlinkFeed) { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const mimFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeWMIM3Pool() + + opts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + opts.erc20 = fix.wPool.address + opts.chainlinkFeed = mimFeed.address + } + + opts = { ...defaultCvxStableCollateralOpts, ...opts } + + const CvxStableCollateralFactory = await ethers.getContractFactory( + 'CurveStableMetapoolCollateral' + ) + + const collateral = await CvxStableCollateralFactory.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!, + { + nTokens: opts.nTokens!, + curvePool: opts.curvePool!, + poolType: opts.poolType!, + feeds: opts.feeds!, + oracleTimeouts: opts.oracleTimeouts!, + oracleErrors: opts.oracleErrors!, + lpToken: opts.lpToken!, + }, + opts.metapoolToken!, + opts.pairedTokenDefaultThreshold! + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral as unknown as TestICollateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveMetapoolCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCvxStableCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const mimFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeWMIM3Pool() + + collateralOpts.erc20 = fix.wPool.address + collateralOpts.chainlinkFeed = mimFeed.address + collateralOpts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + collateralOpts.curvePool = fix.curvePool.address + collateralOpts.metapoolToken = fix.metapoolToken.address + + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const cvx = await ethers.getContractAt('ERC20Mock', CVX) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.metapoolToken, + wrapper: fix.wPool, + rewardTokens: [cvx, crv], + poolTokens: [fix.dai, fix.usdc, fix.usdt], + feeds: [mimFeed, daiFeed, usdcFeed, usdtFeed], + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWMIM3Pool(ctx, amount, user, recipient, MIM_THREE_POOL_HOLDER) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => { + it('does not allow empty metaPoolToken', async () => { + await expect(deployCollateral({ metapoolToken: ZERO_ADDRESS })).to.be.revertedWith( + 'metapoolToken address is zero' + ) + }) + + it('does not allow invalid pairedTokenDefaultThreshold', async () => { + await expect(deployCollateral({ pairedTokenDefaultThreshold: bn(0) })).to.be.revertedWith( + 'pairedTokenDefaultThreshold out of bounds' + ) + + await expect( + deployCollateral({ pairedTokenDefaultThreshold: bn('1.1e18') }) + ).to.be.revertedWith('pairedTokenDefaultThreshold out of bounds') + }) + + it('does not allow invalid Pool Type', async () => { + await expect(deployCollateral({ metapoolToken: ZERO_ADDRESS })).to.be.revertedWith( + 'metapoolToken address is zero' + ) + }) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: true, + resetFork, + collateralName: 'CurveStableMetapoolCollateral - ConvexStakingWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts new file mode 100644 index 000000000..fbb8b04f7 --- /dev/null +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts @@ -0,0 +1,221 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + CurveMetapoolCollateralOpts, + MintCurveCollateralFunc, +} from '../pluginTestTypes' +import { makeWeUSDFraxBP, mintWeUSDFraxBP, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS, ONE_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + eUSD_FRAX_BP, + FRAX_BP, + FRAX_BP_TOKEN, + CVX, + USDC_USD_FEED, + USDC_ORACLE_TIMEOUT, + USDC_ORACLE_ERROR, + FRAX_USD_FEED, + FRAX_ORACLE_TIMEOUT, + FRAX_ORACLE_ERROR, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + RTOKEN_DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, + eUSD_FRAX_HOLDER, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCvxStableCollateralOpts: CurveMetapoolCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: ONE_ADDRESS, // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: RTOKEN_DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), // TODO + nTokens: bn('2'), + curvePool: FRAX_BP, + lpToken: FRAX_BP_TOKEN, + poolType: CurvePoolType.Plain, // for fraxBP, not the top-level pool + feeds: [[FRAX_USD_FEED], [USDC_USD_FEED]], + oracleTimeouts: [[FRAX_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT]], + oracleErrors: [[FRAX_ORACLE_ERROR], [USDC_ORACLE_ERROR]], + metapoolToken: eUSD_FRAX_BP, + pairedTokenDefaultThreshold: DEFAULT_THRESHOLD, +} + +export const deployCollateral = async ( + opts: CurveMetapoolCollateralOpts = {} +): Promise<[TestICollateral, CurveMetapoolCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds) { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: FRAX, USDC, eUSD + const fraxFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const eusdFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeWeUSDFraxBP(eusdFeed) + + opts.feeds = [[fraxFeed.address], [usdcFeed.address]] + opts.erc20 = fix.wPool.address + } + + opts = { ...defaultCvxStableCollateralOpts, ...opts } + + const CvxStableRTokenMetapoolCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveStableRTokenMetapoolCollateral' + ) + + const collateral = await CvxStableRTokenMetapoolCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + }, + opts.metapoolToken, + opts.pairedTokenDefaultThreshold + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral as unknown as TestICollateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveMetapoolCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCvxStableCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all feeds: FRAX, USDC, RToken + const fraxFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const eusdFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const fix = await makeWeUSDFraxBP(eusdFeed) + collateralOpts.feeds = [[fraxFeed.address], [usdcFeed.address]] + + collateralOpts.erc20 = fix.wPool.address + collateralOpts.curvePool = fix.curvePool.address + collateralOpts.metapoolToken = fix.metapoolToken.address + + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const cvx = await ethers.getContractAt('ERC20Mock', CVX) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.metapoolToken, + wrapper: fix.wPool, + rewardTokens: [cvx, crv], + chainlinkFeed: usdcFeed, + poolTokens: [fix.frax, fix.usdc], + feeds: [fraxFeed, usdcFeed, eusdFeed], + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWeUSDFraxBP(ctx, amount, user, recipient, eUSD_FRAX_HOLDER) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => { + it('does not allow empty metaPoolToken', async () => { + await expect(deployCollateral({ metapoolToken: ZERO_ADDRESS })).to.be.revertedWith( + 'metapoolToken address is zero' + ) + }) + + it('does not allow invalid pairedTokenDefaultThreshold', async () => { + await expect(deployCollateral({ pairedTokenDefaultThreshold: bn(0) })).to.be.revertedWith( + 'pairedTokenDefaultThreshold out of bounds' + ) + + await expect( + deployCollateral({ pairedTokenDefaultThreshold: bn('1.1e18') }) + ).to.be.revertedWith('pairedTokenDefaultThreshold out of bounds') + }) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: true, + resetFork, + collateralName: 'CurveStableRTokenMetapoolCollateral - ConvexStakingWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts new file mode 100644 index 000000000..eb9fc0eb4 --- /dev/null +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableTestSuite.test.ts @@ -0,0 +1,436 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + CurveCollateralOpts, + MintCurveCollateralFunc, +} from '../pluginTestTypes' +import { mintWPool, makeW3PoolStable, makeWSUSDPoolStable, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn, fp } from '../../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + THREE_POOL, + THREE_POOL_TOKEN, + SUSD_POOL_TOKEN, + CVX, + DAI_USD_FEED, + DAI_ORACLE_TIMEOUT, + DAI_ORACLE_ERROR, + USDC_USD_FEED, + USDC_ORACLE_TIMEOUT, + USDC_ORACLE_ERROR, + USDT_USD_FEED, + USDT_ORACLE_TIMEOUT, + USDT_ORACLE_ERROR, + SUSD_USD_FEED, + SUSD_ORACLE_TIMEOUT, + SUSD_ORACLE_ERROR, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, + THREE_POOL_HOLDER, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCvxStableCollateralOpts: CurveCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('USD'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: DAI_USD_FEED, // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), + nTokens: bn('3'), + curvePool: THREE_POOL, + lpToken: THREE_POOL_TOKEN, + poolType: CurvePoolType.Plain, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED]], + oracleTimeouts: [[DAI_ORACLE_TIMEOUT], [USDC_ORACLE_TIMEOUT], [USDT_ORACLE_TIMEOUT]], + oracleErrors: [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR]], +} + +export const deployCollateral = async ( + opts: CurveCollateralOpts = {} +): Promise<[TestICollateral, CurveCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds) { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const fix = await makeW3PoolStable() + + opts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + opts.erc20 = fix.wrapper.address + } + + opts = { ...defaultCvxStableCollateralOpts, ...opts } + + const CvxStableCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveStableCollateral' + ) + + const collateral = await CvxStableCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + } + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral, opts] +} + +export const deployMaxTokensCollateral = async ( + opts: CurveCollateralOpts = {} +): Promise<[TestICollateral, CurveCollateralOpts]> => { + const fix = await makeWSUSDPoolStable() + + // Set default options for max tokens case + const maxTokenCollOpts = { + ...defaultCvxStableCollateralOpts, + ...{ + nTokens: bn('4'), + erc20: fix.wrapper.address, + curvePool: fix.curvePool.address, + lpToken: SUSD_POOL_TOKEN, + feeds: [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED], [SUSD_USD_FEED, SUSD_USD_FEED]], + oracleTimeouts: [ + [DAI_ORACLE_TIMEOUT], + [USDC_ORACLE_TIMEOUT], + [USDT_ORACLE_TIMEOUT], + [SUSD_ORACLE_TIMEOUT, SUSD_ORACLE_TIMEOUT], + ], + oracleErrors: [ + [DAI_ORACLE_ERROR], + [USDC_ORACLE_ERROR], + [USDT_ORACLE_ERROR], + [SUSD_ORACLE_ERROR], + ], + }, + } + + opts = { ...maxTokenCollOpts, ...opts } + + const CvxStableCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveStableCollateral' + ) + + const collateral = await CvxStableCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + } + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCvxStableCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const daiFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + collateralOpts.feeds = [[daiFeed.address], [usdcFeed.address], [usdtFeed.address]] + + const fix = await makeW3PoolStable() + + collateralOpts.erc20 = fix.wrapper.address + collateralOpts.curvePool = fix.curvePool.address + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const cvx = await ethers.getContractAt('ERC20Mock', CVX) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.curvePool, + wrapper: fix.wrapper, + rewardTokens: [cvx, crv], + poolTokens: [fix.dai, fix.usdc, fix.usdt], + feeds: [daiFeed, usdcFeed, usdtFeed], + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWPool(ctx, amount, user, recipient, THREE_POOL_HOLDER) +} + +/* + Define collateral-specific tests +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificConstructorTests = () => { + describe('Handles constructor with 4 tokens (max allowed) - sUSD', () => { + let collateral: TestICollateral + before(resetFork) + it('deploys plugin successfully', async () => { + ;[collateral] = await deployMaxTokensCollateral() + expect(await collateral.address).to.not.equal(ZERO_ADDRESS) + const [low, high] = await collateral.price() + expect(low).to.be.closeTo(fp('1.06'), fp('0.01')) // close to $1 + expect(high).to.be.closeTo(fp('1.07'), fp('0.01')) + expect(high).to.be.gt(low) + + // Token price + const cvxMultiFeedStableCollateral = await ethers.getContractAt( + 'CurveStableCollateral', + collateral.address + ) + for (let i = 0; i < 4; i++) { + const [lowTkn, highTkn] = await cvxMultiFeedStableCollateral.tokenPrice(i) + expect(lowTkn).to.be.closeTo(fp('1'), fp('0.01')) // close to $1 + expect(highTkn).to.be.closeTo(fp('1'), fp('0.01')) + expect(highTkn).to.be.gt(lowTkn) + } + + await expect(cvxMultiFeedStableCollateral.tokenPrice(5)).to.be.revertedWithCustomError( + cvxMultiFeedStableCollateral, + 'WrongIndex' + ) + }) + + it('validates non-zero-address for final token - edge case', async () => { + // Set empty the final feed + let feeds = [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED], [ZERO_ADDRESS, ZERO_ADDRESS]] + await expect(deployMaxTokensCollateral({ feeds })).to.be.revertedWith('t3feed0 empty') + + feeds = [[DAI_USD_FEED], [USDC_USD_FEED], [USDT_USD_FEED], [SUSD_USD_FEED, ZERO_ADDRESS]] + await expect(deployMaxTokensCollateral({ feeds })).to.be.revertedWith('t3feed1 empty') + }) + + it('validates non-zero oracle timeout for final token - edge case', async () => { + // Set empty the final oracle timeouts + let oracleTimeouts = [ + [DAI_ORACLE_TIMEOUT], + [USDC_ORACLE_TIMEOUT], + [USDT_ORACLE_TIMEOUT], + [bn(0), bn(0)], + ] + await expect(deployMaxTokensCollateral({ oracleTimeouts })).to.be.revertedWith( + 't3timeout0 zero' + ) + + const feeds = [ + [DAI_USD_FEED], + [USDC_USD_FEED], + [USDT_USD_FEED], + [SUSD_USD_FEED, SUSD_USD_FEED], + ] + oracleTimeouts = [ + [DAI_ORACLE_TIMEOUT], + [USDC_ORACLE_TIMEOUT], + [USDT_ORACLE_TIMEOUT], + [SUSD_ORACLE_TIMEOUT, bn(0)], + ] + + await expect(deployMaxTokensCollateral({ feeds, oracleTimeouts })).to.be.revertedWith( + 't3timeout1 zero' + ) + }) + + it('validates non-large oracle error for final token - edge case', async () => { + // Set empty the final oracle errors + let oracleErrors = [[DAI_ORACLE_ERROR], [USDC_ORACLE_ERROR], [USDT_ORACLE_ERROR], [fp('1')]] + await expect(deployMaxTokensCollateral({ oracleErrors })).to.be.revertedWith( + 't3error0 too large' + ) + + const feeds = [ + [DAI_USD_FEED], + [USDC_USD_FEED], + [USDT_USD_FEED], + [SUSD_USD_FEED, SUSD_USD_FEED], + ] + const oracleTimeouts = [ + [DAI_ORACLE_TIMEOUT], + [USDC_ORACLE_TIMEOUT], + [USDT_ORACLE_TIMEOUT], + [SUSD_ORACLE_TIMEOUT, SUSD_ORACLE_TIMEOUT], + ] + + oracleErrors = [ + [DAI_ORACLE_ERROR], + [USDC_ORACLE_ERROR], + [USDT_ORACLE_ERROR], + [SUSD_ORACLE_ERROR, fp('1')], + ] + + await expect( + deployMaxTokensCollateral({ feeds, oracleTimeouts, oracleErrors }) + ).to.be.revertedWith('t3error1 too large') + + // If we don't specify it it will use 0 + oracleErrors = [ + [DAI_ORACLE_ERROR], + [USDC_ORACLE_ERROR], + [USDT_ORACLE_ERROR], + [SUSD_ORACLE_ERROR], + ] + + await expect(deployMaxTokensCollateral({ feeds, oracleTimeouts, oracleErrors })).to.not.be + .reverted + }) + }) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const collateralSpecificStatusTests = () => { + it('handles properly multiple price feeds', async () => { + const MockV3AggregatorFactory = await ethers.getContractFactory('MockV3Aggregator') + const feed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const feedStable = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const fix = await makeW3PoolStable() + + const opts: CurveCollateralOpts = { ...defaultCvxStableCollateralOpts } + const nonzeroError = opts.oracleTimeouts![0][0] + const nonzeroTimeout = bn(opts.oracleTimeouts![0][0]) + const feeds = [ + [feed.address, feedStable.address], + [feed.address, feedStable.address], + [feed.address, feedStable.address], + ] + const oracleTimeouts = [ + [nonzeroTimeout, nonzeroTimeout], + [nonzeroTimeout, nonzeroTimeout], + [nonzeroTimeout, nonzeroTimeout], + ] + const oracleErrors = [ + [nonzeroError, nonzeroError], + [nonzeroError, nonzeroError], + [nonzeroError, nonzeroError], + ] + + const [multiFeedCollateral] = await deployCollateral({ + erc20: fix.wrapper.address, + feeds, + oracleTimeouts, + oracleErrors, + }) + + const initialRefPerTok = await multiFeedCollateral.refPerTok() + const [low, high] = await multiFeedCollateral.price() + + // Update values in Oracles increase by 10% + const initialPrice = await feed.latestRoundData() + await (await feed.updateAnswer(initialPrice.answer.mul(110).div(100))).wait() + + const [newLow, newHigh] = await multiFeedCollateral.price() + + // with 18 decimals of price precision a 1e-9 tolerance seems fine for a 10% change + // and without this kind of tolerance the Volatile pool tests fail due to small movements + expect(newLow).to.be.closeTo(low.mul(110).div(100), fp('1e-9')) + expect(newHigh).to.be.closeTo(high.mul(110).div(100), fp('1e-9')) + + // Check refPerTok remains the same (because we have not refreshed) + const finalRefPerTok = await multiFeedCollateral.refPerTok() + expect(finalRefPerTok).to.equal(initialRefPerTok) + }) +} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: false, + resetFork, + collateralName: 'CurveStableCollateral - ConvexStakingWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/curve/cvx/CvxVolatileTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxVolatileTestSuite.test.ts new file mode 100644 index 000000000..181a7fe59 --- /dev/null +++ b/test/plugins/individual-collateral/curve/cvx/CvxVolatileTestSuite.test.ts @@ -0,0 +1,227 @@ +import collateralTests from '../collateralTests' +import { + CurveCollateralFixtureContext, + CurveCollateralOpts, + MintCurveCollateralFunc, +} from '../pluginTestTypes' +import { mintWPool, makeWTricryptoPoolVolatile, resetFork } from './helpers' +import { ethers } from 'hardhat' +import { ContractFactory, BigNumberish } from 'ethers' +import { + ERC20Mock, + MockV3Aggregator, + MockV3Aggregator__factory, + TestICollateral, +} from '../../../../../typechain' +import { bn } from '../../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../../common/constants' +import { expect } from 'chai' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + PRICE_TIMEOUT, + CVX, + USDT_ORACLE_TIMEOUT, + USDT_ORACLE_ERROR, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + CurvePoolType, + CRV, + TRI_CRYPTO_HOLDER, + TRI_CRYPTO, + TRI_CRYPTO_TOKEN, + WBTC_BTC_FEED, + BTC_USD_FEED, + BTC_ORACLE_TIMEOUT, + WETH_USD_FEED, + WBTC_BTC_ORACLE_ERROR, + WBTC_ORACLE_TIMEOUT, + WETH_ORACLE_TIMEOUT, + USDT_USD_FEED, + BTC_USD_ORACLE_ERROR, + WETH_ORACLE_ERROR, +} from '../constants' + +type Fixture = () => Promise + +export const defaultCvxVolatileCollateralOpts: CurveCollateralOpts = { + erc20: ZERO_ADDRESS, + targetName: ethers.utils.formatBytes32String('TRICRYPTO'), + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: USDT_USD_FEED, // unused but cannot be zero + oracleTimeout: bn('1'), // unused but cannot be zero + oracleError: bn('1'), // unused but cannot be zero + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: bn('0'), + nTokens: bn('3'), + curvePool: TRI_CRYPTO, + lpToken: TRI_CRYPTO_TOKEN, + poolType: CurvePoolType.Plain, + feeds: [[USDT_USD_FEED], [WBTC_BTC_FEED, BTC_USD_FEED], [WETH_USD_FEED]], + oracleTimeouts: [ + [USDT_ORACLE_TIMEOUT], + [WBTC_ORACLE_TIMEOUT, BTC_ORACLE_TIMEOUT], + [WETH_ORACLE_TIMEOUT], + ], + oracleErrors: [ + [USDT_ORACLE_ERROR], + [WBTC_BTC_ORACLE_ERROR, BTC_USD_ORACLE_ERROR], + [WETH_ORACLE_ERROR], + ], +} + +const makeFeeds = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + // Substitute all 3 feeds: DAI, USDC, USDT + const wethFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const wbtcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const btcFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + const usdtFeed = await MockV3AggregatorFactory.deploy(8, bn('1e8')) + + const wethFeedOrg = MockV3AggregatorFactory.attach(WETH_USD_FEED) + const wbtcFeedOrg = MockV3AggregatorFactory.attach(WBTC_BTC_FEED) + const btcFeedOrg = MockV3AggregatorFactory.attach(BTC_USD_FEED) + const usdtFeedOrg = MockV3AggregatorFactory.attach(USDT_USD_FEED) + + await wethFeed.updateAnswer(await wethFeedOrg.latestAnswer()) + await wbtcFeed.updateAnswer(await wbtcFeedOrg.latestAnswer()) + await btcFeed.updateAnswer(await btcFeedOrg.latestAnswer()) + await usdtFeed.updateAnswer(await usdtFeedOrg.latestAnswer()) + + return { wethFeed, wbtcFeed, btcFeed, usdtFeed } +} + +export const deployCollateral = async ( + opts: CurveCollateralOpts = {} +): Promise<[TestICollateral, CurveCollateralOpts]> => { + if (!opts.erc20 && !opts.feeds) { + const { wethFeed, wbtcFeed, btcFeed, usdtFeed } = await makeFeeds() + + const fix = await makeWTricryptoPoolVolatile() + + opts.feeds = [[wethFeed.address], [wbtcFeed.address, btcFeed.address], [usdtFeed.address]] + opts.erc20 = fix.wrapper.address + } + + opts = { ...defaultCvxVolatileCollateralOpts, ...opts } + + const CvxVolatileCollateralFactory: ContractFactory = await ethers.getContractFactory( + 'CurveVolatileCollateral' + ) + + const collateral = await CvxVolatileCollateralFactory.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, + { + nTokens: opts.nTokens, + curvePool: opts.curvePool, + poolType: opts.poolType, + feeds: opts.feeds, + oracleTimeouts: opts.oracleTimeouts, + oracleErrors: opts.oracleErrors, + lpToken: opts.lpToken, + } + ) + await collateral.deployed() + + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return [collateral, opts] +} + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CurveCollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultCvxVolatileCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const { wethFeed, wbtcFeed, btcFeed, usdtFeed } = await makeFeeds() + + collateralOpts.feeds = [ + [usdtFeed.address], + [wbtcFeed.address, btcFeed.address], + [wethFeed.address], + ] + + const fix = await makeWTricryptoPoolVolatile() + + collateralOpts.erc20 = fix.wrapper.address + collateralOpts.curvePool = fix.curvePool.address + const collateral = ((await deployCollateral(collateralOpts))[0] as unknown) + const cvx = await ethers.getContractAt('ERC20Mock', CVX) + const crv = await ethers.getContractAt('ERC20Mock', CRV) + + return { + alice, + collateral, + curvePool: fix.curvePool, + wrapper: fix.wrapper, + rewardTokens: [cvx, crv], + poolTokens: [fix.usdt, fix.wbtc, fix.weth], + feeds: [usdtFeed, btcFeed, wethFeed], // exclude wbtcFeed + } + } + + return makeCollateralFixtureContext +} + +/* + Define helper functions +*/ + +const mintCollateralTo: MintCurveCollateralFunc = async ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintWPool(ctx, amount, user, recipient, TRI_CRYPTO_HOLDER) +} + +/* + 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 = () => {} + +/* + Run the test suite +*/ + +const opts = { + deployCollateral, + collateralSpecificConstructorTests, + collateralSpecificStatusTests, + makeCollateralFixtureContext, + mintCollateralTo, + itChecksTargetPerRefDefault: it, + itChecksRefPerTokDefault: it, + itHasRevenueHiding: it, + isMetapool: false, + resetFork, + collateralName: 'CurveVolatileCollateral - ConvexStakingWrapper', +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/convex/helpers.ts b/test/plugins/individual-collateral/curve/cvx/helpers.ts similarity index 66% rename from test/plugins/individual-collateral/convex/helpers.ts rename to test/plugins/individual-collateral/curve/cvx/helpers.ts index a083da8ab..686a2d8f8 100644 --- a/test/plugins/individual-collateral/convex/helpers.ts +++ b/test/plugins/individual-collateral/curve/cvx/helpers.ts @@ -1,30 +1,33 @@ import { ethers } from 'hardhat' import { BigNumberish } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { whileImpersonating } from '../../../utils/impersonation' -import { bn, fp } from '../../../../common/numbers' +import { whileImpersonating } from '../../../../utils/impersonation' +import { bn, fp } from '../../../../../common/numbers' import { ConvexStakingWrapper, CurvePoolMock, + CurvePoolMockVariantInt, CurveMetapoolMock, ERC20Mock, ICurvePool, MockV3Aggregator, -} from '../../../../typechain' -import { getResetFork } from '../helpers' + RewardableERC4626Vault, +} from '../../../../../typechain' +import { getResetFork } from '../../helpers' import { DAI, USDC, USDT, + SUSD, MIM, THREE_POOL, - THREE_POOL_TOKEN, THREE_POOL_CVX_POOL_ID, + SUSD_POOL, + SUSD_POOL_CVX_POOL_ID, FORK_BLOCK, WBTC, WETH, TRI_CRYPTO, - TRI_CRYPTO_TOKEN, TRI_CRYPTO_CVX_POOL_ID, FRAX, FRAX_BP, @@ -35,20 +38,19 @@ import { MIM_THREE_POOL, MIM_THREE_POOL_POOL_ID, MIM_THREE_POOL_HOLDER, -} from './constants' +} from '../constants' +import { CurveBase } from '../pluginTestTypes' -interface WrappedPoolBase { - curvePool: CurvePoolMock - crv3Pool: ERC20Mock - w3Pool: ConvexStakingWrapper -} - -export interface Wrapped3PoolFixtureStable extends WrappedPoolBase { +export interface Wrapped3PoolFixtureStable extends CurveBase { dai: ERC20Mock usdc: ERC20Mock usdt: ERC20Mock } +export interface WrappedSUSDPoolFixtureStable extends Wrapped3PoolFixtureStable { + susd: ERC20Mock +} + export const makeW3PoolStable = async (): Promise => { // Use real reference ERC20s const dai = await ethers.getContractAt('ERC20Mock', DAI) @@ -70,8 +72,58 @@ export const makeW3PoolStable = async (): Promise => ) await curvePool.setVirtualPrice(await realCurvePool.get_virtual_price()) - // Use real Curve/Convex contracts - const crv3Pool = await ethers.getContractAt('ERC20Mock', THREE_POOL_TOKEN) + // Deploy external cvxMining lib + const CvxMiningFactory = await ethers.getContractFactory('CvxMining') + const cvxMining = await CvxMiningFactory.deploy() + + // Deploy Wrapper + const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { + libraries: { CvxMining: cvxMining.address }, + }) + const wrapper = await wrapperFactory.deploy() + await wrapper.initialize(THREE_POOL_CVX_POOL_ID) + + return { + dai, + usdc, + usdt, + curvePool, + wrapper: wrapper as unknown as RewardableERC4626Vault, + } +} + +export const makeWSUSDPoolStable = async (): Promise => { + // Use real reference ERC20s + const dai = await ethers.getContractAt('ERC20Mock', DAI) + const usdc = await ethers.getContractAt('ERC20Mock', USDC) + const usdt = await ethers.getContractAt('ERC20Mock', USDT) + const susd = await ethers.getContractAt('ERC20Mock', SUSD) + + // Use mock curvePool seeded with initial balances + const CurvePoolMockFactory = await ethers.getContractFactory('CurvePoolMock') + // Requires a special interface to interact with the real sUSD Curve Pool + const realCurvePool = ( + await ethers.getContractAt('CurvePoolMockVariantInt', SUSD_POOL) + ) + + const curvePool = ( + await CurvePoolMockFactory.deploy( + [ + await realCurvePool.balances(0), + await realCurvePool.balances(1), + await realCurvePool.balances(2), + await realCurvePool.balances(3), + ], + [ + await realCurvePool.coins(0), + await realCurvePool.coins(1), + await realCurvePool.coins(2), + await realCurvePool.coins(3), + ] + ) + ) + + await curvePool.setVirtualPrice(await realCurvePool.get_virtual_price()) // Deploy external cvxMining lib const CvxMiningFactory = await ethers.getContractFactory('CvxMining') @@ -81,19 +133,19 @@ export const makeW3PoolStable = async (): Promise => const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { libraries: { CvxMining: cvxMining.address }, }) - const w3Pool = await wrapperFactory.deploy() - await w3Pool.initialize(THREE_POOL_CVX_POOL_ID) + const wrapper = await wrapperFactory.deploy() + await wrapper.initialize(SUSD_POOL_CVX_POOL_ID) - return { dai, usdc, usdt, curvePool, crv3Pool, w3Pool } + return { dai, usdc, usdt, susd, curvePool, wrapper: wrapper as unknown as RewardableERC4626Vault } } -export interface Wrapped3PoolFixtureVolatile extends WrappedPoolBase { +export interface Wrapped3PoolFixtureVolatile extends CurveBase { usdt: ERC20Mock wbtc: ERC20Mock weth: ERC20Mock } -export const makeW3PoolVolatile = async (): Promise => { +export const makeWTricryptoPoolVolatile = async (): Promise => { // Use real reference ERC20s const usdt = await ethers.getContractAt('ERC20Mock', USDT) const wbtc = await ethers.getContractAt('ERC20Mock', WBTC) @@ -114,9 +166,6 @@ export const makeW3PoolVolatile = async (): Promise ) await curvePool.setVirtualPrice(await realCurvePool.get_virtual_price()) - // Use real Curve/Convex contracts - const crv3Pool = await ethers.getContractAt('ERC20Mock', TRI_CRYPTO_TOKEN) - // Deploy external cvxMining lib const CvxMiningFactory = await ethers.getContractFactory('CvxMining') const cvxMining = await CvxMiningFactory.deploy() @@ -125,25 +174,36 @@ export const makeW3PoolVolatile = async (): Promise const wrapperFactory = await ethers.getContractFactory('ConvexStakingWrapper', { libraries: { CvxMining: cvxMining.address }, }) - const w3Pool = await wrapperFactory.deploy() - await w3Pool.initialize(TRI_CRYPTO_CVX_POOL_ID) - - return { usdt, wbtc, weth, curvePool, crv3Pool, w3Pool } + const wrapper = await wrapperFactory.deploy() + await wrapper.initialize(TRI_CRYPTO_CVX_POOL_ID) + + return { + usdt, + wbtc, + weth, + curvePool, + wrapper: wrapper as unknown as RewardableERC4626Vault, + } } -export const mintW3Pool = async ( - ctx: WrappedPoolBase, +export const mintWPool = async ( + ctx: CurveBase, amount: BigNumberish, user: SignerWithAddress, recipient: string, holder: string ) => { + const cvxWrapper = ctx.wrapper as ConvexStakingWrapper + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await cvxWrapper.curveToken() + ) await whileImpersonating(holder, async (signer) => { - await ctx.crv3Pool.connect(signer).transfer(user.address, amount) + await lpToken.connect(signer).transfer(user.address, amount) }) - await ctx.crv3Pool.connect(user).approve(ctx.w3Pool.address, amount) - await ctx.w3Pool.connect(user).deposit(amount, recipient) + await lpToken.connect(user).approve(ctx.wrapper.address, amount) + await ctx.wrapper.connect(user).deposit(amount, recipient) } export const resetFork = getResetFork(FORK_BLOCK) @@ -224,18 +284,23 @@ export const makeWeUSDFraxBP = async ( } export const mintWeUSDFraxBP = async ( - ctx: WrappedEUSDFraxBPFixture, + ctx: CurveBase, amount: BigNumberish, user: SignerWithAddress, recipient: string, holder: string ) => { + const cvxWrapper = ctx.wrapper as ConvexStakingWrapper + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await cvxWrapper.curveToken() + ) await whileImpersonating(holder, async (signer) => { - await ctx.realMetapool.connect(signer).transfer(user.address, amount) + await lpToken.connect(signer).transfer(user.address, amount) }) - await ctx.realMetapool.connect(user).approve(ctx.wPool.address, amount) - await ctx.wPool.connect(user).deposit(amount, recipient) + await lpToken.connect(user).approve(ctx.wrapper.address, amount) + await ctx.wrapper.connect(user).deposit(amount, recipient) } // === MIM + 3Pool @@ -293,16 +358,21 @@ export const makeWMIM3Pool = async (): Promise => { } export const mintWMIM3Pool = async ( - ctx: WrappedMIM3PoolFixture, + ctx: CurveBase, amount: BigNumberish, user: SignerWithAddress, recipient: string, holder: string ) => { + const cvxWrapper = ctx.wrapper as ConvexStakingWrapper + const lpToken = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', + await cvxWrapper.curveToken() + ) await whileImpersonating(holder, async (signer) => { - await ctx.realMetapool.connect(signer).transfer(user.address, amount) + await lpToken.connect(signer).transfer(user.address, amount) }) - await ctx.realMetapool.connect(user).approve(ctx.wPool.address, amount) - await ctx.wPool.connect(user).deposit(amount, recipient) + await lpToken.connect(user).approve(ctx.wrapper.address, amount) + await ctx.wrapper.connect(user).deposit(amount, recipient) } diff --git a/test/plugins/individual-collateral/curve/pluginTestTypes.ts b/test/plugins/individual-collateral/curve/pluginTestTypes.ts new file mode 100644 index 000000000..230e6cd14 --- /dev/null +++ b/test/plugins/individual-collateral/curve/pluginTestTypes.ts @@ -0,0 +1,89 @@ +import { BigNumberish } from 'ethers' +import { + ConvexStakingWrapper, + CurveGaugeWrapper, + CurvePoolMock, + ERC20Mock, + MockV3Aggregator, + TestICollateral, +} from '../../../../typechain' +import { CollateralOpts } from '../pluginTestTypes' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { CurvePoolType } from './constants' + +type Fixture = () => Promise + +export interface CurveBase { + curvePool: CurvePoolMock + wrapper: CurveGaugeWrapper | ConvexStakingWrapper +} + +// The basic fixture context used in the Curve collateral plugin tests +export interface CurveCollateralFixtureContext extends CurveBase { + alice: SignerWithAddress + collateral: TestICollateral + rewardTokens: ERC20Mock[] // ie [CRV, CVX, FXS] + poolTokens: ERC20Mock[] // ie [USDC, DAI, USDT] + feeds: MockV3Aggregator[] // ie [USDC/USD feed, DAI/USD feed, USDT/USD feed] +} + +// The basic constructor arguments for a Curve collateral plugin -- extension +export interface CurveCollateralOpts extends CollateralOpts { + nTokens?: BigNumberish + curvePool?: string + poolType?: CurvePoolType + feeds?: string[][] + oracleTimeouts?: BigNumberish[][] + oracleErrors?: BigNumberish[][] + lpToken?: string +} + +export interface CurveMetapoolCollateralOpts extends CurveCollateralOpts { + metapoolToken?: string + pairedTokenDefaultThreshold?: BigNumberish +} + +// A function to deploy the collateral plugin and return the deployed instance of the contract +export type DeployCurveCollateralFunc = ( + opts: CurveCollateralOpts +) => Promise<[TestICollateral, CurveCollateralOpts]> + +// A function to deploy and return the plugin-specific test suite context +export type MakeCurveCollateralFixtureFunc = ( + alice: SignerWithAddress, + opts: CurveCollateralOpts +) => Fixture + +// A function to mint a certain amount of collateral to a target address +export type MintCurveCollateralFunc = ( + ctx: CurveCollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => Promise + +// The interface that defines the test suite for the collateral plugin +export interface CurveCollateralTestSuiteFixtures { + // a function to deploy the collateral plugin and return the deployed instance of the contract + deployCollateral: DeployCurveCollateralFunc + + // a group of tests, specific to the collateral plugin, focused on the plugin's constructor + collateralSpecificConstructorTests: () => void + + // a group of tests, specific to the collateral plugin, focused on status checks + collateralSpecificStatusTests: () => void + + // a function to deploy and return the plugin-specific test suite context + makeCollateralFixtureContext: MakeCurveCollateralFixtureFunc + + // a function to mint a certain amount of collateral to a target address + mintCollateralTo: MintCurveCollateralFunc + + isMetapool: boolean + + // a function to reset the fork to a desired block + resetFork: () => void + + // the name of the collateral plugin being tested + collateralName: string +} diff --git a/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts new file mode 100644 index 000000000..4b50a5379 --- /dev/null +++ b/test/plugins/individual-collateral/dsr/SDaiCollateralTestSuite.test.ts @@ -0,0 +1,211 @@ +import collateralTests from '../collateralTests' +import { CollateralFixtureContext, CollateralOpts, MintCollateralFunc } from '../pluginTestTypes' +import { resetFork, mintSDAI } from './helpers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { ContractFactory, BigNumberish, BigNumber } from 'ethers' +import { + ERC20Mock, + IERC20Metadata, + MockV3Aggregator, + MockV3Aggregator__factory, + PotMock, + TestICollateral, +} from '../../../../typechain' +import { bn, fp } from '../../../../common/numbers' +import { ZERO_ADDRESS } from '../../../../common/constants' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { + POT, + SDAI, + ORACLE_ERROR, + ORACLE_TIMEOUT, + PRICE_TIMEOUT, + MAX_TRADE_VOL, + DEFAULT_THRESHOLD, + DELAY_UNTIL_DEFAULT, + DAI_USD_PRICE_FEED, +} from './constants' + +/* + Define deployment functions +*/ + +export const defaultSDaiCollateralOpts: CollateralOpts = { + erc20: SDAI, + targetName: ethers.utils.formatBytes32String('USD'), + rewardERC20: ZERO_ADDRESS, + priceTimeout: PRICE_TIMEOUT, + chainlinkFeed: DAI_USD_PRICE_FEED, + oracleTimeout: ORACLE_TIMEOUT, + oracleError: ORACLE_ERROR, + maxTradeVolume: MAX_TRADE_VOL, + defaultThreshold: DEFAULT_THRESHOLD, + delayUntilDefault: DELAY_UNTIL_DEFAULT, + revenueHiding: fp('0'), +} + +export const deployCollateral = async (opts: CollateralOpts = {}): Promise => { + opts = { ...defaultSDaiCollateralOpts, ...opts } + + const PotFactory: ContractFactory = await ethers.getContractFactory('PotMock') + const pot = await PotFactory.deploy(POT) + + const SDaiCollateralFactory: ContractFactory = await ethers.getContractFactory('SDaiCollateral') + const collateral = await SDaiCollateralFactory.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, + pot.address, + { gasLimit: 2000000000 } + ) + await collateral.deployed() + // sometimes we are trying to test a negative test case and we want this to fail silently + // fortunately this syntax fails silently because our tools are terrible + await expect(collateral.refresh()) + + return collateral +} + +const chainlinkDefaultAnswer = bn('1e8') + +type Fixture = () => Promise + +const makeCollateralFixtureContext = ( + alice: SignerWithAddress, + opts: CollateralOpts = {} +): Fixture => { + const collateralOpts = { ...defaultSDaiCollateralOpts, ...opts } + + const makeCollateralFixtureContext = async () => { + const MockV3AggregatorFactory = ( + await ethers.getContractFactory('MockV3Aggregator') + ) + + const chainlinkFeed = ( + await MockV3AggregatorFactory.deploy(8, chainlinkDefaultAnswer) + ) + collateralOpts.chainlinkFeed = chainlinkFeed.address + + const sdai = (await ethers.getContractAt('IERC20Metadata', SDAI)) as IERC20Metadata + const rewardToken = (await ethers.getContractAt('ERC20Mock', ZERO_ADDRESS)) as ERC20Mock + const collateral = await deployCollateral(collateralOpts) + const tok = await ethers.getContractAt('IERC20Metadata', await collateral.erc20()) + + return { + alice, + collateral, + chainlinkFeed, + tok, + sdai, + rewardToken, + } + } + + return makeCollateralFixtureContext +} + +const mintCollateralTo: MintCollateralFunc = async ( + ctx: CollateralFixtureContext, + amount: BigNumberish, + user: SignerWithAddress, + recipient: string +) => { + await mintSDAI(ctx.tok, user, amount, recipient) +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const reduceTargetPerRef = async () => {} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const increaseTargetPerRef = async () => {} + +// prettier-ignore +const reduceRefPerTok = async ( + ctx: CollateralFixtureContext, + pctDecrease: BigNumberish +) => { + const collateral = await ethers.getContractAt('SDaiCollateral', ctx.collateral.address) + const pot = await ethers.getContractAt('PotMock', await collateral.pot()) + await pot.drip() + + const chi = await pot.chi() + const newChi = chi.sub(chi.mul(bn(pctDecrease)).div(bn('100'))) + await pot.setChi(newChi) +} + +// prettier-ignore +const increaseRefPerTok = async ( + ctx: CollateralFixtureContext, + pctIncrease: BigNumberish + +) => { + const collateral = await ethers.getContractAt('SDaiCollateral', ctx.collateral.address) + const pot = await ethers.getContractAt('PotMock', await collateral.pot()) + await pot.drip() + + const chi = await pot.chi() + const newChi = chi.add(chi.mul(bn(pctIncrease)).div(bn('100'))) + await pot.setChi(newChi) +} + +const getExpectedPrice = async (ctx: CollateralFixtureContext): 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 () => {} + +/* + 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, + collateralName: 'SDaiCollateral', + chainlinkDefaultAnswer, +} + +collateralTests(opts) diff --git a/test/plugins/individual-collateral/dsr/constants.ts b/test/plugins/individual-collateral/dsr/constants.ts new file mode 100644 index 000000000..010a13f76 --- /dev/null +++ b/test/plugins/individual-collateral/dsr/constants.ts @@ -0,0 +1,19 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const RSR = networkConfig['31337'].tokens.RSR as string +export const DAI_USD_PRICE_FEED = networkConfig['31337'].chainlinkFeeds.DAI as string +export const DAI = networkConfig['31337'].tokens.DAI as string +export const SDAI = networkConfig['31337'].tokens.sDAI as string +export const SDAI_HOLDER = '0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906' +export const POT = '0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7' + +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const ORACLE_TIMEOUT = bn(86400) // 24 hours in seconds +export const ORACLE_ERROR = fp('0.0025') // 0.25% +export const DEFAULT_THRESHOLD = fp('0.05') +export const DELAY_UNTIL_DEFAULT = bn(86400) +export const MAX_TRADE_VOL = bn(1000) + +export const FORK_BLOCK = 17439282 diff --git a/test/plugins/individual-collateral/dsr/helpers.ts b/test/plugins/individual-collateral/dsr/helpers.ts new file mode 100644 index 000000000..695dfbfff --- /dev/null +++ b/test/plugins/individual-collateral/dsr/helpers.ts @@ -0,0 +1,19 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { IERC20Metadata } from '../../../../typechain' +import { whileImpersonating } from '../../../utils/impersonation' +import { BigNumberish } from 'ethers' +import { FORK_BLOCK, SDAI_HOLDER } from './constants' +import { getResetFork } from '../helpers' + +export const mintSDAI = async ( + sdai: IERC20Metadata, + account: SignerWithAddress, + amount: BigNumberish, + recipient: string +) => { + await whileImpersonating(SDAI_HOLDER, async (whale) => { + await sdai.connect(whale).transfer(recipient, amount) + }) +} + +export const resetFork = getResetFork(FORK_BLOCK) diff --git a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts index 77c8d1f43..9367ee577 100644 --- a/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts +++ b/test/plugins/individual-collateral/flux-finance/FTokenFiatCollateral.test.ts @@ -8,8 +8,8 @@ import { import { ethers } from 'hardhat' import { ContractFactory, BigNumberish } from 'ethers' import { - CTokenVault, - CTokenVaultMock, + CTokenWrapper, + CTokenWrapperMock, ICToken, MockV3Aggregator, MockV3Aggregator__factory, @@ -119,9 +119,9 @@ all.forEach((curr: FTokenEnumeration) => { if (erc20Address && erc20Address != ZERO_ADDRESS && erc20Address == curr.fToken) { const erc20 = await ethers.getContractAt('ERC20Mock', opts.erc20!) - const CTokenVaultFactory: ContractFactory = await ethers.getContractFactory('CTokenVault') - const fTokenVault = ( - await CTokenVaultFactory.deploy( + const CTokenWrapperFactory: ContractFactory = await ethers.getContractFactory('CTokenWrapper') + const fTokenVault = ( + await CTokenWrapperFactory.deploy( opts.erc20, await erc20.name(), await erc20.symbol(), @@ -176,7 +176,10 @@ all.forEach((curr: FTokenEnumeration) => { collateralOpts.chainlinkFeed = chainlinkFeed.address const collateral = await deployCollateral(collateralOpts) - const erc20 = await ethers.getContractAt('CTokenVault', (await collateral.erc20()) as string) // the fToken + const erc20 = await ethers.getContractAt( + 'CTokenWrapper', + (await collateral.erc20()) as string + ) // the fToken return { alice, @@ -211,8 +214,8 @@ all.forEach((curr: FTokenEnumeration) => { curr.underlying ) - const CTokenVaultMockFactory: ContractFactory = await ethers.getContractFactory( - 'CTokenVaultMock' + const CTokenWrapperMockFactory: ContractFactory = await ethers.getContractFactory( + 'CTokenWrapperMock' ) let compAddress = ZERO_ADDRESS @@ -221,8 +224,8 @@ all.forEach((curr: FTokenEnumeration) => { // eslint-disable-next-line no-empty } catch {} - const fTokenVault = ( - await CTokenVaultMockFactory.deploy( + const fTokenVault = ( + await CTokenWrapperMockFactory.deploy( await underlyingFToken.name(), await underlyingFToken.symbol(), underlyingFToken.address, @@ -252,8 +255,8 @@ all.forEach((curr: FTokenEnumeration) => { user: SignerWithAddress, recipient: string ) => { - const tok = ctx.tok as CTokenVault - const fToken = await ethers.getContractAt('ICToken', await tok.asset()) + const tok = ctx.tok as CTokenWrapper + const fToken = await ethers.getContractAt('ICToken', await tok.underlying()) const underlying = await ethers.getContractAt('IERC20Metadata', await fToken.underlying()) await mintFToken(underlying, curr.holderUnderlying, fToken, tok, amount, recipient) } @@ -272,21 +275,21 @@ all.forEach((curr: FTokenEnumeration) => { const rate = fp('2') const rateAsRefPerTok = rate.div(50) - await (tok as CTokenVaultMock).setExchangeRate(rate) // above current + await (tok as CTokenWrapperMock).setExchangeRate(rate) // above current await collateral.refresh() const before = await collateral.refPerTok() expect(before).to.equal(rateAsRefPerTok.mul(fp('0.99')).div(fp('1'))) expect(await collateral.status()).to.equal(CollateralStatus.SOUND) // Should be SOUND if drops just under 1% - await (tok as CTokenVaultMock).setExchangeRate(rate.mul(fp('0.99001')).div(fp('1'))) + await (tok as CTokenWrapperMock).setExchangeRate(rate.mul(fp('0.99001')).div(fp('1'))) await collateral.refresh() let after = await collateral.refPerTok() expect(before).to.eq(after) expect(await collateral.status()).to.equal(CollateralStatus.SOUND) // Should be DISABLED if drops just over 1% - await (tok as CTokenVaultMock).setExchangeRate(before.mul(fp('0.98999')).div(fp('1'))) + await (tok as CTokenWrapperMock).setExchangeRate(before.mul(fp('0.98999')).div(fp('1'))) await collateral.refresh() after = await collateral.refPerTok() expect(before).to.be.gt(after) diff --git a/test/plugins/individual-collateral/flux-finance/helpers.ts b/test/plugins/individual-collateral/flux-finance/helpers.ts index 9327270bb..74cb8d961 100644 --- a/test/plugins/individual-collateral/flux-finance/helpers.ts +++ b/test/plugins/individual-collateral/flux-finance/helpers.ts @@ -1,4 +1,4 @@ -import { CTokenVault, ICToken, IERC20Metadata } from '../../../../typechain' +import { CTokenWrapper, ICToken, IERC20Metadata } from '../../../../typechain' import { whileImpersonating } from '../../../utils/impersonation' import { BigNumberish } from 'ethers' import { getResetFork } from '../helpers' @@ -8,7 +8,7 @@ export const mintFToken = async ( underlying: IERC20Metadata, holderUnderlying: string, fToken: ICToken, - fTokenVault: CTokenVault, + fTokenVault: CTokenWrapper, amount: BigNumberish, recipient: string ) => { @@ -17,7 +17,7 @@ export const mintFToken = async ( await underlying.connect(signer).approve(fToken.address, balUnderlying) await fToken.connect(signer).mint(balUnderlying) await fToken.connect(signer).approve(fTokenVault.address, amount) - await fTokenVault.connect(signer).mint(amount, recipient) + await fTokenVault.connect(signer).deposit(amount, recipient) }) } diff --git a/test/plugins/individual-collateral/pluginTestTypes.ts b/test/plugins/individual-collateral/pluginTestTypes.ts index a25ea47e1..445a23728 100644 --- a/test/plugins/individual-collateral/pluginTestTypes.ts +++ b/test/plugins/individual-collateral/pluginTestTypes.ts @@ -70,16 +70,16 @@ export interface CollateralTestSuiteFixtures mintCollateralTo: MintCollateralFunc // a function to reduce the value of `targetPerRef` - reduceTargetPerRef: (ctx: T, pctDecrease: BigNumberish) => void + reduceTargetPerRef: (ctx: T, pctDecrease: BigNumberish) => Promise | void // a function to increase the value of `targetPerRef` - increaseTargetPerRef: (ctx: T, pctIncrease: BigNumberish) => void + increaseTargetPerRef: (ctx: T, pctIncrease: BigNumberish) => Promise | void // a function to reduce the value of `refPerTok` - reduceRefPerTok: (ctx: T, pctDecrease: BigNumberish) => void + reduceRefPerTok: (ctx: T, pctDecrease: BigNumberish) => Promise | void // a function to increase the value of `refPerTok` - increaseRefPerTok: (ctx: T, pctIncrease: BigNumberish) => void + increaseRefPerTok: (ctx: T, pctIncrease: BigNumberish) => Promise | void // a function to calculate the expected price (ignoring oracle error) // that should be returned from `plugin.price()` diff --git a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts index 70194a235..21b019a34 100644 --- a/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts +++ b/test/plugins/individual-collateral/rocket-eth/RethCollateralTestSuite.test.ts @@ -273,7 +273,19 @@ const getExpectedPrice = async (ctx: RethCollateralFixtureContext): Promise {} +const collateralSpecificConstructorTests = () => { + it('does not allow missing refPerTok chainlink feed', async () => { + await expect( + deployCollateral({ refPerTokChainlinkFeed: ethers.constants.AddressZero }) + ).to.be.revertedWith('missing refPerTok feed') + }) + + it('does not allow refPerTok oracle timeout at 0', async () => { + await expect(deployCollateral({ refPerTokChainlinkTimeout: 0 })).to.be.revertedWith( + 'refPerTokChainlinkTimeout zero' + ) + }) +} // eslint-disable-next-line @typescript-eslint/no-empty-function const collateralSpecificStatusTests = () => {} diff --git a/test/scenario/ComplexBasket.test.ts b/test/scenario/ComplexBasket.test.ts index 50fbae5bf..c00f465cb 100644 --- a/test/scenario/ComplexBasket.test.ts +++ b/test/scenario/ComplexBasket.test.ts @@ -9,7 +9,7 @@ import { bn, fp, pow10, toBNDecimals } from '../../common/numbers' import { Asset, ComptrollerMock, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FacadeRead, FacadeTest, @@ -83,11 +83,11 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { let usdToken: ERC20Mock let eurToken: ERC20Mock - let cUSDTokenVault: CTokenVaultMock + let cUSDTokenVault: CTokenWrapperMock let aUSDToken: StaticATokenMock let wbtc: ERC20Mock - let cWBTCVault: CTokenVaultMock - let cETHVault: CTokenVaultMock + let cWBTCVault: CTokenWrapperMock + let cETHVault: CTokenWrapperMock let weth: WETH9 @@ -156,7 +156,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { // Setup Factories const ERC20: ContractFactory = await ethers.getContractFactory('ERC20Mock') const WETH: ContractFactory = await ethers.getContractFactory('WETH9') - const CToken: ContractFactory = await ethers.getContractFactory('CTokenVaultMock') + const CToken: ContractFactory = await ethers.getContractFactory('CTokenWrapperMock') const MockV3AggregatorFactory: ContractFactory = await ethers.getContractFactory( 'MockV3Aggregator' ) @@ -241,7 +241,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { collateral.push(await ethers.getContractAt('EURFiatCollateral', fiatEUR)) // 3. CTokenFiatCollateral against USD - cUSDTokenVault = erc20s[4] // cDAI Token + cUSDTokenVault = erc20s[4] // cDAI Token const { collateral: cUSDCollateral } = await hre.run('deploy-ctoken-fiat-collateral', { priceTimeout: PRICE_TIMEOUT.toString(), priceFeed: usdFeed.address, @@ -307,7 +307,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { collateral.push(await ethers.getContractAt('NonFiatCollateral', wBTCCollateral)) // 6. CTokenNonFiatCollateral cWBTCVault against BTC - cWBTCVault = ( + cWBTCVault = ( await CToken.deploy( 'cWBTCVault Token', 'cWBTCVault', @@ -363,7 +363,7 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => { // 8. CTokenSelfReferentialCollateral cETHVault against ETH // Give higher maxTradeVolume: MAX_TRADE_VOLUME.toString(), - cETHVault = ( + cETHVault = ( await CToken.deploy( 'cETHVault Token', 'cETHVault', diff --git a/test/scenario/MaxBasketSize.test.ts b/test/scenario/MaxBasketSize.test.ts index 79c5c1e3b..a3ab63214 100644 --- a/test/scenario/MaxBasketSize.test.ts +++ b/test/scenario/MaxBasketSize.test.ts @@ -10,7 +10,7 @@ import { ATokenFiatCollateral, ComptrollerMock, CTokenFiatCollateral, - CTokenVaultMock, + CTokenWrapperMock, ERC20Mock, FacadeRead, FacadeTest, @@ -211,10 +211,10 @@ describe(`Max Basket Size - P${IMPLEMENTATION}`, () => { return atoken } - const makeCToken = async (tokenName: string): Promise => { + const makeCToken = async (tokenName: string): Promise => { const ERC20MockFactory: ContractFactory = await ethers.getContractFactory('ERC20Mock') - const CTokenVaultMockFactory: ContractFactory = await ethers.getContractFactory( - 'CTokenVaultMock' + const CTokenWrapperMockFactory: ContractFactory = await ethers.getContractFactory( + 'CTokenWrapperMock' ) const CTokenCollateralFactory: ContractFactory = await ethers.getContractFactory( 'CTokenFiatCollateral' @@ -224,8 +224,8 @@ describe(`Max Basket Size - P${IMPLEMENTATION}`, () => { await ERC20MockFactory.deploy(tokenName, `${tokenName} symbol`) ) - const ctoken: CTokenVaultMock = ( - await CTokenVaultMockFactory.deploy( + const ctoken: CTokenWrapperMock = ( + await CTokenWrapperMockFactory.deploy( 'c' + tokenName, `${'c' + tokenName} symbol`, erc20.address, @@ -491,7 +491,7 @@ describe(`Max Basket Size - P${IMPLEMENTATION}`, () => { await assetRegistry.toColl(backing[0]) ) for (let i = maxBasketSize - tokensToDefault; i < backing.length; i++) { - const erc20 = await ethers.getContractAt('CTokenVaultMock', backing[i]) + const erc20 = await ethers.getContractAt('CTokenWrapperMock', backing[i]) // Decrease rate to cause default in Ctoken await erc20.setExchangeRate(fp('0.8')) diff --git a/test/scenario/RevenueHiding.test.ts b/test/scenario/RevenueHiding.test.ts index 603560305..641232860 100644 --- a/test/scenario/RevenueHiding.test.ts +++ b/test/scenario/RevenueHiding.test.ts @@ -7,7 +7,7 @@ import { bn, fp, divCeil } from '../../common/numbers' import { IConfig } from '../../common/configuration' import { CollateralStatus, TradeKind } from '../../common/constants' import { - CTokenVaultMock, + CTokenWrapperMock, CTokenFiatCollateral, ERC20Mock, IAssetRegistry, @@ -44,7 +44,7 @@ describe(`RevenueHiding basket collateral (/w CTokenFiatCollateral) - P${IMPLEME // Tokens and Assets let dai: ERC20Mock let daiCollateral: SelfReferentialCollateral - let cDAI: CTokenVaultMock + let cDAI: CTokenWrapperMock let cDAICollateral: CTokenFiatCollateral // Config values @@ -106,7 +106,7 @@ describe(`RevenueHiding basket collateral (/w CTokenFiatCollateral) - P${IMPLEME // Main ERC20 dai = erc20s[0] daiCollateral = collateral[0] - cDAI = erc20s[4] + cDAI = erc20s[4] cDAICollateral = await ( await ethers.getContractFactory('CTokenFiatCollateral') ).deploy( diff --git a/test/scenario/cETH.test.ts b/test/scenario/cETH.test.ts index 164109c2a..870f11dcc 100644 --- a/test/scenario/cETH.test.ts +++ b/test/scenario/cETH.test.ts @@ -21,7 +21,7 @@ import { TestIRevenueTrader, TestIRToken, WETH9, - CTokenVaultMock, + CTokenWrapperMock, } from '../../typechain' import { advanceTime } from '../utils/time' import { getTrade } from '../utils/trades' @@ -53,7 +53,7 @@ describe(`CToken of self-referential collateral (eg cETH) - P${IMPLEMENTATION}`, // Tokens and Assets let weth: WETH9 let wethCollateral: SelfReferentialCollateral - let cETH: CTokenVaultMock + let cETH: CTokenWrapperMock let cETHCollateral: CTokenSelfReferentialCollateral let token0: CTokenMock let collateral0: Collateral @@ -123,7 +123,7 @@ describe(`CToken of self-referential collateral (eg cETH) - P${IMPLEMENTATION}`, // cETH cETH = await ( - await ethers.getContractFactory('CTokenVaultMock') + await ethers.getContractFactory('CTokenWrapperMock') ).deploy('cETH Token', 'cETH', weth.address, compToken.address, compoundMock.address) cETHCollateral = await ( diff --git a/test/scenario/cWBTC.test.ts b/test/scenario/cWBTC.test.ts index 2754cdce8..35d916c2f 100644 --- a/test/scenario/cWBTC.test.ts +++ b/test/scenario/cWBTC.test.ts @@ -8,7 +8,7 @@ import { advanceTime } from '../utils/time' import { IConfig } from '../../common/configuration' import { CollateralStatus, TradeKind } from '../../common/constants' import { - CTokenVaultMock, + CTokenWrapperMock, CTokenNonFiatCollateral, ComptrollerMock, ERC20Mock, @@ -52,9 +52,9 @@ describe(`CToken of non-fiat collateral (eg cWBTC) - P${IMPLEMENTATION}`, () => // Tokens and Assets let wbtc: ERC20Mock let wBTCCollateral: SelfReferentialCollateral - let cWBTC: CTokenVaultMock + let cWBTC: CTokenWrapperMock let cWBTCCollateral: CTokenNonFiatCollateral - let token0: CTokenVaultMock + let token0: CTokenWrapperMock let collateral0: Collateral let backupToken: ERC20Mock let backupCollateral: Collateral @@ -101,7 +101,7 @@ describe(`CToken of non-fiat collateral (eg cWBTC) - P${IMPLEMENTATION}`, () => } = await loadFixture(defaultFixtureNoBasket)) // Main ERC20 - token0 = erc20s[4] // cDai + token0 = erc20s[4] // cDai collateral0 = collateral[4] wbtc = await (await ethers.getContractFactory('ERC20Mock')).deploy('WBTC Token', 'WBTC') @@ -131,7 +131,7 @@ describe(`CToken of non-fiat collateral (eg cWBTC) - P${IMPLEMENTATION}`, () => // cWBTC cWBTC = await ( - await ethers.getContractFactory('CTokenVaultMock') + await ethers.getContractFactory('CTokenWrapperMock') ).deploy('cWBTC Token', 'cWBTC', wbtc.address, compToken.address, compoundMock.address) cWBTCCollateral = await ( await ethers.getContractFactory('CTokenNonFiatCollateral') diff --git a/test/utils/tokens.ts b/test/utils/tokens.ts index 0d24fba28..3ceebb862 100644 --- a/test/utils/tokens.ts +++ b/test/utils/tokens.ts @@ -1,5 +1,5 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { CTokenVaultMock } from '@typechain/CTokenVaultMock' +import { CTokenWrapperMock } from '@typechain/CTokenWrapperMock' import { ERC20Mock } from '@typechain/ERC20Mock' import { StaticATokenMock } from '@typechain/StaticATokenMock' import { USDCMock } from '@typechain/USDCMock' @@ -18,8 +18,8 @@ export const mintCollaterals = async ( const token2 = ( await ethers.getContractAt('StaticATokenMock', await basket[2].erc20()) ) - const token3 = ( - await ethers.getContractAt('CTokenVaultMock', await basket[3].erc20()) + const token3 = ( + await ethers.getContractAt('CTokenWrapperMock', await basket[3].erc20()) ) for (const recipient of recipients) { diff --git a/utils/subgraph.ts b/utils/subgraph.ts index b276547e2..76cd7b6a4 100644 --- a/utils/subgraph.ts +++ b/utils/subgraph.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { BigNumber, BigNumberish } from 'ethers' import { gql, GraphQLClient } from 'graphql-request' import { useEnv } from './env' diff --git a/yarn.lock b/yarn.lock index f1b32350f..fc614f5e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1029,7 +1029,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/solidity@npm:5.7.0": +"@ethersproject/solidity@npm:5.7.0, @ethersproject/solidity@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/solidity@npm:5.7.0" dependencies: @@ -1110,7 +1110,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/wallet@npm:5.7.0": +"@ethersproject/wallet@npm:5.7.0, @ethersproject/wallet@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/wallet@npm:5.7.0" dependencies: @@ -1753,7 +1753,7 @@ __metadata: languageName: node linkType: hard -"@nomiclabs/hardhat-ethers@npm:^2.2.3": +"@nomiclabs/hardhat-ethers@npm:^2.1.1, @nomiclabs/hardhat-ethers@npm:^2.2.3": version: 2.2.3 resolution: "@nomiclabs/hardhat-ethers@npm:2.2.3" peerDependencies: @@ -1996,6 +1996,25 @@ __metadata: languageName: node linkType: hard +"@tenderly/hardhat-tenderly@npm:^1.7.7": + version: 1.7.7 + resolution: "@tenderly/hardhat-tenderly@npm:1.7.7" + dependencies: + "@ethersproject/bignumber": ^5.7.0 + "@nomiclabs/hardhat-ethers": ^2.1.1 + axios: ^0.27.2 + ethers: ^5.7.0 + fs-extra: ^10.1.0 + hardhat-deploy: ^0.11.14 + js-yaml: ^4.1.0 + tenderly: ^0.5.3 + tslog: ^4.3.1 + peerDependencies: + hardhat: ^2.10.2 + checksum: bd43441c6b121cb23c42f1f02b2aafa8c59c3a8e6b9bdcd980d594b91c15e02d7a239772b705b9cf1c0c292e7e8430a7ea24d7d1af24f54202e979e828bb9be6 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -2252,7 +2271,7 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:^6.2.31": +"@types/qs@npm:^6.2.31, @types/qs@npm:^6.9.7": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" checksum: 7fd6f9c25053e9b5bb6bc9f9f76c1d89e6c04f7707a7ba0e44cc01f17ef5284adb82f230f542c2d5557d69407c9a40f0f3515e8319afd14e1e16b5543ac6cdba @@ -2456,6 +2475,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: ~2.1.34 + negotiator: 0.6.3 + checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 + languageName: node + linkType: hard + "acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -2752,6 +2781,13 @@ __metadata: languageName: node linkType: hard +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: a9925bf3512d9dce202112965de90c222cd59a4fbfce68a0951d25d965cf44642931f40aac72309c41f12df19afa010ecadceb07cfff9ccc1621e99d89ab5f3b + languageName: node + linkType: hard + "array-includes@npm:^3.1.4": version: 3.1.5 resolution: "array-includes@npm:3.1.5" @@ -2899,7 +2935,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^0.21.2": +"axios@npm:^0.21.1, axios@npm:^0.21.2": version: 0.21.4 resolution: "axios@npm:0.21.4" dependencies: @@ -2917,6 +2953,16 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.27.2": + version: 0.27.2 + resolution: "axios@npm:0.27.2" + dependencies: + follow-redirects: ^1.14.9 + form-data: ^4.0.0 + checksum: 38cb7540465fe8c4102850c4368053c21683af85c5fdf0ea619f9628abbcb59415d1e22ebc8a6390d2bbc9b58a9806c874f139767389c862ec9b772235f06854 + languageName: node + linkType: hard + "babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" @@ -3056,6 +3102,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:1.20.1": + version: 1.20.1 + resolution: "body-parser@npm:1.20.1" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.4 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: ~1.6.18 + unpipe: 1.0.0 + checksum: f1050dbac3bede6a78f0b87947a8d548ce43f91ccc718a50dd774f3c81f2d8b04693e52acf62659fad23101827dd318da1fb1363444ff9a8482b886a3e4a5266 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -3448,7 +3514,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:^3.4.0": +"chokidar@npm:3.5.3, chokidar@npm:^3.4.0, chokidar@npm:^3.5.2": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -3555,6 +3621,19 @@ __metadata: languageName: node linkType: hard +"cli-table3@npm:^0.6.2": + version: 0.6.3 + resolution: "cli-table3@npm:0.6.3" + dependencies: + "@colors/colors": 1.5.0 + string-width: ^4.2.0 + dependenciesMeta: + "@colors/colors": + optional: true + checksum: 09897f68467973f827c04e7eaadf13b55f8aec49ecd6647cc276386ea660059322e2dd8020a8b6b84d422dbdd619597046fa89cbbbdc95b2cea149a2df7c096c + languageName: node + linkType: hard + "cli-width@npm:^2.0.0": version: 2.2.1 resolution: "cli-width@npm:2.2.1" @@ -3682,6 +3761,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^9.4.0": + version: 9.5.0 + resolution: "commander@npm:9.5.0" + checksum: c7a3e27aa59e913b54a1bafd366b88650bc41d6651f0cbe258d4ff09d43d6a7394232a4dadd0bf518b3e696fdf595db1028a0d82c785b88bd61f8a440cecfade + languageName: node + linkType: hard + "compare-versions@npm:^5.0.0": version: 5.0.1 resolution: "compare-versions@npm:5.0.1" @@ -3715,6 +3801,22 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: 5.2.1 + checksum: afb9d545e296a5171d7574fcad634b2fdf698875f4006a9dd04a3e1333880c5c0c98d47b560d01216fb6505a54a2ba6a843ee3a02ec86d7e911e8315255f56c3 + languageName: node + linkType: hard + +"content-type@npm:~1.0.4": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 + languageName: node + linkType: hard + "convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.7.0": version: 1.8.0 resolution: "convert-source-map@npm:1.8.0" @@ -3724,6 +3826,20 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: f4e1b0a98a27a0e6e66fd7ea4e4e9d8e038f624058371bf4499cfcd8f3980be9a121486995202ba3fca74fbed93a407d6d54d43a43f96fd28d0bd7a06761591a + languageName: node + linkType: hard + +"cookie@npm:0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 + languageName: node + linkType: hard + "cookie@npm:^0.4.1": version: 0.4.2 resolution: "cookie@npm:0.4.2" @@ -3856,6 +3972,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.9": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: 2.0.0 + checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 + languageName: node + linkType: hard + "debug@npm:3.2.6": version: 3.2.6 resolution: "debug@npm:3.2.6" @@ -3877,15 +4002,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^2.6.0, debug@npm:^2.6.9": - version: 2.6.9 - resolution: "debug@npm:2.6.9" - dependencies: - ms: 2.0.0 - checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 - languageName: node - linkType: hard - "debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -3967,6 +4083,13 @@ __metadata: languageName: node linkType: hard +"define-lazy-prop@npm:^2.0.0": + version: 2.0.0 + resolution: "define-lazy-prop@npm:2.0.0" + checksum: 0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 + languageName: node + linkType: hard + "define-properties@npm:^1.1.2, define-properties@npm:^1.1.3, define-properties@npm:^1.1.4": version: 1.1.4 resolution: "define-properties@npm:1.1.4" @@ -4005,6 +4128,13 @@ __metadata: languageName: node linkType: hard +"destroy@npm:1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 + languageName: node + linkType: hard + "detect-port@npm:^1.3.0": version: 1.3.0 resolution: "detect-port@npm:1.3.0" @@ -4099,6 +4229,13 @@ __metadata: languageName: node linkType: hard +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 1b4cac778d64ce3b582a7e26b218afe07e207a0f9bfe13cc7395a6d307849cfe361e65033c3251e00c27dd060cab43014c2d6b2647676135e18b77d2d05b3f4f + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.172": version: 1.4.180 resolution: "electron-to-chromium@npm:1.4.180" @@ -4142,6 +4279,20 @@ __metadata: languageName: node linkType: hard +"encode-utf8@npm:^1.0.2": + version: 1.0.3 + resolution: "encode-utf8@npm:1.0.3" + checksum: 550224bf2a104b1d355458c8a82e9b4ea07f9fc78387bc3a49c151b940ad26473de8dc9e121eefc4e84561cb0b46de1e4cd2bc766f72ee145e9ea9541482817f + languageName: node + linkType: hard + +"encodeurl@npm:~1.0.2": + version: 1.0.2 + resolution: "encodeurl@npm:1.0.2" + checksum: e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -4151,7 +4302,7 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.0": +"enquirer@npm:^2.3.0, enquirer@npm:^2.3.6": version: 2.3.6 resolution: "enquirer@npm:2.3.6" dependencies: @@ -4248,6 +4399,13 @@ __metadata: languageName: node linkType: hard +"escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 + languageName: node + linkType: hard + "escape-string-regexp@npm:1.0.5, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -4665,6 +4823,13 @@ __metadata: languageName: node linkType: hard +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff + languageName: node + linkType: hard + "eth-gas-reporter@npm:^0.2.24": version: 0.2.25 resolution: "eth-gas-reporter@npm:0.2.25" @@ -4801,7 +4966,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^5.7.1, ethers@npm:^5.7.2": +"ethers@npm:^5.5.3, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -4890,6 +5055,45 @@ __metadata: languageName: node linkType: hard +"express@npm:^4.18.1": + version: 4.18.2 + resolution: "express@npm:4.18.2" + dependencies: + accepts: ~1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: ~1.0.4 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: ~1.1.2 + on-finished: 2.4.1 + parseurl: ~1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: ~2.0.7 + qs: 6.11.0 + range-parser: ~1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: ~1.6.18 + utils-merge: 1.0.1 + vary: ~1.1.2 + checksum: 3c4b9b076879442f6b968fe53d85d9f1eeacbb4f4c41e5f16cc36d77ce39a2b0d81b3f250514982110d815b2f7173f5561367f9110fcc541f9371948e8c8b037 + languageName: node + linkType: hard + "extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -5040,6 +5244,21 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:1.2.0": + version: 1.2.0 + resolution: "finalhandler@npm:1.2.0" + dependencies: + debug: 2.6.9 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + on-finished: 2.4.1 + parseurl: ~1.3.3 + statuses: 2.0.1 + unpipe: ~1.0.0 + checksum: 92effbfd32e22a7dff2994acedbd9bcc3aa646a3e919ea6a53238090e87097f8ef07cced90aa2cc421abdf993aefbdd5b00104d55c7c5479a8d00ed105b45716 + languageName: node + linkType: hard + "find-package-json@npm:^1.2.0": version: 1.2.0 resolution: "find-package-json@npm:1.2.0" @@ -5150,6 +5369,15 @@ __metadata: languageName: node linkType: hard +"fmix@npm:^0.1.0": + version: 0.1.0 + resolution: "fmix@npm:0.1.0" + dependencies: + imul: ^1.0.0 + checksum: c465344d4f169eaf10d45c33949a1e7a633f09dba2ac7063ce8ae8be743df5979d708f7f24900163589f047f5194ac5fc2476177ce31175e8805adfa7b8fb7a4 + languageName: node + linkType: hard + "follow-redirects@npm:^1.12.1, follow-redirects@npm:^1.14.4": version: 1.15.1 resolution: "follow-redirects@npm:1.15.1" @@ -5160,7 +5388,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.14.0": +"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.9": version: 1.15.2 resolution: "follow-redirects@npm:1.15.2" peerDependenciesMeta: @@ -5199,6 +5427,17 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + mime-types: ^2.1.12 + checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c + languageName: node + linkType: hard + "form-data@npm:~2.3.2": version: 2.3.3 resolution: "form-data@npm:2.3.3" @@ -5210,6 +5449,13 @@ __metadata: languageName: node linkType: hard +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: fd27e2394d8887ebd16a66ffc889dc983fbbd797d5d3f01087c020283c0f019a7d05ee85669383d8e0d216b116d720fc0cef2f6e9b7eb9f4c90c6e0bc7fd28e6 + languageName: node + linkType: hard + "fp-ts@npm:1.19.3": version: 1.19.3 resolution: "fp-ts@npm:1.19.3" @@ -5224,6 +5470,13 @@ __metadata: languageName: node linkType: hard +"fresh@npm:0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 + languageName: node + linkType: hard + "fs-extra@npm:^0.30.0": version: 0.30.0 resolution: "fs-extra@npm:0.30.0" @@ -5237,6 +5490,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": + version: 10.1.0 + resolution: "fs-extra@npm:10.1.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: dc94ab37096f813cc3ca12f0f1b5ad6744dfed9ed21e953d72530d103cea193c2f81584a39e9dee1bea36de5ee66805678c0dddc048e8af1427ac19c00fffc50 + languageName: node + linkType: hard + "fs-extra@npm:^7.0.0, fs-extra@npm:^7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" @@ -5692,6 +5956,38 @@ fsevents@~2.1.1: languageName: node linkType: hard +"hardhat-deploy@npm:^0.11.14": + version: 0.11.30 + resolution: "hardhat-deploy@npm:0.11.30" + dependencies: + "@ethersproject/abi": ^5.7.0 + "@ethersproject/abstract-signer": ^5.7.0 + "@ethersproject/address": ^5.7.0 + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/constants": ^5.7.0 + "@ethersproject/contracts": ^5.7.0 + "@ethersproject/providers": ^5.7.2 + "@ethersproject/solidity": ^5.7.0 + "@ethersproject/transactions": ^5.7.0 + "@ethersproject/wallet": ^5.7.0 + "@types/qs": ^6.9.7 + axios: ^0.21.1 + chalk: ^4.1.2 + chokidar: ^3.5.2 + debug: ^4.3.2 + enquirer: ^2.3.6 + ethers: ^5.5.3 + form-data: ^4.0.0 + fs-extra: ^10.0.0 + match-all: ^1.2.6 + murmur-128: ^0.2.1 + qs: ^6.9.4 + zksync-web3: ^0.14.3 + checksum: 7b9ac9d856097be1df88ed86cbec88e5bdeb6258c7167c097d6ad4e80a1131b9288fc7704ff6457253f293f57c9992d83383f15ce8f22190d94966b8bb05d832 + languageName: node + linkType: hard + "hardhat-gas-reporter@npm:^1.0.8": version: 1.0.8 resolution: "hardhat-gas-reporter@npm:1.0.8" @@ -5991,6 +6287,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"hyperlinker@npm:^1.0.0": + version: 1.0.0 + resolution: "hyperlinker@npm:1.0.0" + checksum: f6d020ac552e9d048668206c805a737262b4c395546c773cceea3bc45252c46b4fa6eeb67c5896499dad00d21cb2f20f89fdd480a4529cfa3d012da2957162f9 + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -6057,6 +6360,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"imul@npm:^1.0.0": + version: 1.0.1 + resolution: "imul@npm:1.0.1" + checksum: 6c2af3d5f09e2135e14d565a2c108412b825b221eb2c881f9130467f2adccf7ae201773ae8bcf1be169e2d090567a1fdfa9cf20d3b7da7b9cecb95b920ff3e52 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -6157,6 +6467,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: f88d3825981486f5a1942414c8d77dd6674dd71c065adcfa46f578d677edcb99fda25af42675cb59db492fdf427b34a5abfcde3982da11a8fd83a500b41cfe77 + languageName: node + linkType: hard + "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -6231,6 +6548,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: 3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -6365,6 +6691,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"is-wsl@npm:^2.2.0": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: ^2.0.0 + checksum: 20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 + languageName: node + linkType: hard + "isarray@npm:^1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" @@ -6785,6 +7120,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: df82cd1e172f957bae9c536286265a5cdbd5eeca487cb0a3b2a7b41ef959fc61f8e7c0e9aeea9c114ccf2c166b6a8dd45a46fd619c1c569d210ecd2765ad5169 + languageName: node + linkType: hard + "level-supports@npm:^4.0.0": version: 4.0.1 resolution: "level-supports@npm:4.0.1" @@ -7005,6 +7347,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"match-all@npm:^1.2.6": + version: 1.2.6 + resolution: "match-all@npm:1.2.6" + checksum: 3d4f16b8fd082f2fd10e362f4a8b71c62f8a767591b3db831ca2bdcf726337e9a64e4abc30e2ef053dc2bcfb875a9ed80bd78e006ad5ef11380a7158d0cb00e1 + languageName: node + linkType: hard + "mcl-wasm@npm:^0.7.1": version: 0.7.9 resolution: "mcl-wasm@npm:0.7.9" @@ -7023,6 +7372,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 + languageName: node + linkType: hard + "memory-level@npm:^1.0.0": version: 1.0.0 resolution: "memory-level@npm:1.0.0" @@ -7041,6 +7397,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"merge-descriptors@npm:1.0.1": + version: 1.0.1 + resolution: "merge-descriptors@npm:1.0.1" + checksum: 5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -7055,6 +7418,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a + languageName: node + linkType: hard + "micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -7072,7 +7442,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:~2.1.19": +"mime-types@npm:^2.1.12, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -7081,6 +7451,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: fef25e39263e6d207580bdc629f8872a3f9772c923c7f8c7e793175cee22777bbe8bba95e5d509a40aaa292d8974514ce634ae35769faa45f22d17edda5e8557 + languageName: node + linkType: hard + "mimic-fn@npm:^1.0.0": version: 1.2.0 resolution: "mimic-fn@npm:1.2.0" @@ -7409,6 +7788,17 @@ fsevents@~2.1.1: languageName: node linkType: hard +"murmur-128@npm:^0.2.1": + version: 0.2.1 + resolution: "murmur-128@npm:0.2.1" + dependencies: + encode-utf8: ^1.0.2 + fmix: ^0.1.0 + imul: ^1.0.0 + checksum: 94ff8b39bf1a1a7bde83b6d13f656bbe591e0a5b5ffe4384c39470120ab70e9eadf0af38557742a30d24421ddc63aea6bba1028a1d6b66553038ee86a660dd92 + languageName: node + linkType: hard + "mute-stream@npm:0.0.7": version: 0.0.7 resolution: "mute-stream@npm:0.0.7" @@ -7439,7 +7829,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 @@ -7708,6 +8098,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"on-finished@npm:2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: 1.1.1 + checksum: d20929a25e7f0bb62f937a425b5edeb4e4cde0540d77ba146ec9357f00b0d497cdb3b9b05b9c8e46222407d1548d08166bff69cc56dfa55ba0e4469228920ff0 + languageName: node + linkType: hard + "once@npm:1.x, once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -7726,6 +8125,17 @@ fsevents@~2.1.1: languageName: node linkType: hard +"open@npm:^8.4.0": + version: 8.4.2 + resolution: "open@npm:8.4.2" + dependencies: + define-lazy-prop: ^2.0.0 + is-docker: ^2.1.1 + is-wsl: ^2.2.0 + checksum: 6388bfff21b40cb9bd8f913f9130d107f2ed4724ea81a8fd29798ee322b361ca31fa2cdfb491a5c31e43a3996cfe9566741238c7a741ada8d7af1cb78d85cf26 + languageName: node + linkType: hard + "optionator@npm:^0.8.1, optionator@npm:^0.8.2": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -7880,6 +8290,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 + languageName: node + linkType: hard + "path-exists@npm:^3.0.0": version: 3.0.0 resolution: "path-exists@npm:3.0.0" @@ -7929,6 +8346,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"path-to-regexp@npm:0.1.7": + version: 0.1.7 + resolution: "path-to-regexp@npm:0.1.7" + checksum: 69a14ea24db543e8b0f4353305c5eac6907917031340e5a8b37df688e52accd09e3cebfe1660b70d76b6bd89152f52183f28c74813dbf454ba1a01c82a38abce + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -8122,6 +8546,16 @@ fsevents@~2.1.1: languageName: node linkType: hard +"prompts@npm:^2.4.2": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: ^3.0.3 + sisteransi: ^1.0.5 + checksum: d8fd1fe63820be2412c13bfc5d0a01909acc1f0367e32396962e737cb2fc52d004f3302475d5ce7d18a1e8a79985f93ff04ee03007d091029c3f9104bffc007d + languageName: node + linkType: hard + "proper-lockfile@npm:^4.1.1": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -8133,6 +8567,16 @@ fsevents@~2.1.1: languageName: node linkType: hard +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + checksum: 29c6990ce9364648255454842f06f8c46fcd124d3e6d7c5066df44662de63cdc0bad032e9bf5a3d653ff72141cc7b6019873d685708ac8210c30458ad99f2b74 + languageName: node + linkType: hard + "psl@npm:^1.1.28": version: 1.9.0 resolution: "psl@npm:1.9.0" @@ -8154,7 +8598,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"qs@npm:^6.4.0, qs@npm:^6.7.0": +"qs@npm:6.11.0, qs@npm:^6.4.0, qs@npm:^6.7.0": version: 6.11.0 resolution: "qs@npm:6.11.0" dependencies: @@ -8163,6 +8607,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"qs@npm:^6.9.4": + version: 6.11.2 + resolution: "qs@npm:6.11.2" + dependencies: + side-channel: ^1.0.4 + checksum: e812f3c590b2262548647d62f1637b6989cc56656dc960b893fe2098d96e1bd633f36576f4cd7564dfbff9db42e17775884db96d846bebe4f37420d073ecdc0b + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -8186,7 +8639,14 @@ fsevents@~2.1.1: languageName: node linkType: hard -"raw-body@npm:^2.4.1": +"range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 + languageName: node + linkType: hard + +"raw-body@npm:2.5.1, raw-body@npm:^2.4.1": version: 2.5.1 resolution: "raw-body@npm:2.5.1" dependencies: @@ -8398,6 +8858,7 @@ fsevents@~2.1.1: "@openzeppelin/contracts": ~4.7.3 "@openzeppelin/contracts-upgradeable": ~4.7.3 "@openzeppelin/hardhat-upgrades": ^1.23.0 + "@tenderly/hardhat-tenderly": ^1.7.7 "@typechain/ethers-v5": ^7.2.0 "@typechain/hardhat": ^2.3.1 "@types/chai": ^4.3.0 @@ -8649,7 +9110,7 @@ resolve@1.17.0: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -8749,6 +9210,27 @@ resolve@1.17.0: languageName: node linkType: hard +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: ~1.2.1 + statuses: 2.0.1 + checksum: 74fc07ebb58566b87b078ec63e5a3e41ecd987e4272ba67b7467e86c6ad51bc6b0b0154133b6d8b08a2ddda360464f71382f7ef864700f34844a76c8027817a8 + languageName: node + linkType: hard + "serialize-javascript@npm:6.0.0": version: 6.0.0 resolution: "serialize-javascript@npm:6.0.0" @@ -8758,6 +9240,18 @@ resolve@1.17.0: languageName: node linkType: hard +"serve-static@npm:1.15.0": + version: 1.15.0 + resolution: "serve-static@npm:1.15.0" + dependencies: + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + parseurl: ~1.3.3 + send: 0.18.0 + checksum: af57fc13be40d90a12562e98c0b7855cf6e8bd4c107fe9a45c212bf023058d54a1871b1c89511c3958f70626fff47faeb795f5d83f8cf88514dbaeb2b724464d + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -8871,6 +9365,13 @@ resolve@1.17.0: languageName: node linkType: hard +"sisteransi@npm:^1.0.5": + version: 1.0.5 + resolution: "sisteransi@npm:1.0.5" + checksum: aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -9375,6 +9876,31 @@ resolve@1.17.0: languageName: node linkType: hard +"tenderly@npm:^0.5.3": + version: 0.5.3 + resolution: "tenderly@npm:0.5.3" + dependencies: + axios: ^0.27.2 + cli-table3: ^0.6.2 + commander: ^9.4.0 + express: ^4.18.1 + hyperlinker: ^1.0.0 + js-yaml: ^4.1.0 + open: ^8.4.0 + prompts: ^2.4.2 + tslog: ^4.4.0 + peerDependencies: + ts-node: "*" + typescript: "*" + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + checksum: 167f132d20acb12b841ac29f0e0cdd428e1329fed6c37beaf1e2d05a0ff48ab860b40a734d581d595b053c5bdb80622f3c37409fccba6148af63092a1eb67359 + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -9587,6 +10113,13 @@ resolve@1.17.0: languageName: node linkType: hard +"tslog@npm:^4.3.1, tslog@npm:^4.4.0": + version: 4.8.2 + resolution: "tslog@npm:4.8.2" + checksum: 04d2c2c68be53578b090ff1729b08ee23f3b5c8c509d6ffa332c43d25bcb9599ede8cce1db2c9dcb29ea3647b21c47c1ba11180ee9652a03c901702199dc87cd + languageName: node + linkType: hard + "tsort@npm:0.0.1": version: 0.0.1 resolution: "tsort@npm:0.0.1" @@ -9681,6 +10214,16 @@ resolve@1.17.0: languageName: node linkType: hard +"type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: 0.3.0 + mime-types: ~2.1.24 + checksum: 2c8e47675d55f8b4e404bcf529abdf5036c537a04c2b20177bcf78c9e3c1da69da3942b1346e6edb09e823228c0ee656ef0e033765ec39a70d496ef601a0c657 + languageName: node + linkType: hard + "typechain@npm:^5.2.0": version: 5.2.0 resolution: "typechain@npm:5.2.0" @@ -9813,7 +10356,7 @@ typescript@^4.4.2: languageName: node linkType: hard -"unpipe@npm:1.0.0": +"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" checksum: 4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 @@ -9857,6 +10400,13 @@ typescript@^4.4.2: languageName: node linkType: hard +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: c81095493225ecfc28add49c106ca4f09cdf56bc66731aa8dabc2edbbccb1e1bfe2de6a115e5c6a380d3ea166d1636410b62ef216bb07b3feb1cfde1d95d5080 + languageName: node + linkType: hard + "uuid@npm:2.0.1": version: 2.0.1 resolution: "uuid@npm:2.0.1" @@ -9896,6 +10446,13 @@ typescript@^4.4.2: languageName: node linkType: hard +"vary@npm:~1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b + languageName: node + linkType: hard + "verror@npm:1.10.0": version: 1.10.0 resolution: "verror@npm:1.10.0" @@ -10264,3 +10821,12 @@ typescript@^4.4.2: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"zksync-web3@npm:^0.14.3": + version: 0.14.4 + resolution: "zksync-web3@npm:0.14.4" + peerDependencies: + ethers: ^5.7.0 + checksum: f702a3437f48a8d42c4bb35b8dd13671a168aadfc4e23ce723d62959220ccb6bf9c529c60331fe5b91afaa622147c6a37490551474fe3e35c06ac476524b5160 + languageName: node + linkType: hard