From 9c6460bc412495dc3ee664fef9be09b13cda2a6e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 28 Oct 2024 17:54:18 -0500 Subject: [PATCH] Moonwell EURC (#1189) * CHECKPOINT: Started working on the snARS tests * Fixed the snARS instance tests * Added the snARS deployment script * Addressed review feedback from @jrhea * Reverted erroneous change to the deploy script * Added an instance test for the Moonwell EURC vault * Fixed broken tests * Added the deploy script * Added a test for the Moonwell USDC yield source * Added a Moonwell USDC yield source test and deployment script * Addressed review feedback from @jrhea * Deployed snARs, Moonwell EURC, and Moonwell USDC * Small changes * Redeployed Moonwell EURC and USDC * Re-deployed (and verified) `StkWellHyperdrive` * Removed old snARS deployment * Redeployed the snARS pool * Fixed the issue with the `_withdrawWithBase` * Redeployed the Moonwell contracts * Uncommented deploy script --- .github/workflows/solidity_test.yml | 26 ++ .../templates/instances/Conversions.sol.jinja | 4 +- .../src/instances/erc4626/ERC4626Base.sol | 11 + .../instances/stk-well/StkWellConversions.sol | 4 +- deployments.json | 259 ++++++++++++------ hardhat.config.base.ts | 6 + scripts/deploy.sh | 2 +- tasks/deploy/config/base/index.ts | 3 + .../config/base/moonwell-eurc-182day.ts | 89 ++++++ .../config/base/moonwell-usdc-182day.ts | 89 ++++++ tasks/deploy/config/base/snars-30day.ts | 87 ++++++ tasks/deploy/config/base/stk-well-182day.ts | 8 +- tasks/deploy/lib/constants.ts | 23 +- tasks/deploy/verify.ts | 5 + .../ERC4626HyperdriveInstanceTest.t.sol | 12 +- test/instances/erc4626/MoonwellEURC.t.sol | 131 +++++++++ test/instances/erc4626/MoonwellUSDC.t.sol | 136 +++++++++ test/instances/erc4626/SnARS.t.sol | 154 +++++++++++ test/utils/HyperdriveTest.sol | 39 +-- test/utils/InstanceTest.sol | 31 ++- 20 files changed, 990 insertions(+), 129 deletions(-) create mode 100644 tasks/deploy/config/base/moonwell-eurc-182day.ts create mode 100644 tasks/deploy/config/base/moonwell-usdc-182day.ts create mode 100644 tasks/deploy/config/base/snars-30day.ts create mode 100644 test/instances/erc4626/MoonwellEURC.t.sol create mode 100644 test/instances/erc4626/MoonwellUSDC.t.sol create mode 100644 test/instances/erc4626/SnARS.t.sol diff --git a/.github/workflows/solidity_test.yml b/.github/workflows/solidity_test.yml index 7d33b484b..86d3eccb9 100644 --- a/.github/workflows/solidity_test.yml +++ b/.github/workflows/solidity_test.yml @@ -114,3 +114,29 @@ jobs: - name: test run: FOUNDRY_FUZZ_RUNS=1000 make test-sol-zombie + + test-instances: + name: solidity test core + runs-on: ubuntu-latest-16core + needs: detect-changes + if: needs.detect-changes.outputs.changed == 'true' + env: + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} + BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }} + GNOSIS_CHAIN_RPC_URL: ${{ secrets.GNOSIS_CHAIN_RPC_URL }} + LINEA_RPC_URL: ${{ secrets.LINEA_RPC_URL }} + ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }} + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: install foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: test + run: FOUNDRY_FUZZ_RUNS=10000 make test-sol-instances diff --git a/codegen/templates/instances/Conversions.sol.jinja b/codegen/templates/instances/Conversions.sol.jinja index 761ac2041..0652ad562 100644 --- a/codegen/templates/instances/Conversions.sol.jinja +++ b/codegen/templates/instances/Conversions.sol.jinja @@ -13,7 +13,7 @@ library {{ name.capitalized }}Conversions { /// @return The base amount. function convertToBase( uint256 _shareAmount - ) external view returns (uint256) { + ) internal view returns (uint256) { // FIXME } @@ -22,7 +22,7 @@ library {{ name.capitalized }}Conversions { /// @return The vault shares amount. function convertToShares( uint256 _baseAmount - ) external view returns (uint256) { + ) internal view returns (uint256) { // FIXME } } diff --git a/contracts/src/instances/erc4626/ERC4626Base.sol b/contracts/src/instances/erc4626/ERC4626Base.sol index ce4f42fcf..aeabbc72b 100644 --- a/contracts/src/instances/erc4626/ERC4626Base.sol +++ b/contracts/src/instances/erc4626/ERC4626Base.sol @@ -79,6 +79,9 @@ abstract contract ERC4626Base is HyperdriveBase { address _destination, bytes calldata // unused ) internal override returns (uint256 amountWithdrawn) { + // Get the destination's base balance before the withdrawal. + uint256 baseBalanceBefore = _baseToken.balanceOf(_destination); + // Redeem from the yield source and transfer the // resulting base to the destination address. amountWithdrawn = IERC4626(address(_vaultSharesToken)).redeem( @@ -87,6 +90,14 @@ abstract contract ERC4626Base is HyperdriveBase { address(this) ); + // Ensure that the base was actually received by the destination address. + if ( + _baseToken.balanceOf(_destination) != + baseBalanceBefore + amountWithdrawn + ) { + revert IHyperdrive.UnsupportedToken(); + } + return amountWithdrawn; } diff --git a/contracts/src/instances/stk-well/StkWellConversions.sol b/contracts/src/instances/stk-well/StkWellConversions.sol index 96f6ce8ca..1230759de 100644 --- a/contracts/src/instances/stk-well/StkWellConversions.sol +++ b/contracts/src/instances/stk-well/StkWellConversions.sol @@ -15,7 +15,7 @@ library StkWellConversions { /// @return The base amount. function convertToBase( uint256 _shareAmount - ) external pure returns (uint256) { + ) internal pure returns (uint256) { return _shareAmount; } @@ -26,7 +26,7 @@ library StkWellConversions { /// @return The vault shares amount. function convertToShares( uint256 _baseAmount - ) external pure returns (uint256) { + ) internal pure returns (uint256) { return _baseAmount; } } diff --git a/deployments.json b/deployments.json index 352ca9e4f..9165c8d78 100644 --- a/deployments.json +++ b/deployments.json @@ -1453,81 +1453,6 @@ "address": "0xFcdaF9A4A731C24ed2E1BFd6FA918d9CF7F50137", "timestamp": "2024-09-23T17:24:41.079Z" }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626HyperdriveCoreDeployer": { - "contract": "ERC4626HyperdriveCoreDeployer", - "address": "0xb1c456824cb526a0b57f6cea309785752ec015d7", - "timestamp": "2024-10-09T20:36:51.355Z" - }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target0Deployer": { - "contract": "ERC4626Target0Deployer", - "address": "0xd171c68eaa3fbd25c1fa34c1a5e63e193df39aa6", - "timestamp": "2024-10-09T20:36:58.302Z" - }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target1Deployer": { - "contract": "ERC4626Target1Deployer", - "address": "0xcc2282bf8002701fd31e56d3f6b2c744493ee27f", - "timestamp": "2024-10-09T20:37:03.173Z" - }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target2Deployer": { - "contract": "ERC4626Target2Deployer", - "address": "0xd917235d89a58af93302e9a998d62735740c8117", - "timestamp": "2024-10-09T20:37:06.975Z" - }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target3Deployer": { - "contract": "ERC4626Target3Deployer", - "address": "0x77644b188a5d1d58246a233761273c74b0f9d88d", - "timestamp": "2024-10-09T20:37:10.677Z" - }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target4Deployer": { - "contract": "ERC4626Target4Deployer", - "address": "0xbaa863b62c69993143b013fe7b8af278fd52f76b", - "timestamp": "2024-10-09T20:37:15.186Z" - }, - "ElementDAO ERC4626 Hyperdrive Deployer Coordinator": { - "contract": "ERC4626HyperdriveDeployerCoordinator", - "address": "0x68ba944d89d7481f3a9d73dcb75b7e6c7db5562b", - "timestamp": "2024-10-09T20:37:24.510Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellHyperdriveCoreDeployer": { - "contract": "StkWellHyperdriveCoreDeployer", - "address": "0x8d6d5d48f881bcccd6c09256724692b3a971b87b", - "timestamp": "2024-10-09T20:37:34.640Z" - }, - "StkWellConversions": { - "contract": "StkWellConversions", - "address": "0xe670f140dedcdf142e15ce198e1a99fecd67d21c", - "timestamp": "2024-10-09T20:37:38.315Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget0Deployer": { - "contract": "StkWellTarget0Deployer", - "address": "0xb8bd93848aa71887e447642299b1f2a5cf898a9b", - "timestamp": "2024-10-09T20:37:42.255Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget1Deployer": { - "contract": "StkWellTarget1Deployer", - "address": "0xf74085c4404fd08e7d636f151b734a580f44100f", - "timestamp": "2024-10-09T20:37:46.909Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget2Deployer": { - "contract": "StkWellTarget2Deployer", - "address": "0x57858abb2773fa20de1bac61eda8283a8b1ecea3", - "timestamp": "2024-10-09T20:37:50.544Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget3Deployer": { - "contract": "StkWellTarget3Deployer", - "address": "0xea21fbc9eb872c025959d82383bc898540334484", - "timestamp": "2024-10-09T20:38:07.386Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget4Deployer": { - "contract": "StkWellTarget4Deployer", - "address": "0x02d668b32fc23f223464f3daf9df6e518cf1e9ee", - "timestamp": "2024-10-09T20:38:12.258Z" - }, - "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator": { - "contract": "StkWellHyperdriveDeployerCoordinator", - "address": "0x7d78ee16d314f14e01e222ee6dd839689bd294ca", - "timestamp": "2024-10-09T20:38:16.484Z" - }, "ElementDAO 182 Day Moonwell ETH Hyperdrive_ERC4626Target0": { "contract": "ERC4626Target0", "address": "0x6e596c06Bb5a7eaf654BA14f8352c3257dD8403c", @@ -1558,35 +1483,195 @@ "address": "0xceD9F810098f8329472AEFbaa1112534E96A5c7b", "timestamp": "2024-10-09T20:39:12.262Z" }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellHyperdriveCoreDeployer": { + "contract": "StkWellHyperdriveCoreDeployer", + "address": "0x52f00bd18e53134ed75073e09f48a190559a0fe2", + "timestamp": "2024-10-17T07:38:36.544Z" + }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget0Deployer": { + "contract": "StkWellTarget0Deployer", + "address": "0xcec152d19982ef88e69f538f5a7d06a38ca8773e", + "timestamp": "2024-10-17T07:38:40.474Z" + }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget1Deployer": { + "contract": "StkWellTarget1Deployer", + "address": "0x1a4cee4e32ea51ec7671a0fd7333ca64fbf004f0", + "timestamp": "2024-10-17T07:38:44.527Z" + }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget2Deployer": { + "contract": "StkWellTarget2Deployer", + "address": "0xe299a8c26bf6a7958f1a0aa8ea6fee9b8ede1df9", + "timestamp": "2024-10-17T07:38:48.536Z" + }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget3Deployer": { + "contract": "StkWellTarget3Deployer", + "address": "0x8015beb77685a583751b411f896a8aca05044aa7", + "timestamp": "2024-10-17T07:38:52.814Z" + }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator_StkWellTarget4Deployer": { + "contract": "StkWellTarget4Deployer", + "address": "0x78beeccfeedb99a1a461619c84cc7be78c97de70", + "timestamp": "2024-10-17T07:38:56.558Z" + }, + "ElementDAO Moonwell StkWell Hyperdrive Deployer Coordinator": { + "contract": "StkWellHyperdriveDeployerCoordinator", + "address": "0x948860043742b53f73742c981aebb2242ed0e707", + "timestamp": "2024-10-17T07:39:00.782Z" + }, "ElementDAO 182 Day Moonwell StkWell Hyperdrive_StkWellTarget0": { "contract": "StkWellTarget0", - "address": "0x387948e60f947bd9ee782e33241349bA3aE31aeC", - "timestamp": "2024-10-09T20:39:57.771Z" + "address": "0x6EF7ec1cC1F7bed3b3a2461EE78b2b381dFf152C", + "timestamp": "2024-10-17T07:39:46.558Z" }, "ElementDAO 182 Day Moonwell StkWell Hyperdrive_StkWellTarget1": { "contract": "StkWellTarget1", - "address": "0x730F5A9A4649cD88a91c6B0cDc0111cd7E096eDf", - "timestamp": "2024-10-09T20:39:35.867Z" + "address": "0xdEAC3B00E20a7bc1dee2aF459ee9a3C2e48A5F81", + "timestamp": "2024-10-17T07:39:24.126Z" }, "ElementDAO 182 Day Moonwell StkWell Hyperdrive_StkWellTarget2": { "contract": "StkWellTarget2", - "address": "0xD00dd133937063DbA2028FAb66ebB9B764cfCA5E", - "timestamp": "2024-10-09T20:39:42.386Z" + "address": "0x4cB748492cc5F300abea39E327E21AA88a157a01", + "timestamp": "2024-10-17T07:39:33.666Z" }, "ElementDAO 182 Day Moonwell StkWell Hyperdrive_StkWellTarget3": { "contract": "StkWellTarget3", - "address": "0x8cB7D92029d5369186A789c3dD69a3B4E3cb9d94", - "timestamp": "2024-10-09T20:39:46.388Z" + "address": "0xADD2ec1A20edcd26fb18f30214f48D06d36B9c94", + "timestamp": "2024-10-17T07:39:38.522Z" }, "ElementDAO 182 Day Moonwell StkWell Hyperdrive_StkWellTarget4": { "contract": "StkWellTarget4", - "address": "0xf7b7cCb57e8101cd59f40aF39ceC7f9624EF4d88", - "timestamp": "2024-10-09T20:39:52.240Z" + "address": "0x15C4c91E7b82e725544b412e66863dBf7dc50f83", + "timestamp": "2024-10-17T07:39:43.192Z" }, "ElementDAO 182 Day Moonwell StkWell Hyperdrive": { "contract": "StkWellHyperdrive", - "address": "0x1681aa2E7551343D9dd53C778A83D70243b64360", - "timestamp": "2024-10-09T20:39:57.249Z" + "address": "0x9705c9BC7E9f3da041F23033225c4967d1Aa6385", + "timestamp": "2024-10-17T07:39:46.321Z" + }, + "ElementDAO 30 Day Num Finance snARS Hyperdrive_ERC4626Target0": { + "contract": "ERC4626Target0", + "address": "0xcC36026D325519672d062f233A1FB84a023ad65D", + "timestamp": "2024-10-17T22:44:58.662Z" + }, + "ElementDAO 30 Day Num Finance snARS Hyperdrive_ERC4626Target1": { + "contract": "ERC4626Target1", + "address": "0x59e48b46180cFA53A23884E076d999dAF4C6C174", + "timestamp": "2024-10-17T22:44:42.434Z" + }, + "ElementDAO 30 Day Num Finance snARS Hyperdrive_ERC4626Target2": { + "contract": "ERC4626Target2", + "address": "0x4f06409E9a775F79b2E5970c8C27aA9AEc24E635", + "timestamp": "2024-10-17T22:44:46.336Z" + }, + "ElementDAO 30 Day Num Finance snARS Hyperdrive_ERC4626Target3": { + "contract": "ERC4626Target3", + "address": "0x265aA795D8a52768C67e3eC3e4940565441b4253", + "timestamp": "2024-10-17T22:44:50.576Z" + }, + "ElementDAO 30 Day Num Finance snARS Hyperdrive_ERC4626Target4": { + "contract": "ERC4626Target4", + "address": "0x19328453e9B570a8a4BC047BFAc0a28564edB02e", + "timestamp": "2024-10-17T22:44:54.440Z" + }, + "ElementDAO 30 Day Num Finance snARS Hyperdrive": { + "contract": "ERC4626Hyperdrive", + "address": "0x1243C06146ACa2D4Aaf8F9860F6D8d59d636d46C", + "timestamp": "2024-10-17T22:44:58.410Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626HyperdriveCoreDeployer": { + "contract": "ERC4626HyperdriveCoreDeployer", + "address": "0x7dd0143eb6344808058912434d59df7e6b750b45", + "timestamp": "2024-10-22T00:16:14.719Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target0Deployer": { + "contract": "ERC4626Target0Deployer", + "address": "0xa35411b03078ef2edc254d1a2f0497d77c5b3e1d", + "timestamp": "2024-10-22T00:16:19.357Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target1Deployer": { + "contract": "ERC4626Target1Deployer", + "address": "0x30f48d0cace859fa633031035bf2395bbbfe030a", + "timestamp": "2024-10-22T00:16:27.301Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target2Deployer": { + "contract": "ERC4626Target2Deployer", + "address": "0x0a97d6e87661c9158e1dc4fde66e98ea96fccde0", + "timestamp": "2024-10-22T00:16:32.101Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target3Deployer": { + "contract": "ERC4626Target3Deployer", + "address": "0x144f74f2b663ab5030fb5e6f24bada13441aa4b3", + "timestamp": "2024-10-22T00:16:36.644Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator_ERC4626Target4Deployer": { + "contract": "ERC4626Target4Deployer", + "address": "0xb3f2380f7f9f78112ab152c486d27222bff4673c", + "timestamp": "2024-10-22T00:16:41.210Z" + }, + "ElementDAO ERC4626 Hyperdrive Deployer Coordinator": { + "contract": "ERC4626HyperdriveDeployerCoordinator", + "address": "0xfa8a5165219c5bd7992038202bf504110de61efd", + "timestamp": "2024-10-22T00:16:44.039Z" + }, + "ElementDAO 182 Day Moonwell EURC Hyperdrive_ERC4626Target0": { + "contract": "ERC4626Target0", + "address": "0x207deD1eEDE260Fd40760e8A230B51aE27B44dD3", + "timestamp": "2024-10-22T00:17:29.891Z" + }, + "ElementDAO 182 Day Moonwell EURC Hyperdrive_ERC4626Target1": { + "contract": "ERC4626Target1", + "address": "0x34542Df0D0dB6524A7eBbb504d4a607b802Fad70", + "timestamp": "2024-10-22T00:17:07.906Z" + }, + "ElementDAO 182 Day Moonwell EURC Hyperdrive_ERC4626Target2": { + "contract": "ERC4626Target2", + "address": "0x66264C6707E8d7eAb8f7a5E0bb522423c4E8A6a0", + "timestamp": "2024-10-22T00:17:12.562Z" + }, + "ElementDAO 182 Day Moonwell EURC Hyperdrive_ERC4626Target3": { + "contract": "ERC4626Target3", + "address": "0xfd10C9d2695Dc78AB96fF75828E6beA438969518", + "timestamp": "2024-10-22T00:17:20.512Z" + }, + "ElementDAO 182 Day Moonwell EURC Hyperdrive_ERC4626Target4": { + "contract": "ERC4626Target4", + "address": "0x0827943d58aB22dbE79A1FE2AB158480cb4F0D9A", + "timestamp": "2024-10-22T00:17:25.158Z" + }, + "ElementDAO 182 Day Moonwell EURC Hyperdrive": { + "contract": "ERC4626Hyperdrive", + "address": "0xdd8E1B14A04cbdD98dfcAF3F0Db84A80Bfb8FC25", + "timestamp": "2024-10-22T00:17:29.739Z" + }, + "ElementDAO 182 Day Moonwell USDC Hyperdrive_ERC4626Target0": { + "contract": "ERC4626Target0", + "address": "0x5580845D3F597A9A309a4448924c270ea0f4c736", + "timestamp": "2024-10-22T00:18:26.594Z" + }, + "ElementDAO 182 Day Moonwell USDC Hyperdrive_ERC4626Target1": { + "contract": "ERC4626Target1", + "address": "0x5Db16781a29549d708c4c34bF2771C1b34415c21", + "timestamp": "2024-10-22T00:17:53.152Z" + }, + "ElementDAO 182 Day Moonwell USDC Hyperdrive_ERC4626Target2": { + "contract": "ERC4626Target2", + "address": "0x257411570Bc419B1B13AEcb5Fe83eba6e2903CeA", + "timestamp": "2024-10-22T00:18:01.120Z" + }, + "ElementDAO 182 Day Moonwell USDC Hyperdrive_ERC4626Target3": { + "contract": "ERC4626Target3", + "address": "0x56D2430eE396d3f4811781B146D66361206BA3Dc", + "timestamp": "2024-10-22T00:18:15.715Z" + }, + "ElementDAO 182 Day Moonwell USDC Hyperdrive_ERC4626Target4": { + "contract": "ERC4626Target4", + "address": "0x0a4d43365229F46250fdBaAece73B6bce28e925F", + "timestamp": "2024-10-22T00:18:23.639Z" + }, + "ElementDAO 182 Day Moonwell USDC Hyperdrive": { + "contract": "ERC4626Hyperdrive", + "address": "0xD9b66D9a819B36ECEfC26B043eF3B422d5A6123a", + "timestamp": "2024-10-22T00:18:26.463Z" } } } \ No newline at end of file diff --git a/hardhat.config.base.ts b/hardhat.config.base.ts index 601c4c9ca..3cf1896f6 100644 --- a/hardhat.config.base.ts +++ b/hardhat.config.base.ts @@ -12,7 +12,10 @@ import { BASE_ERC4626_COORDINATOR, BASE_FACTORY, BASE_MOONWELL_ETH_182DAY, + BASE_MOONWELL_EURC_182DAY, + BASE_MOONWELL_USDC_182DAY, BASE_MORPHO_BLUE_COORDINATOR, + BASE_SNARS_30DAY, BASE_STK_WELL_182DAY, BASE_STK_WELL_COORDINATOR, } from "./tasks/deploy/config/base"; @@ -40,6 +43,9 @@ const config: HardhatUserConfig = { BASE_MORPHO_BLUE_CBETH_USDC_182DAY, BASE_MOONWELL_ETH_182DAY, BASE_STK_WELL_182DAY, + BASE_SNARS_30DAY, + BASE_MOONWELL_EURC_182DAY, + BASE_MOONWELL_USDC_182DAY, ], checkpointRewarders: [], checkpointSubrewarders: [], diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 46a74f046..a50dc5ac1 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -15,4 +15,4 @@ if [[ "${NETWORK}" == "hardhat" ]]; then config_filename="hardhat.config.ts" fi npx hardhat deploy:hyperdrive --show-stack-traces --network ${NETWORK} --config "$config_filename" -# npx hardhat deploy:verify --show-stack-traces --network ${NETWORK} --config "$config_filename" +npx hardhat deploy:verify --show-stack-traces --network ${NETWORK} --config "$config_filename" diff --git a/tasks/deploy/config/base/index.ts b/tasks/deploy/config/base/index.ts index 7dc73c851..d5854c187 100644 --- a/tasks/deploy/config/base/index.ts +++ b/tasks/deploy/config/base/index.ts @@ -3,6 +3,9 @@ export * from "./chainlink-coordinator"; export * from "./erc4626-coordinator"; export * from "./factory"; export * from "./moonwell-eth-182day"; +export * from "./moonwell-eurc-182day"; +export * from "./moonwell-usdc-182day"; export * from "./morpho-blue-coordinator"; +export * from "./snars-30day"; export * from "./stk-well-182day"; export * from "./stk-well-coordinator"; diff --git a/tasks/deploy/config/base/moonwell-eurc-182day.ts b/tasks/deploy/config/base/moonwell-eurc-182day.ts new file mode 100644 index 000000000..511d344e7 --- /dev/null +++ b/tasks/deploy/config/base/moonwell-eurc-182day.ts @@ -0,0 +1,89 @@ +import { Address, keccak256, parseEther, toBytes } from "viem"; +import { + HyperdriveInstanceConfig, + getLinkerDetails, + normalizeFee, + parseDuration, + toBytes32, +} from "../../lib"; +import { + EURC_ADDRESS_BASE, + MOONWELL_EURC_ADDRESS_BASE, + SIX_MONTHS, +} from "../../lib/constants"; +import { BASE_ERC4626_COORDINATOR_NAME } from "./erc4626-coordinator"; +import { BASE_FACTORY_NAME } from "./factory"; + +// The name of the pool. +export const BASE_MOONWELL_EURC_182DAY_NAME = + "ElementDAO 182 Day Moonwell EURC Hyperdrive"; + +// EURC only has 6 decimals. +const CONTRIBUTION = 100_000_000n; + +export const BASE_MOONWELL_EURC_182DAY: HyperdriveInstanceConfig<"ERC4626"> = { + name: BASE_MOONWELL_EURC_182DAY_NAME, + prefix: "ERC4626", + coordinatorAddress: async (hre) => + hre.hyperdriveDeploy.deployments.byName(BASE_ERC4626_COORDINATOR_NAME) + .address, + deploymentId: keccak256(toBytes(BASE_MOONWELL_EURC_182DAY_NAME + "2")), + salt: toBytes32("0x69422"), + extraData: "0x", + contribution: CONTRIBUTION, + // The moonwell EURC rate is currently at 19.4%, but we have a cap on our + // fixed rate of 10%. This is the link to the vaults page: https://moonwell.fi/vaults + fixedAPR: parseEther("0.1"), + timestretchAPR: parseEther("0.075"), + options: async (hre) => ({ + extraData: "0x", + asBase: true, + destination: (await hre.getNamedAccounts())["deployer"] as Address, + }), + // Prepare to deploy the contract by setting approvals. + prepare: async (hre) => { + let baseToken = await hre.viem.getContractAt( + "contracts/src/interfaces/IERC20.sol:IERC20", + EURC_ADDRESS_BASE, + ); + let tx = await baseToken.write.approve([ + hre.hyperdriveDeploy.deployments.byName( + BASE_ERC4626_COORDINATOR_NAME, + ).address, + CONTRIBUTION, + ]); + let pc = await hre.viem.getPublicClient(); + await pc.waitForTransactionReceipt({ hash: tx }); + }, + poolDeployConfig: async (hre) => { + let factoryContract = await hre.viem.getContractAt( + "HyperdriveFactory", + hre.hyperdriveDeploy.deployments.byName(BASE_FACTORY_NAME).address, + ); + return { + baseToken: EURC_ADDRESS_BASE, + vaultSharesToken: MOONWELL_EURC_ADDRESS_BASE, + circuitBreakerDelta: parseEther("0.075"), + minimumShareReserves: parseEther("0.001"), + minimumTransactionAmount: 1_000_000n, + positionDuration: parseDuration(SIX_MONTHS), + checkpointDuration: parseDuration("1 day"), + timeStretch: 0n, + governance: await factoryContract.read.governance(), + feeCollector: await factoryContract.read.feeCollector(), + sweepCollector: await factoryContract.read.sweepCollector(), + checkpointRewarder: await factoryContract.read.checkpointRewarder(), + ...(await getLinkerDetails( + hre, + hre.hyperdriveDeploy.deployments.byName(BASE_FACTORY_NAME) + .address, + )), + fees: { + curve: parseEther("0.01"), + flat: normalizeFee(parseEther("0.0005"), SIX_MONTHS), + governanceLP: parseEther("0.15"), + governanceZombie: parseEther("0.03"), + }, + }; + }, +}; diff --git a/tasks/deploy/config/base/moonwell-usdc-182day.ts b/tasks/deploy/config/base/moonwell-usdc-182day.ts new file mode 100644 index 000000000..f5e6fc981 --- /dev/null +++ b/tasks/deploy/config/base/moonwell-usdc-182day.ts @@ -0,0 +1,89 @@ +import { Address, keccak256, parseEther, toBytes } from "viem"; +import { + HyperdriveInstanceConfig, + getLinkerDetails, + normalizeFee, + parseDuration, + toBytes32, +} from "../../lib"; +import { + MOONWELL_USDC_ADDRESS_BASE, + SIX_MONTHS, + USDC_ADDRESS_BASE, +} from "../../lib/constants"; +import { BASE_ERC4626_COORDINATOR_NAME } from "./erc4626-coordinator"; +import { BASE_FACTORY_NAME } from "./factory"; + +// The name of the pool. +export const BASE_MOONWELL_USDC_182DAY_NAME = + "ElementDAO 182 Day Moonwell USDC Hyperdrive"; + +// USDC only has 6 decimals. +const CONTRIBUTION = 100_000_000n; + +export const BASE_MOONWELL_USDC_182DAY: HyperdriveInstanceConfig<"ERC4626"> = { + name: BASE_MOONWELL_USDC_182DAY_NAME, + prefix: "ERC4626", + coordinatorAddress: async (hre) => + hre.hyperdriveDeploy.deployments.byName(BASE_ERC4626_COORDINATOR_NAME) + .address, + deploymentId: keccak256(toBytes(BASE_MOONWELL_USDC_182DAY_NAME + "2")), + salt: toBytes32("0x69422"), + extraData: "0x", + contribution: CONTRIBUTION, + // The moonwell EURC rate is currently at 12%, but we have a cap on our + // fixed rate of 10%. This is the link to the vaults page: https://moonwell.fi/vaults + fixedAPR: parseEther("0.1"), + timestretchAPR: parseEther("0.075"), + options: async (hre) => ({ + extraData: "0x", + asBase: true, + destination: (await hre.getNamedAccounts())["deployer"] as Address, + }), + // Prepare to deploy the contract by setting approvals. + prepare: async (hre) => { + let baseToken = await hre.viem.getContractAt( + "contracts/src/interfaces/IERC20.sol:IERC20", + USDC_ADDRESS_BASE, + ); + let tx = await baseToken.write.approve([ + hre.hyperdriveDeploy.deployments.byName( + BASE_ERC4626_COORDINATOR_NAME, + ).address, + CONTRIBUTION, + ]); + let pc = await hre.viem.getPublicClient(); + await pc.waitForTransactionReceipt({ hash: tx }); + }, + poolDeployConfig: async (hre) => { + let factoryContract = await hre.viem.getContractAt( + "HyperdriveFactory", + hre.hyperdriveDeploy.deployments.byName(BASE_FACTORY_NAME).address, + ); + return { + baseToken: USDC_ADDRESS_BASE, + vaultSharesToken: MOONWELL_USDC_ADDRESS_BASE, + circuitBreakerDelta: parseEther("0.075"), + minimumShareReserves: parseEther("0.001"), + minimumTransactionAmount: 1_000_000n, + positionDuration: parseDuration(SIX_MONTHS), + checkpointDuration: parseDuration("1 day"), + timeStretch: 0n, + governance: await factoryContract.read.governance(), + feeCollector: await factoryContract.read.feeCollector(), + sweepCollector: await factoryContract.read.sweepCollector(), + checkpointRewarder: await factoryContract.read.checkpointRewarder(), + ...(await getLinkerDetails( + hre, + hre.hyperdriveDeploy.deployments.byName(BASE_FACTORY_NAME) + .address, + )), + fees: { + curve: parseEther("0.01"), + flat: normalizeFee(parseEther("0.0005"), SIX_MONTHS), + governanceLP: parseEther("0.15"), + governanceZombie: parseEther("0.03"), + }, + }; + }, +}; diff --git a/tasks/deploy/config/base/snars-30day.ts b/tasks/deploy/config/base/snars-30day.ts new file mode 100644 index 000000000..d6531b061 --- /dev/null +++ b/tasks/deploy/config/base/snars-30day.ts @@ -0,0 +1,87 @@ +import { Address, keccak256, parseEther, toBytes } from "viem"; +import { + HyperdriveInstanceConfig, + getLinkerDetails, + normalizeFee, + parseDuration, + toBytes32, +} from "../../lib"; +import { + NARS_ADDRESS_BASE, + ONE_MONTH, + SNARS_ADDRESS_BASE, +} from "../../lib/constants"; +import { BASE_ERC4626_COORDINATOR_NAME } from "./erc4626-coordinator"; +import { BASE_FACTORY_NAME } from "./factory"; + +// The name of the pool. +export const BASE_SNARS_30DAY_NAME = + "ElementDAO 30 Day Num Finance snARS Hyperdrive"; + +// The initial contribution of the pool. +const CONTRIBUTION = parseEther("91000"); + +export const BASE_SNARS_30DAY: HyperdriveInstanceConfig<"ERC4626"> = { + name: BASE_SNARS_30DAY_NAME, + prefix: "ERC4626", + coordinatorAddress: async (hre) => + hre.hyperdriveDeploy.deployments.byName(BASE_ERC4626_COORDINATOR_NAME) + .address, + deploymentId: keccak256(toBytes(BASE_SNARS_30DAY_NAME + "1")), + salt: toBytes32("0x69421"), + extraData: "0x", + contribution: CONTRIBUTION, + fixedAPR: parseEther("0.1"), + timestretchAPR: parseEther("0.15"), + options: async (hre) => ({ + extraData: "0x", + asBase: true, + destination: (await hre.getNamedAccounts())["deployer"] as Address, + }), + // Prepare to deploy the contract by setting approvals. + prepare: async (hre) => { + let baseToken = await hre.viem.getContractAt( + "contracts/src/interfaces/IERC20.sol:IERC20", + NARS_ADDRESS_BASE, + ); + let tx = await baseToken.write.approve([ + hre.hyperdriveDeploy.deployments.byName( + BASE_ERC4626_COORDINATOR_NAME, + ).address, + CONTRIBUTION, + ]); + let pc = await hre.viem.getPublicClient(); + await pc.waitForTransactionReceipt({ hash: tx }); + }, + poolDeployConfig: async (hre) => { + let factoryContract = await hre.viem.getContractAt( + "HyperdriveFactory", + hre.hyperdriveDeploy.deployments.byName(BASE_FACTORY_NAME).address, + ); + return { + baseToken: NARS_ADDRESS_BASE, + vaultSharesToken: SNARS_ADDRESS_BASE, + circuitBreakerDelta: parseEther("0.075"), + minimumShareReserves: parseEther("0.001"), + minimumTransactionAmount: parseEther("0.001"), + positionDuration: parseDuration(ONE_MONTH), + checkpointDuration: parseDuration("1 day"), + timeStretch: 0n, + governance: await factoryContract.read.governance(), + feeCollector: await factoryContract.read.feeCollector(), + sweepCollector: await factoryContract.read.sweepCollector(), + checkpointRewarder: await factoryContract.read.checkpointRewarder(), + ...(await getLinkerDetails( + hre, + hre.hyperdriveDeploy.deployments.byName(BASE_FACTORY_NAME) + .address, + )), + fees: { + curve: parseEther("0.01"), + flat: normalizeFee(parseEther("0.0005"), ONE_MONTH), + governanceLP: parseEther("0.15"), + governanceZombie: parseEther("0.03"), + }, + }; + }, +}; diff --git a/tasks/deploy/config/base/stk-well-182day.ts b/tasks/deploy/config/base/stk-well-182day.ts index a2730fb9c..afe004441 100644 --- a/tasks/deploy/config/base/stk-well-182day.ts +++ b/tasks/deploy/config/base/stk-well-182day.ts @@ -15,8 +15,8 @@ import { BASE_STK_WELL_COORDINATOR_NAME } from "./stk-well-coordinator"; export const BASE_STK_WELL_182DAY_NAME = "ElementDAO 182 Day Moonwell StkWell Hyperdrive"; -// WELL is currently worth ~$0.03, so this is a contribution of around $80. -const CONTRIBUTION = parseEther("2700"); +// WELL is currently worth ~$0.05, so this is a contribution of around $80. +const CONTRIBUTION = parseEther("1500"); export const BASE_STK_WELL_182DAY: HyperdriveInstanceConfig<"StkWell"> = { name: BASE_STK_WELL_182DAY_NAME, @@ -24,8 +24,8 @@ export const BASE_STK_WELL_182DAY: HyperdriveInstanceConfig<"StkWell"> = { coordinatorAddress: async (hre) => hre.hyperdriveDeploy.deployments.byName(BASE_STK_WELL_COORDINATOR_NAME) .address, - deploymentId: keccak256(toHex(BASE_STK_WELL_182DAY_NAME)), - salt: toBytes32("0x42080085"), + deploymentId: keccak256(toHex(BASE_STK_WELL_182DAY_NAME + "1")), + salt: toBytes32("0x42080086"), extraData: "0x", contribution: CONTRIBUTION, // NOTE: The latest variable rate on Moonwell's Staked Well market is diff --git a/tasks/deploy/lib/constants.ts b/tasks/deploy/lib/constants.ts index 1fbfe748b..c7966a85d 100644 --- a/tasks/deploy/lib/constants.ts +++ b/tasks/deploy/lib/constants.ts @@ -158,9 +158,27 @@ export const CBETH_ADDRESS_BASE = export const CHAINLINK_AGGREGATOR_CBETH_ETH_PROXY_BASE = "0x868a501e68F3D1E89CfC0D22F6b22E8dabce5F04" as Address; +export const EURC_ADDRESS_BASE = + "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42" as Address; + export const MOONWELL_ETH_ADDRESS_BASE = "0xa0E430870c4604CcfC7B38Ca7845B1FF653D0ff1" as Address; +export const MOONWELL_EURC_ADDRESS_BASE = + "0xf24608E0CCb972b0b0f4A6446a0BBf58c701a026" as Address; + +export const MOONWELL_USDC_ADDRESS_BASE = + "0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca" as Address; + +export const NARS_ADDRESS_BASE = + "0x5e40f26E89213660514c51Fb61b2d357DBf63C85" as Address; + +export const SNARS_ADDRESS_BASE = + "0xC1F4C75e8925A67BE4F35D6b1c044B5ea8849a58" as Address; + +export const STK_WELL_ADDRESS_BASE = + "0xe66E3A37C3274Ac24FE8590f7D84A2427194DC17" as Address; + export const USDC_ADDRESS_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address; @@ -170,9 +188,6 @@ export const WETH_ADDRESS_BASE = export const WELL_ADDRESS_BASE = "0xA88594D404727625A9437C3f886C7643872296AE" as Address; -export const STK_WELL_ADDRESS_BASE = - "0xe66E3A37C3274Ac24FE8590f7D84A2427194DC17" as Address; - // ╭─────────────────────────────────────────────────────────╮ // │ Base Whales │ // ╰─────────────────────────────────────────────────────────╯ @@ -210,6 +225,8 @@ export const WSTETH_WHALE_GNOSIS = // │ Durations │ // ╰─────────────────────────────────────────────────────────╯ +export const ONE_MONTH = "30 days"; + export const THREE_MONTHS = "91 days"; export const SIX_MONTHS = "182 days"; diff --git a/tasks/deploy/verify.ts b/tasks/deploy/verify.ts index e23d5f581..57e8f3cdb 100644 --- a/tasks/deploy/verify.ts +++ b/tasks/deploy/verify.ts @@ -251,6 +251,9 @@ task( ]; } else if (extras) { targetArgs = [poolConfig, factoryAddress, ...extras]; + } else { + console.log("got here"); + targetArgs = [poolConfig, factoryAddress]; } // verify the targets @@ -322,6 +325,8 @@ task( pathName = "ezeth-linea"; } else if (kind == "RsETHLineaHyperdrive") { pathName = "rseth-linea"; + } else if (kind == "StkWellHyperdrive") { + pathName = "stk-well"; } else { pathName = instanceConfig.prefix.toLowerCase(); } diff --git a/test/instances/erc4626/ERC4626HyperdriveInstanceTest.t.sol b/test/instances/erc4626/ERC4626HyperdriveInstanceTest.t.sol index fe5a50d24..ea04b0c52 100644 --- a/test/instances/erc4626/ERC4626HyperdriveInstanceTest.t.sol +++ b/test/instances/erc4626/ERC4626HyperdriveInstanceTest.t.sol @@ -83,7 +83,13 @@ abstract contract ERC4626HyperdriveInstanceTest is InstanceTest { /// @dev Fetches the total supply of the base and share tokens. /// @return The total supply of base. /// @return The total supply of vault shares. - function getSupply() internal view override returns (uint256, uint256) { + function getSupply() + internal + view + virtual + override + returns (uint256, uint256) + { return ( IERC4626(address(config.vaultSharesToken)).totalAssets(), IERC4626(address(config.vaultSharesToken)).totalSupply() @@ -118,11 +124,11 @@ abstract contract ERC4626HyperdriveInstanceTest is InstanceTest { /// @dev Fuzz test that verifies that the vault share price is the price /// that dictates the conversion between base and shares. /// @param basePaid the fuzz parameter for the base paid. - function test__pricePerVaultShare(uint256 basePaid) external { + function test__pricePerVaultShare(uint256 basePaid) external virtual { // Ensure that the share price is the expected value. (uint256 totalBase, uint256 totalSupply) = getSupply(); uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; - assertEq(vaultSharePrice, totalBase.divDown(totalSupply)); + assertApproxEqAbs(vaultSharePrice, totalBase.divDown(totalSupply), 1); // Ensure that the share price accurately predicts the amount of shares // that will be minted for depositing a given amount of shares. This will diff --git a/test/instances/erc4626/MoonwellEURC.t.sol b/test/instances/erc4626/MoonwellEURC.t.sol new file mode 100644 index 000000000..596c9f2ec --- /dev/null +++ b/test/instances/erc4626/MoonwellEURC.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { IERC20 } from "../../../contracts/src/interfaces/IERC20.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { FixedPointMath } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { InstanceTest } from "../../utils/InstanceTest.sol"; +import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol"; +import { Lib } from "../../utils/Lib.sol"; +import { MetaMorphoHyperdriveInstanceTest } from "./MetaMorphoHyperdriveInstanceTest.t.sol"; + +contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// @dev The EURC contract. + IERC20 internal constant EURC = + IERC20(0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42); + + /// @dev The Moonwell EURC contract. + IERC20 internal constant MWEURC = + IERC20(0xf24608E0CCb972b0b0f4A6446a0BBf58c701a026); + + /// @dev Whale accounts. + address internal EURC_TOKEN_WHALE = + address(0x9DFf4b5AE4fD673213502Ab8fbf6d36015efb3E1); + address[] internal baseTokenWhaleAccounts = [EURC_TOKEN_WHALE]; + address internal MWEURC_TOKEN_WHALE = + address(0xb554B9856DFdbf52B98E0e4D2b981C34E20e1dAB); + address[] internal vaultSharesTokenWhaleAccounts = [MWEURC_TOKEN_WHALE]; + + /// @notice Instantiates the instance testing suite with the configuration. + constructor() + InstanceTest( + InstanceTestConfig({ + name: "Hyperdrive", + kind: "ERC4626Hyperdrive", + decimals: 6, + baseTokenWhaleAccounts: baseTokenWhaleAccounts, + vaultSharesTokenWhaleAccounts: vaultSharesTokenWhaleAccounts, + baseToken: EURC, + vaultSharesToken: MWEURC, + shareTolerance: 1e3, + minimumShareReserves: 1e3, + minimumTransactionAmount: 1e3, + positionDuration: POSITION_DURATION, + fees: IHyperdrive.Fees({ + curve: 0.001e18, + flat: 0.0001e18, + governanceLP: 0, + governanceZombie: 0 + }), + enableBaseDeposits: true, + enableShareDeposits: true, + enableBaseWithdraws: true, + enableShareWithdraws: true, + baseWithdrawError: new bytes(0), + isRebasing: false, + shouldAccrueInterest: true, + // The base test tolerances. + closeLongWithBaseTolerance: 20, + roundTripLpInstantaneousWithBaseTolerance: 1e5, + roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, + roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripLongInstantaneousWithBaseTolerance: 1e5, + roundTripLongMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripLongMaturityWithBaseTolerance: 1e5, + roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripShortInstantaneousWithBaseTolerance: 1e5, + roundTripShortMaturityWithBaseTolerance: 1e5, + // The share test tolerances. + closeLongWithSharesTolerance: 20, + closeShortWithSharesTolerance: 100, + roundTripLpInstantaneousWithSharesTolerance: 1e16, + roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, + roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripLongInstantaneousWithSharesTolerance: 1e5, + roundTripLongMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripLongMaturityWithSharesTolerance: 1e5, + roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripShortInstantaneousWithSharesTolerance: 1e5, + roundTripShortMaturityWithSharesTolerance: 1e5, + // The verification tolerances. + verifyDepositTolerance: 1e12, + verifyWithdrawalTolerance: 1e13 + }) + ) + {} + + /// @notice Forge function that is invoked to setup the testing environment. + function setUp() public override __base_fork(20_773_209) { + // Invoke the Instance testing suite setup. + super.setUp(); + } + + /// Price Per Share /// + + /// @dev Fuzz test that verifies that the vault share price is the price + /// that dictates the conversion between base and shares. + /// + /// NOTE: We use a custom tolerance for this test to avoid reducing the + /// utility of `shareTolerance`. + /// @param basePaid the fuzz parameter for the base paid. + function test__pricePerVaultShare(uint256 basePaid) external override { + // Ensure that the share price is the expected value. + (uint256 totalBase, uint256 totalSupply) = getSupply(); + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + assertEq(vaultSharePrice, totalBase.divDown(totalSupply)); + + // Ensure that the share price accurately predicts the amount of shares + // that will be minted for depositing a given amount of shares. This will + // be an approximation. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + hyperdrive.calculateMaxLong() + ); + (, uint256 hyperdriveSharesBefore) = getTokenBalances( + address(hyperdrive) + ); + openLong(bob, basePaid); + (, uint256 hyperdriveSharesAfter) = getTokenBalances( + address(hyperdrive) + ); + assertApproxEqAbs( + hyperdriveSharesAfter, + hyperdriveSharesBefore + basePaid.divDown(vaultSharePrice), + 1e14 + ); + } +} diff --git a/test/instances/erc4626/MoonwellUSDC.t.sol b/test/instances/erc4626/MoonwellUSDC.t.sol new file mode 100644 index 000000000..fe84296d9 --- /dev/null +++ b/test/instances/erc4626/MoonwellUSDC.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { IERC20 } from "../../../contracts/src/interfaces/IERC20.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { FixedPointMath } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { InstanceTest } from "../../utils/InstanceTest.sol"; +import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol"; +import { Lib } from "../../utils/Lib.sol"; +import { MetaMorphoHyperdriveInstanceTest } from "./MetaMorphoHyperdriveInstanceTest.t.sol"; + +contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// @dev The USDC contract. + IERC20 internal constant USDC = + IERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913); + + /// @dev The Moonwell USDC contract. + IERC20 internal constant MWUSDC = + IERC20(0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca); + + /// @dev Whale accounts. + address internal USDC_TOKEN_WHALE = + address(0x711799fcb318010c3eBebD094B3eefB7d1e2Cf08); + address[] internal baseTokenWhaleAccounts = [USDC_TOKEN_WHALE]; + address internal MWUSDC_TOKEN_WHALE = + address(0x49E96E255bA418d08E66c35b588E2f2F3766E1d0); + address[] internal vaultSharesTokenWhaleAccounts = [MWUSDC_TOKEN_WHALE]; + + /// @notice Instantiates the instance testing suite with the configuration. + constructor() + InstanceTest( + InstanceTestConfig({ + name: "Hyperdrive", + kind: "ERC4626Hyperdrive", + decimals: 6, + baseTokenWhaleAccounts: baseTokenWhaleAccounts, + vaultSharesTokenWhaleAccounts: vaultSharesTokenWhaleAccounts, + baseToken: USDC, + vaultSharesToken: MWUSDC, + shareTolerance: 1e3, + minimumShareReserves: 1e3, + minimumTransactionAmount: 1e3, + positionDuration: POSITION_DURATION, + fees: IHyperdrive.Fees({ + curve: 0.001e18, + flat: 0.0001e18, + governanceLP: 0, + governanceZombie: 0 + }), + enableBaseDeposits: true, + enableShareDeposits: true, + enableBaseWithdraws: true, + enableShareWithdraws: true, + baseWithdrawError: new bytes(0), + isRebasing: false, + shouldAccrueInterest: true, + // The base test tolerances. + closeLongWithBaseTolerance: 20, + roundTripLpInstantaneousWithBaseTolerance: 1e5, + roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, + roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripLongInstantaneousWithBaseTolerance: 1e5, + roundTripLongMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripLongMaturityWithBaseTolerance: 1e5, + roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripShortInstantaneousWithBaseTolerance: 1e5, + roundTripShortMaturityWithBaseTolerance: 1e5, + // The share test tolerances. + closeLongWithSharesTolerance: 20, + closeShortWithSharesTolerance: 100, + // NOTE: This is high, but the vault share proceeds are always + // less than the expected amount. This seems to be caused by the + // lack of precision of our vault share price. For reasonable + // numbers (above 1e18), the result is consistently within 6 + // decimals of precision. + roundTripLpInstantaneousWithSharesTolerance: 1e18, + roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, + roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripLongInstantaneousWithSharesTolerance: 1e5, + roundTripLongMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripLongMaturityWithSharesTolerance: 1e5, + roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripShortInstantaneousWithSharesTolerance: 1e5, + roundTripShortMaturityWithSharesTolerance: 1e5, + // The verification tolerances. + verifyDepositTolerance: 1e12, + verifyWithdrawalTolerance: 1e13 + }) + ) + {} + + /// @notice Forge function that is invoked to setup the testing environment. + function setUp() public override __base_fork(21_117_198) { + // Invoke the Instance testing suite setup. + super.setUp(); + } + + /// Price Per Share /// + + /// @dev Fuzz test that verifies that the vault share price is the price + /// that dictates the conversion between base and shares. + /// + /// NOTE: We use a custom tolerance for this test to avoid reducing the + /// utility of `shareTolerance`. + /// @param basePaid the fuzz parameter for the base paid. + function test__pricePerVaultShare(uint256 basePaid) external override { + // Ensure that the share price is the expected value. + (uint256 totalBase, uint256 totalSupply) = getSupply(); + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + assertEq(vaultSharePrice, totalBase.divDown(totalSupply)); + + // Ensure that the share price accurately predicts the amount of shares + // that will be minted for depositing a given amount of shares. This will + // be an approximation. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + hyperdrive.calculateMaxLong() + ); + (, uint256 hyperdriveSharesBefore) = getTokenBalances( + address(hyperdrive) + ); + openLong(bob, basePaid); + (, uint256 hyperdriveSharesAfter) = getTokenBalances( + address(hyperdrive) + ); + assertApproxEqAbs( + hyperdriveSharesAfter, + hyperdriveSharesBefore + basePaid.divDown(vaultSharePrice), + 1e15 + ); + } +} diff --git a/test/instances/erc4626/SnARS.t.sol b/test/instances/erc4626/SnARS.t.sol new file mode 100644 index 000000000..a317eadf6 --- /dev/null +++ b/test/instances/erc4626/SnARS.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { IERC20 } from "../../../contracts/src/interfaces/IERC20.sol"; +import { IERC4626 } from "../../../contracts/src/interfaces/IERC4626.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol"; +import { InstanceTest } from "../../utils/InstanceTest.sol"; +import { Lib } from "../../utils/Lib.sol"; +import { ERC4626HyperdriveInstanceTest } from "./ERC4626HyperdriveInstanceTest.t.sol"; + +interface ISnARS is IERC20 { + function numStakingContract() external view returns (IStaking); +} + +interface IStaking { + function setApy(uint256) external; +} + +contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { + using HyperdriveUtils for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + using stdStorage for StdStorage; + + /// @dev The admin that can update the snARS APY. + address internal constant SNARS_ADMIN = + 0x062F9f2e46cDEF536D0E10c6cb31f951921c4A68; + + /// @dev The nARS contract. + IERC20 internal constant NARS = + IERC20(0x5e40f26E89213660514c51Fb61b2d357DBf63C85); + + /// @dev The snARS contract. + ISnARS internal constant SNARS = + ISnARS(0xC1F4C75e8925A67BE4F35D6b1c044B5ea8849a58); + + /// @dev Whale accounts. + address internal NARS_TOKEN_WHALE = + address(0x1ebc5e10D58e1d78A28971B5776Ea6A64CB88329); + address[] internal baseTokenWhaleAccounts = [NARS_TOKEN_WHALE]; + address internal SNARS_TOKEN_WHALE = + address(0x54423d0A5c4e3a6Eb8Bd12FDD54c1e6b42D52Ebe); + address[] internal vaultSharesTokenWhaleAccounts = [SNARS_TOKEN_WHALE]; + + /// @notice Instantiates the instance testing suite with the configuration. + constructor() + InstanceTest( + InstanceTestConfig({ + name: "Hyperdrive", + kind: "ERC4626Hyperdrive", + decimals: 18, + baseTokenWhaleAccounts: baseTokenWhaleAccounts, + vaultSharesTokenWhaleAccounts: vaultSharesTokenWhaleAccounts, + baseToken: NARS, + vaultSharesToken: SNARS, + shareTolerance: 1e3, + minimumShareReserves: 1e15, + minimumTransactionAmount: 1e15, + positionDuration: POSITION_DURATION, + fees: IHyperdrive.Fees({ + curve: 0, + flat: 0, + governanceLP: 0, + governanceZombie: 0 + }), + enableBaseDeposits: true, + enableShareDeposits: true, + enableBaseWithdraws: false, + enableShareWithdraws: true, + baseWithdrawError: abi.encodeWithSelector( + IHyperdrive.UnsupportedToken.selector + ), + isRebasing: false, + shouldAccrueInterest: true, + // NOTE: Base withdrawals are disabled, so the tolerances are zero. + // + // The base test tolerances. + closeLongWithBaseTolerance: 0, + roundTripLpInstantaneousWithBaseTolerance: 0, + roundTripLpWithdrawalSharesWithBaseTolerance: 0, + roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripLongInstantaneousWithBaseTolerance: 0, + roundTripLongMaturityWithBaseUpperBoundTolerance: 0, + roundTripLongMaturityWithBaseTolerance: 0, + roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripShortInstantaneousWithBaseTolerance: 0, + roundTripShortMaturityWithBaseTolerance: 0, + // The share test tolerances. + closeLongWithSharesTolerance: 20, + closeShortWithSharesTolerance: 100, + roundTripLpInstantaneousWithSharesTolerance: 1e7, + roundTripLpWithdrawalSharesWithSharesTolerance: 1e8, + roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripLongInstantaneousWithSharesTolerance: 1e5, + roundTripLongMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripLongMaturityWithSharesTolerance: 1e5, + roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripShortInstantaneousWithSharesTolerance: 1e5, + roundTripShortMaturityWithSharesTolerance: 1e5, + // The verification tolerances. + verifyDepositTolerance: 2, + verifyWithdrawalTolerance: 2 + }) + ) + {} + + /// @notice Forge function that is invoked to setup the testing environment. + function setUp() public override __base_fork(21_071_334) { + // Invoke the Instance testing suite setup. + super.setUp(); + } + + /// @dev HACK: The snARS vault returns a total assets of 0, so we have to + /// override this. + /// @dev Fetches the total supply of the base and share tokens. + /// @return The total supply of base. + /// @return The total supply of vault shares. + function getSupply() internal view override returns (uint256, uint256) { + uint256 totalShares = IERC4626(address(config.vaultSharesToken)) + .totalSupply(); + return ( + IERC4626(address(config.vaultSharesToken)).convertToAssets( + totalShares + ), + totalShares + ); + } + + /// Helpers /// + + /// @dev Advance time and accrue interest. + /// @param timeDelta The time to advance. + /// @param variableRate The variable rate. + function advanceTime( + uint256 timeDelta, + int256 variableRate + ) internal override { + // Ensure the variable rate isn't negative. + require(variableRate >= 0, "SnARSHyperdriveTest: negative rate"); + + // Update the APY of the staking contract. + vm.stopPrank(); + vm.startPrank(SNARS_ADMIN); + IStaking staking = SNARS.numStakingContract(); + // NOTE: Since we aren't calling this contract repeatedly, this + // functions as an APR. + staking.setApy(uint256(variableRate)); + + // Advance the time. + vm.warp(block.timestamp + timeDelta); + } +} diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index a4fddf3e3..c616032c9 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -621,19 +621,20 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { function openLong( address trader, - uint256 baseAmount, + uint256 amount, bool asBase ) internal returns (uint256 maturityTime, uint256 bondAmount) { + uint256 minOutput = asBase ? amount : hyperdrive.convertToBase(amount); return openLong( trader, - baseAmount, + amount, DepositOverrides({ asBase: asBase, destination: trader, - depositAmount: baseAmount, + depositAmount: amount, minSharePrice: 0, // min vault share price of 0 - minSlippage: baseAmount, // min bond proceeds of baseAmount + minSlippage: minOutput, // min bond proceeds of baseAmount maxSlippage: type(uint256).max, // unused extraData: new bytes(0) // unused }) @@ -706,7 +707,7 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { address trader, uint256 bondAmount, DepositOverrides memory overrides - ) internal returns (uint256 maturityTime, uint256 baseAmount) { + ) internal returns (uint256 maturityTime, uint256 amount) { vm.stopPrank(); vm.startPrank(trader); @@ -718,7 +719,7 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { address(hyperdrive.getPoolConfig().baseToken) == address(ETH) && overrides.asBase ) { - (maturityTime, baseAmount) = hyperdrive.openShort{ + (maturityTime, amount) = hyperdrive.openShort{ value: overrides.depositAmount }( bondAmount, @@ -733,7 +734,7 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { } else { baseToken.mint(overrides.depositAmount); baseToken.approve(address(hyperdrive), overrides.maxSlippage); - (maturityTime, baseAmount) = hyperdrive.openShort( + (maturityTime, amount) = hyperdrive.openShort( bondAmount, overrides.maxSlippage, // max base payment overrides.minSharePrice, // min vault share price @@ -743,16 +744,16 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { extraData: overrides.extraData }) ); - baseToken.burn(overrides.depositAmount - baseAmount); + baseToken.burn(overrides.depositAmount - amount); } - return (maturityTime, baseAmount); + return (maturityTime, amount); } function openShort( address trader, uint256 bondAmount - ) internal returns (uint256 maturityTime, uint256 baseAmount) { + ) internal returns (uint256 maturityTime, uint256 amount) { return openShort( trader, @@ -773,7 +774,13 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { address trader, uint256 bondAmount, bool asBase - ) internal returns (uint256 maturityTime, uint256 baseAmount) { + ) internal returns (uint256 maturityTime, uint256 amount) { + uint256 depositAmount = asBase + ? bondAmount + : hyperdrive.convertToShares(bondAmount); + uint256 maxDeposit = asBase + ? bondAmount + : hyperdrive.convertToShares(bondAmount); return openShort( trader, @@ -781,10 +788,10 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { DepositOverrides({ asBase: asBase, destination: trader, - depositAmount: bondAmount, + depositAmount: depositAmount, minSharePrice: 0, // min vault share price of 0 minSlippage: 0, // unused - maxSlippage: bondAmount, // max base payment of bondAmount + maxSlippage: maxDeposit, // max base payment of bondAmount extraData: new bytes(0) // unused }) ); @@ -1114,9 +1121,9 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { if (asBase) { assertApproxEqAbs( eventLpAmount, - contribution_.divDown( - hyperdrive_.getPoolConfig().initialVaultSharePrice - ) - 2 * minimumShareReserves, + hyperdrive_.convertToShares(contribution_) - + 2 * + minimumShareReserves, tolerance ); } else { diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 1db3b991e..c25f1014c 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -229,7 +229,7 @@ abstract contract InstanceTest is HyperdriveTest { uint256 contribution; if (config.enableShareDeposits && !config.isRebasing) { contribution = (poolConfig.vaultSharesToken.balanceOf(alice) / 10) - .min(1_000 * 10 ** config.decimals); + .min(1_000 * 10 ** poolConfig.vaultSharesToken.decimals()); } // If share deposits are enabled and the vault shares token is a // rebasing token, the contribution is the minimum of a tenth of Alice's @@ -954,7 +954,7 @@ abstract contract InstanceTest is HyperdriveTest { // Contribution in terms of shares. uint256 contribution = (poolConfig.vaultSharesToken.balanceOf(alice) / - 10).min(1_000 * 10 ** config.decimals); + 10).min(1_000 * 10 ** poolConfig.vaultSharesToken.decimals()); if (config.isRebasing) { contribution = convertToShares(contribution); } @@ -1050,8 +1050,10 @@ abstract contract InstanceTest is HyperdriveTest { (baseProceeds, withdrawalShares) = removeLiquidity(bob, lpShares); assertEq(withdrawalShares, 0); - // Bob should receive approximately as much base as he contributed since - // no time as passed and the fees are zero. + // Bob should receive approximately as much base as he contributed + // since no time as passed and the fees are zero. His proceeds + // should not be more than he originally contributed. + assertLe(baseProceeds, _contribution); assertApproxEqAbs( baseProceeds, _contribution, @@ -1069,7 +1071,12 @@ abstract contract InstanceTest is HyperdriveTest { assertEq(withdrawalShares, 0); // Bob should receive approximately as many vault shares as he - // contributed since no time as passed and the fees are zero. + // contributed since no time as passed and the fees are zero. His + // proceeds should not be more than he originally contributed. + assertLe( + vaultSharesProceeds, + hyperdrive.convertToShares(_contribution) + ); assertApproxEqAbs( vaultSharesProceeds, hyperdrive.convertToShares(_contribution), @@ -1134,7 +1141,9 @@ abstract contract InstanceTest is HyperdriveTest { assertEq(withdrawalShares, 0); // Bob should receive approximately as many vault shares as he - // contributed since no time as passed and the fees are zero. + // contributed since no time as passed and the fees are zero. His + // proceeds should not be more than he originally contributed. + assertLe(vaultSharesProceeds, _contribution); assertApproxEqAbs( vaultSharesProceeds, _contribution, @@ -1148,8 +1157,10 @@ abstract contract InstanceTest is HyperdriveTest { (baseProceeds, withdrawalShares) = removeLiquidity(bob, lpShares); assertEq(withdrawalShares, 0); - // Bob should receive approximately as much base as he contributed since - // no time as passed and the fees are zero. + // Bob should receive approximately as much base as he contributed + // since no time as passed and the fees are zero. His proceeds + // should not be more than he originally contributed. + assertLe(baseProceeds, _contribution); assertApproxEqAbs( baseProceeds, _contribution, @@ -1322,7 +1333,6 @@ abstract contract InstanceTest is HyperdriveTest { uint256 vaultSharesProceeds, uint256 withdrawalShares ) = removeLiquidity(bob, lpShares, false); - assertGt(withdrawalShares, 0); // The term passes and interest accrues. if (config.shouldAccrueInterest) { @@ -1364,7 +1374,6 @@ abstract contract InstanceTest is HyperdriveTest { bob, lpShares ); - assertGt(withdrawalShares, 0); // The term passes and interest accrues. if (config.shouldAccrueInterest) { @@ -2426,7 +2435,7 @@ abstract contract InstanceTest is HyperdriveTest { } (uint256 maturityTime, uint256 sharesPaid) = hyperdrive.openShort( shortAmount, - shortAmount, + hyperdrive.convertToShares(shortAmount), 0, IHyperdrive.Options({ destination: bob,