diff --git a/.gitmodules b/.gitmodules index a2df3f1f..043b2bcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/dss-test"] path = lib/dss-test url = https://github.com/makerdao/dss-test +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/README.md b/README.md index c480ccd1..41a73800 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Direct Deposit Module for Maker + ![Build Status](https://github.com/makerdao/dss-direct-deposit/actions/workflows/.github/workflows/tests.yaml/badge.svg?branch=master) The Dai Direct Deposit Module (D3M) is a tool for directly injecting DAI into third party protocols. @@ -11,7 +12,7 @@ The D3M is made of 3 components on the Maker side: ### D3MHub -The primary manager contract responsible for collecting all information and determining which action to take (if any). Each D3M instance is regsitered on the Hub using relevant `file(ilk, ...)` admin functions. +The primary manager contract responsible for collecting all information and determining which action to take (if any). Each D3M instance is registered on the Hub using relevant `file(ilk, ...)` admin functions. A permissionless `exec(ilk)` function exists which will perform all necessary steps to update the provided liquidity within the debt ceiling and external protocol constraints. `exec(ilk)` will need to be called on a somewhat regular basis to keep the system running properly. During each call to this function, interest will automatically be collected. @@ -51,7 +52,7 @@ Below is a configurable parameter for the Compound DAI D3M: Any Comp that is accured can be permissionlessly collected into the pause proxy by calling `collect()`. -### Setup and Testing +# Setup and Testing To set up the environment and run tests, run the following commands: diff --git a/audits/2024_03_ChainSecurity_MakerDAO_D3M_ERC-4626_audit.pdf b/audits/2024_03_ChainSecurity_MakerDAO_D3M_ERC-4626_audit.pdf new file mode 100644 index 00000000..86cf8d4f Binary files /dev/null and b/audits/2024_03_ChainSecurity_MakerDAO_D3M_ERC-4626_audit.pdf differ diff --git a/audits/2024_03_Spearbit-report-maker-d3m-implementation-for-morpho-review.pdf b/audits/2024_03_Spearbit-report-maker-d3m-implementation-for-morpho-review.pdf new file mode 100644 index 00000000..ca8e8cc5 Binary files /dev/null and b/audits/2024_03_Spearbit-report-maker-d3m-implementation-for-morpho-review.pdf differ diff --git a/lib/solmate b/lib/solmate new file mode 160000 index 00000000..c8923099 --- /dev/null +++ b/lib/solmate @@ -0,0 +1 @@ +Subproject commit c892309933b25c03d32b1b0d674df7ae292ba925 diff --git a/script/D3MDeploy.s.sol b/script/D3MDeploy.s.sol index 68a3adfb..30a9b928 100644 --- a/script/D3MDeploy.s.sol +++ b/script/D3MDeploy.s.sol @@ -103,6 +103,15 @@ contract D3MDeployScript is Script { hub, config.readAddress(".cdai") ); + } else if (poolType.eq("erc4626")) { + d3m.pool = D3MDeploy.deploy4626TypePool( + msg.sender, + admin, + ilk, + hub, + address(dss.dai), + config.readAddress(".vault") + ); } else { revert("Unknown pool type"); } @@ -135,6 +144,11 @@ contract D3MDeployScript is Script { } else { revert("Invalid pool type for liquidity buffer plan type"); } + } else if (planType.eq("operator")) { + d3m.plan = D3MDeploy.deployOperatorPlan( + msg.sender, + admin + ); } else { revert("Unknown plan type"); } diff --git a/script/D3MInit.s.sol b/script/D3MInit.s.sol index c02d9d18..0cc146f4 100644 --- a/script/D3MInit.s.sol +++ b/script/D3MInit.s.sol @@ -35,6 +35,10 @@ import { D3MAaveBufferPlanConfig, D3MCompoundPoolLike, D3MCompoundRateTargetPlanLike, + D3M4626PoolLike, + D3M4626PoolConfig, + D3MOperatorPlanLike, + D3MOperatorPlanConfig, CDaiLike } from "../src/deploy/D3MInit.sol"; @@ -119,6 +123,16 @@ contract D3MInitScript is Script { cfg, compoundCfg ); + } else if (poolType.eq("erc4626")) { + D3M4626PoolConfig memory erc4626Cfg = D3M4626PoolConfig({ + vault: D3M4626PoolLike(d3m.pool).vault() + }); + D3MInit.init4626Pool( + dss, + d3m, + cfg, + erc4626Cfg + ); } else { revert("Unknown pool type"); } @@ -165,6 +179,14 @@ contract D3MInitScript is Script { } else { revert("Invalid pool type for liquidity buffer plan type"); } + } else if (planType.eq("operator")) { + D3MOperatorPlanConfig memory operatorCfg = D3MOperatorPlanConfig({ + operator: config.readAddress(".operator") + }); + D3MInit.initOperatorPlan( + d3m, + operatorCfg + ); } else { revert("Unknown plan type"); } diff --git a/script/input/1/template-morpho.json b/script/input/1/template-morpho.json new file mode 100644 index 00000000..430f9bdc --- /dev/null +++ b/script/input/1/template-morpho.json @@ -0,0 +1,14 @@ +{ + "chainlog": "0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F", + "admin": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", + "poolType": "erc4626", + "planType": "operator", + "ilk": "DIRECT-SPARK-MORPHO-DAI", + "existingIlk": false, + "maxLine": 100000000, + "gap": 100000000, + "ttl": 86400, + "tau": 604800, + "vault": "0x73e65DBD630f90604062f6E02fAb9138e713edD9", + "operator": "0x298b375f24CeDb45e936D7e21d6Eb05e344adFb5" +} diff --git a/src/deploy/D3MDeploy.sol b/src/deploy/D3MDeploy.sol index f85652b8..5d42b6a6 100644 --- a/src/deploy/D3MDeploy.sol +++ b/src/deploy/D3MDeploy.sol @@ -30,6 +30,8 @@ import { D3MAaveV2TypePool } from "../pools/D3MAaveV2TypePool.sol"; import { D3MAaveV3NoSupplyCapTypePool } from "../pools/D3MAaveV3NoSupplyCapTypePool.sol"; import { D3MCompoundV2TypeRateTargetPlan } from "../plans/D3MCompoundV2TypeRateTargetPlan.sol"; import { D3MCompoundV2TypePool } from "../pools/D3MCompoundV2TypePool.sol"; +import { D3M4626TypePool } from "../pools/D3M4626TypePool.sol"; +import { D3MOperatorPlan } from "../plans/D3MOperatorPlan.sol"; import { D3MOracle } from "../D3MOracle.sol"; // Deploy a D3M instance @@ -96,6 +98,19 @@ library D3MDeploy { ScriptTools.switchOwner(pool, deployer, owner); } + function deploy4626TypePool( + address deployer, + address owner, + bytes32 ilk, + address hub, + address dai, + address vault + ) internal returns (address pool) { + pool = address(new D3M4626TypePool(ilk, hub, dai, vault)); + + ScriptTools.switchOwner(pool, deployer, owner); + } + function deployAaveV2TypeRateTargetPlan( address deployer, address owner, @@ -127,4 +142,13 @@ library D3MDeploy { ScriptTools.switchOwner(plan, deployer, owner); } + function deployOperatorPlan( + address deployer, + address owner + ) internal returns (address plan) { + plan = address(new D3MOperatorPlan()); + + ScriptTools.switchOwner(plan, deployer, owner); + } + } diff --git a/src/deploy/D3MInit.sol b/src/deploy/D3MInit.sol index b08f804e..48accbbb 100644 --- a/src/deploy/D3MInit.sol +++ b/src/deploy/D3MInit.sol @@ -80,6 +80,18 @@ interface CDaiLike { function implementation() external view returns (address); } +interface D3M4626PoolLike { + function hub() external view returns (address); + function dai() external view returns (address); + function ilk() external view returns (bytes32); + function vat() external view returns (address); + function vault() external view returns (address); +} + +interface D3MOperatorPlanLike { + function file(bytes32, address) external; +} + interface D3MOracleLike { function vat() external view returns (address); function ilk() external view returns (bytes32); @@ -148,6 +160,14 @@ struct D3MCompoundRateTargetPlanConfig { address delegate; } +struct D3M4626PoolConfig { + address vault; +} + +struct D3MOperatorPlanConfig { + address operator; +} + // Init a D3M instance library D3MInit { @@ -275,6 +295,25 @@ library D3MInit { pool.file("king", compoundCfg.king); } + /** + * @dev Initialize a 4626 pool. + */ + function init4626Pool( + DssInstance memory dss, + D3MInstance memory d3m, + D3MCommonConfig memory cfg, + D3M4626PoolConfig memory erc4626Cfg + ) internal view { + D3M4626PoolLike pool = D3M4626PoolLike(d3m.pool); + + // Sanity checks + require(pool.hub() == cfg.hub, "Pool hub mismatch"); + require(pool.ilk() == cfg.ilk, "Pool ilk mismatch"); + require(pool.vat() == address(dss.vat), "Pool vat mismatch"); + require(pool.dai() == address(dss.dai), "Pool dai mismatch"); + require(pool.vault() == erc4626Cfg.vault, "Pool vault mismatch"); + } + function initAaveRateTargetPlan( D3MInstance memory d3m, D3MAaveRateTargetPlanConfig memory aaveCfg @@ -323,4 +362,13 @@ library D3MInit { plan.file("barb", compoundCfg.barb); } + function initOperatorPlan( + D3MInstance memory d3m, + D3MOperatorPlanConfig memory operatorCfg + ) internal { + D3MOperatorPlanLike plan = D3MOperatorPlanLike(d3m.plan); + + plan.file("operator", operatorCfg.operator); + } + } diff --git a/src/plans/D3MOperatorPlan.sol b/src/plans/D3MOperatorPlan.sol new file mode 100644 index 00000000..e093a697 --- /dev/null +++ b/src/plans/D3MOperatorPlan.sol @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: © 2022 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import "./ID3MPlan.sol"; + +/** + * @title D3M Operator Plan + * @notice An operator sets the desired target assets. + */ +contract D3MOperatorPlan is ID3MPlan { + + mapping (address => uint256) public wards; + uint256 public enabled; + + address public operator; + uint256 public targetAssets; + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, address data); + event File(bytes32 indexed what, uint256 data); + + constructor() { + enabled = 1; + + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + modifier auth { + require(wards[msg.sender] == 1, "D3MOperatorPlan/not-authorized"); + _; + } + + // --- Admin --- + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, address data) external auth { + if (what == "operator") { + operator = data; + } else revert("D3MOperatorPlan/file-unrecognized-param"); + emit File(what, data); + } + + function file(bytes32 what, uint256 data) external auth { + if (what == "enabled") { + require(data <= 1, "D3MOperatorPlan/invalid-value"); + enabled = data; + } else revert("D3MOperatorPlan/file-unrecognized-param"); + emit File(what, data); + } + + function setTargetAssets(uint256 value) external { + require(msg.sender == operator, "D3MOperatorPlan/not-operator"); + + targetAssets = value; + } + + function getTargetAssets(uint256) external override view returns (uint256) { + if (enabled == 0) return 0; + + return targetAssets; + } + + function active() public view override returns (bool) { + return enabled == 1; + } + + function disable() external override auth { + enabled = 0; + emit Disable(); + } +} diff --git a/src/pools/D3M4626TypePool.sol b/src/pools/D3M4626TypePool.sol new file mode 100644 index 00000000..013703f0 --- /dev/null +++ b/src/pools/D3M4626TypePool.sol @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: © 2021 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import "./ID3MPool.sol"; +import "forge-std/interfaces/IERC4626.sol"; + +interface VatLike { + function live() external view returns (uint256); + function hope(address) external; + function nope(address) external; +} + +interface D3mHubLike { + function vat() external view returns (address); + function end() external view returns (EndLike); +} + +interface EndLike { + function Art(bytes32) external view returns (uint256); +} + +contract D3M4626TypePool is ID3MPool { + + mapping (address => uint256) public wards; + address public hub; + uint256 public exited; + + bytes32 public immutable ilk; + VatLike public immutable vat; + IERC4626 public immutable vault; + IERC20 public immutable dai; + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, address data); + + constructor(bytes32 ilk_, address hub_, address dai_, address vault_) { + ilk = ilk_; + dai = IERC20(dai_); + vault = IERC4626(vault_); + + require(ilk_ != bytes32(0), "D3M4626TypePool/zero-bytes32"); + require(hub_ != address(0), "D3M4626TypePool/zero-address"); + require(dai_ != address(0), "D3M4626TypePool/zero-address"); + require(vault_ != address(0), "D3M4626TypePool/zero-address"); + require(IERC4626(vault_).asset() == dai_, "D3M4626TypePool/vault-asset-is-not-dai"); + + dai.approve(vault_, type(uint256).max); + + hub = hub_; + vat = VatLike(D3mHubLike(hub_).vat()); + vat.hope(hub_); + + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + modifier auth() { + require(wards[msg.sender] == 1, "D3M4626TypePool/not-authorized"); + _; + } + + modifier onlyHub() { + require(msg.sender == hub, "D3M4626TypePool/only-hub"); + _; + } + + // --- Admin --- + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, address data) external auth { + require(vat.live() == 1, "D3M4626TypePool/no-file-during-shutdown"); + if (what == "hub") { + vat.nope(hub); + hub = data; + vat.hope(data); + } else revert("D3M4626TypePool/file-unrecognized-param"); + emit File(what, data); + } + + /// https://github.com/morpho-org/metamorpho/blob/fcf3c41d9c113514c9af0bbf6298e88a1060b220/src/MetaMorpho.sol#L531 + /// @inheritdoc ID3MPool + function deposit(uint256 wad) external override onlyHub { + vault.deposit(wad, address(this)); + } + + /// https://github.com/morpho-org/metamorpho/blob/fcf3c41d9c113514c9af0bbf6298e88a1060b220/src/MetaMorpho.sol#L557 + /// @inheritdoc ID3MPool + function withdraw(uint256 wad) external override onlyHub { + vault.withdraw(wad, msg.sender, address(this)); + } + + /// @inheritdoc ID3MPool + function exit(address dst, uint256 wad) external override onlyHub { + uint256 exited_ = exited; + exited = exited_ + wad; + uint256 amt = wad * vault.balanceOf(address(this)) / (D3mHubLike(hub).end().Art(ilk) - exited_); + require(vault.transfer(dst, amt), "D3M4626TypePool/transfer-failed"); + } + + /// @inheritdoc ID3MPool + function quit(address dst) external auth { + require(vat.live() == 1, "D3M4626TypePool/no-quit-during-shutdown"); + require(vault.transfer(dst, vault.balanceOf(address(this))), "D3M4626TypePool/transfer-failed"); + } + + /// @inheritdoc ID3MPool + function preDebtChange() external override {} + + /// @inheritdoc ID3MPool + function postDebtChange() external override {} + + /// @inheritdoc ID3MPool + function assetBalance() external view returns (uint256) { + return vault.convertToAssets(vault.balanceOf(address(this))); + } + + /// @inheritdoc ID3MPool + function maxDeposit() external view returns (uint256) { + return vault.maxDeposit(address(this)); + } + + /// @inheritdoc ID3MPool + function maxWithdraw() external view returns (uint256) { + return vault.maxWithdraw(address(this)); + } + + /// @inheritdoc ID3MPool + function redeemable() external view returns (address) { + return address(vault); + } +} diff --git a/src/tests/integration/D3MCompoundV2.t.sol b/src/tests/integration/D3MCompoundV2.t.sol index 1455eba2..9d9b3bbd 100644 --- a/src/tests/integration/D3MCompoundV2.t.sol +++ b/src/tests/integration/D3MCompoundV2.t.sol @@ -92,7 +92,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { uint256 constant INTEREST_RATE_TOLERANCE = WAD / 10000; function setUp() public { - vm.createSelectFork(vm.envString("ETH_RPC_URL")); + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 19_456_934); vat = VatAbstract(0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B); end = EndAbstract(0x0e2e8F1D1326A4B9633D96222Ce399c708B19c28); diff --git a/src/tests/integration/IntegrationBase.t.sol b/src/tests/integration/IntegrationBase.t.sol index edc20509..eddeb423 100644 --- a/src/tests/integration/IntegrationBase.t.sol +++ b/src/tests/integration/IntegrationBase.t.sol @@ -157,8 +157,8 @@ abstract contract IntegrationBaseTest is DssTest { hub.exec(ilk); (ink, art) = vat.urns(ilk, address(pool)); - assertEq(ink, 0); - assertEq(art, 0); + assertRoundingEq(ink, 0); + assertRoundingEq(art, 0); } function test_cage_perm_insufficient_liquidity() public { @@ -386,9 +386,9 @@ abstract contract IntegrationBaseTest is DssTest { // Rest of the liquidity can be withdrawn hub.exec(ilk); vow.heal(_min(vat.sin(address(vow)), vat.dai(address(vow)))); - assertEq(vat.gem(ilk, address(end)), 0); + assertRoundingEq(vat.gem(ilk, address(end)), 0); assertEq(vat.sin(address(vow)), 0); - assertGe(vat.dai(address(vow)), prevDai); // As also probably accrues interest + assertApproxEqAbs(vat.dai(address(vow)), prevDai, 1e27); } function test_unwind_mcd_caged_skimmed() public { @@ -456,9 +456,9 @@ abstract contract IntegrationBaseTest is DssTest { // Rest of the liquidity can be withdrawn hub.exec(ilk); vow.heal(_min(vat.sin(address(vow)), vat.dai(address(vow)))); - assertEq(vat.gem(ilk, address(end)), 0); + assertRoundingEq(vat.gem(ilk, address(end)), 0); assertEq(vat.sin(address(vow)), 0); - assertGe(vat.dai(address(vow)), prevDai); // As also probably accrues interest + assertApproxEqAbs(vat.dai(address(vow)), prevDai, 1e27); } function test_unwind_mcd_caged_wait_done() public { @@ -906,8 +906,8 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.vice(), viceBefore); assertEq(vat.sin(address(vow)), sinBefore); assertApproxEqAbs(vat.dai(address(vow)), vowDaiBefore + 10 * RAD, RAY * roundingTolerance); - assertEq(getLiquidity(), liquidityBalanceBefore); - assertEq(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore); + assertRoundingEq(getLiquidity(), liquidityBalanceBefore); + assertRoundingEq(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore); // Decrease debt adjustDebt(-standardDebtSize / 2); diff --git a/src/tests/integration/MetaMorpho.t.sol b/src/tests/integration/MetaMorpho.t.sol new file mode 100644 index 00000000..e59f3d1b --- /dev/null +++ b/src/tests/integration/MetaMorpho.t.sol @@ -0,0 +1,538 @@ +// SPDX-FileCopyrightText: © 2021 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import "forge-std/Test.sol"; +import "./IntegrationBase.t.sol"; +import "forge-std/interfaces/IERC4626.sol"; + +type Id is bytes32; + +struct MarketParams { + address loanToken; + address collateralToken; + address oracle; + address irm; + uint256 lltv; +} + +struct Position { + uint256 supplyShares; + uint128 borrowShares; + uint128 collateral; +} + +struct Market { + uint128 totalSupplyAssets; + uint128 totalSupplyShares; + uint128 totalBorrowAssets; + uint128 totalBorrowShares; + uint128 lastUpdate; + uint128 fee; +} + +interface IMorpho { + function supply( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes memory data + ) external returns (uint256 assetsSupplied, uint256 sharesSupplied); + function borrow( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsBorrowed, uint256 sharesBorrowed); + function supplyCollateral(MarketParams memory marketParams, uint256 assets, address onBehalf, bytes memory data) + external; + function accrueInterest(MarketParams memory marketParams) external; + function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory); + function position(Id id, address user) external view returns (Position memory p); + function market(Id id) external view returns (Market memory m); + function idToMarketParams(Id id) external view returns (MarketParams memory); +} + +struct MarketConfig { + uint184 cap; + bool enabled; + uint64 removableAt; +} + +interface IMetaMorpho is IERC4626 { + function owner() external view returns (address); + function timelock() external view returns (uint256); + function withdrawQueue(uint256) external view returns (Id); + function withdrawQueueLength() external view returns (uint256); + function config(Id) external view returns (MarketConfig memory); + function setSupplyQueue(Id[] calldata newSupplyQueue) external; + function submitCap(MarketParams memory marketParams, uint256 newSupplyCap) external; + function acceptCap(MarketParams memory marketParams) external; + function reallocate(MarketAllocation[] calldata allocations) external; +} + +struct MarketAllocation { + MarketParams marketParams; + uint256 assets; +} + +contract MetaMorphoTest is IntegrationBaseTest { + using {id} for MarketParams; + + IMetaMorpho constant spDai = IMetaMorpho(0x73e65DBD630f90604062f6E02fAb9138e713edD9); + address constant sUsde = 0x9D39A5DE30e57443BfF2A8307A4256c8797A3497; + IMorpho constant morpho = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + address constant sUsdeDaiOracle = 0x5D916980D5Ae1737a8330Bf24dF812b2911Aae25; + address constant adaptiveCurveIRM = 0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC; + D3MOperatorPlan plan; + D3M4626TypePool pool; + + // sUSDe/USDC (91.5%). + MarketParams public marketParams; + // sUSDe/USDC (94.5%). + MarketParams public marketParamsHighLltv; + + address operator = makeAddr("operator"); + uint256 constant startingAmount = 5_000_000 * WAD; + uint256 maxLineScaled; + + function setUp() public { + baseInit(); + + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 19456934); + + marketParams = MarketParams({ + loanToken: address(dai), + collateralToken: sUsde, + oracle: sUsdeDaiOracle, + irm: adaptiveCurveIRM, + lltv: 915000000000000000 + }); + marketParamsHighLltv = MarketParams({ + loanToken: address(dai), + collateralToken: sUsde, + oracle: sUsdeDaiOracle, + irm: adaptiveCurveIRM, + lltv: 945000000000000000 + }); + + // Deploy. + d3m.oracle = D3MDeploy.deployOracle(address(this), admin, ilk, address(dss.vat)); + d3m.pool = D3MDeploy.deploy4626TypePool(address(this), admin, ilk, address(hub), address(dai), address(spDai)); + pool = D3M4626TypePool(d3m.pool); + d3m.plan = D3MDeploy.deployOperatorPlan(address(this), admin); + plan = D3MOperatorPlan(d3m.plan); + + // Init. + vm.startPrank(admin); + D3MCommonConfig memory cfg = D3MCommonConfig({ + hub: address(hub), + mom: address(mom), + ilk: ilk, + existingIlk: false, + maxLine: startingAmount * RAY * 100000, // Set gap and max line to large number to avoid hitting limits + gap: startingAmount * RAY * 100000, + ttl: 0, + tau: 7 days + }); + D3MInit.initCommon(dss, d3m, cfg); + D3MInit.init4626Pool(dss, d3m, cfg, D3M4626PoolConfig({vault: address(spDai)})); + D3MInit.initOperatorPlan(d3m, D3MOperatorPlanConfig({operator: operator})); + vm.stopPrank(); + + maxLineScaled = cfg.maxLine * WAD / RAD; + + // Give us some DAI. + deal(address(dai), address(this), startingAmount * 100000000); + dai.approve(address(morpho), type(uint256).max); + // Give us some sUSDe. + deal(address(sUsde), address(this), type(uint128).max); + DaiAbstract(sUsde).approve(address(morpho), type(uint256).max); + // Supply huge collat. + morpho.supplyCollateral(marketParams, type(uint128).max, address(this), ""); + + assertTrue(spDai.config(marketParams.id()).enabled, "low market is not enabled"); + assertTrue(spDai.config(marketParamsHighLltv.id()).enabled, "high market is not enabled"); + + // Set sUSDe/USDC (91.5%) in the supply queue. + Id[] memory newSupplyQueue = new Id[](1); + newSupplyQueue[0] = marketParams.id(); + vm.prank(IMetaMorpho(spDai).owner()); + IMetaMorpho(spDai).setSupplyQueue(newSupplyQueue); + + // Set 0 cap for all listed markets. + for (uint256 i; i < spDai.withdrawQueueLength(); i++) { + setCap(morpho.idToMarketParams(spDai.withdrawQueue(i)), 0); + } + // Set max cap for sUSDe/USDC (91.5%). + setCap(marketParams, type(uint184).max); + + basePostSetup(); + } + + // --- Overrides --- + function adjustDebt(int256 deltaAmount) internal override { + if (deltaAmount == 0) return; + + int256 newTargetAssets = int256(plan.targetAssets()) + deltaAmount; + vm.prank(operator); + plan.setTargetAssets(newTargetAssets >= 0 ? uint256(newTargetAssets) : 0); + hub.exec(ilk); + } + + function adjustLiquidity(int256 deltaAmount) internal override { + if (deltaAmount == 0) return; + + if (deltaAmount > 0) { + // Supply to increase liquidity + uint256 amt = uint256(deltaAmount); + morpho.supply(marketParams, amt, 0, address(this), ""); + } else { + // Borrow to decrease liquidity + uint256 amt = uint256(-deltaAmount); + morpho.borrow(marketParams, amt, 0, address(this), address(this)); + } + } + + function generateInterest() internal override { + vm.warp(block.timestamp + 1 days); + morpho.accrueInterest(marketParams); + } + + function getLiquidity() internal view override returns (uint256) { + return morpho.market(marketParams.id()).totalSupplyAssets - morpho.market(marketParams.id()).totalBorrowAssets; + } + + function getLPTokenBalanceInAssets(address a) internal view override returns (uint256) { + return spDai.convertToAssets(spDai.balanceOf(a)); + } + + // --- Helpers --- + /// @dev Warning: set cap make some time pass, so it can accrue some interest on the markets. + function setCap(MarketParams memory mp, uint256 newCap) internal { + uint256 previousCap = spDai.config(mp.id()).cap; + if (previousCap != newCap) { + vm.prank(IMetaMorpho(spDai).owner()); + spDai.submitCap(mp, newCap); + if (previousCap < newCap) { + vm.warp(block.timestamp + spDai.timelock()); + spDai.acceptCap(mp); + } + } + } + + function supplyAssets(MarketParams memory mp, address user) internal view returns (uint256) { + Id marketId = mp.id(); + uint256 supplyShares = morpho.position(marketId, user).supplyShares; + uint256 totalSupplyAssets = morpho.market(marketId).totalSupplyAssets; + uint256 totalSupplyShares = morpho.market(marketId).totalSupplyShares; + return supplyShares * (totalSupplyAssets + 1) / (totalSupplyShares + 1e6); + } + + // --- Tests --- + function testDepositInOneMarket() public { + uint256 marketSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 spDaiTotalSupplyBefore = spDai.totalSupply(); + uint256 spDaiTotalAssetsBefore = spDai.totalAssets(); + uint256 spDaiMaxDepositBefore = spDai.maxDeposit(address(pool)); + uint256 morphoBalanceBefore = dai.balanceOf(address(morpho)); + uint256 daiTotalSupplyBefore = dai.totalSupply(); + + assertEq(spDai.totalSupply(), 0); + assertEq(spDai.totalAssets(), 0); + assertEq(plan.targetAssets(), 0); + assertEq(spDai.maxDeposit(address(pool)), type(uint184).max); + + vm.prank(operator); + plan.setTargetAssets(100_000_000e18); + hub.exec(ilk); + + assertEq(plan.targetAssets(), 100_000_000e18); + + assertEq(morpho.market(marketParams.id()).totalSupplyAssets, marketSupplyBefore + 100_000_000e18); + + assertEq(spDai.balanceOf(address(pool)), 100_000_000e18); + assertEq(spDai.totalSupply(), spDaiTotalSupplyBefore + 100_000_000e18); + assertEq(spDai.totalAssets(), spDaiTotalAssetsBefore + supplyAssets(marketParams, address(spDai))); + assertEq(spDai.maxDeposit(address(pool)), spDaiMaxDepositBefore - 100_000_000e18); + assertEq(pool.assetBalance(), 100_000_000e18 - 1); + + assertEq(dai.balanceOf(address(morpho)), morphoBalanceBefore + 100_000_000e18); + assertEq(dai.totalSupply(), daiTotalSupplyBefore + 100_000_000e18); + } + + function testDepositInOneMarketLessThanLine(uint256 d3mDeposit) public { + d3mDeposit = bound(d3mDeposit, 0, maxLineScaled); + + uint256 marketSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 spDaiTotalSupplyBefore = spDai.totalSupply(); + uint256 spDaiTotalAssetsBefore = spDai.totalAssets(); + uint256 spDaiMaxDepositBefore = spDai.maxDeposit(address(pool)); + uint256 morphoBalanceBefore = dai.balanceOf(address(morpho)); + uint256 daiTotalSupplyBefore = dai.totalSupply(); + + assertEq(spDai.totalSupply(), 0); + assertEq(spDai.totalAssets(), 0); + assertEq(plan.targetAssets(), 0); + assertEq(spDai.maxDeposit(address(pool)), type(uint184).max); + + // Set target assets at `d3mDeposit` and exec. + vm.prank(operator); + plan.setTargetAssets(d3mDeposit); + hub.exec(ilk); + + assertEq(plan.targetAssets(), d3mDeposit); + + assertEq(morpho.market(marketParams.id()).totalSupplyAssets, marketSupplyBefore + d3mDeposit); + + assertEq(spDai.balanceOf(address(pool)), d3mDeposit); + assertEq(spDai.totalSupply(), spDaiTotalSupplyBefore + d3mDeposit); + assertEq(spDai.totalAssets(), spDaiTotalAssetsBefore + supplyAssets(marketParams, address(spDai))); + assertEq(spDai.maxDeposit(address(pool)), spDaiMaxDepositBefore - d3mDeposit); + assertEq(pool.assetBalance(), zeroFloorSub(d3mDeposit, 1)); + + assertEq(dai.balanceOf(address(morpho)), morphoBalanceBefore + d3mDeposit); + assertEq(dai.totalSupply(), daiTotalSupplyBefore + d3mDeposit); + } + + function testDepositInOneMarketMoreThanLine(uint256 d3mDeposit) public { + d3mDeposit = bound(d3mDeposit, maxLineScaled, type(uint256).max); + + uint256 marketSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 spDaiTotalSupplyBefore = spDai.totalSupply(); + uint256 spDaiTotalAssetsBefore = spDai.totalAssets(); + uint256 spDaiMaxDepositBefore = spDai.maxDeposit(address(pool)); + uint256 morphoBalanceBefore = dai.balanceOf(address(morpho)); + uint256 daiTotalSupplyBefore = dai.totalSupply(); + + assertEq(spDai.totalSupply(), 0); + assertEq(spDai.totalAssets(), 0); + assertEq(plan.targetAssets(), 0); + assertEq(spDai.maxDeposit(address(pool)), type(uint184).max); + + // Set target assets at `d3mDeposit` and exec. + vm.prank(operator); + plan.setTargetAssets(d3mDeposit); + hub.exec(ilk); + + assertEq(plan.targetAssets(), d3mDeposit); + + assertEq(morpho.market(marketParams.id()).totalSupplyAssets, marketSupplyBefore + maxLineScaled); + + assertEq(spDai.balanceOf(address(pool)), maxLineScaled); + assertEq(spDai.totalSupply(), spDaiTotalSupplyBefore + maxLineScaled); + assertEq(spDai.totalAssets(), spDaiTotalAssetsBefore + supplyAssets(marketParams, address(spDai))); + assertEq(spDai.maxDeposit(address(pool)), spDaiMaxDepositBefore - maxLineScaled); + assertEq(pool.assetBalance(), zeroFloorSub(maxLineScaled, 1)); + + assertEq(dai.balanceOf(address(morpho)), morphoBalanceBefore + maxLineScaled); + assertEq(dai.totalSupply(), daiTotalSupplyBefore + maxLineScaled); + } + + function testDepositInCappedMarketLessThanCap(uint184 cap, uint256 d3mDeposit) public { + d3mDeposit = bound(d3mDeposit, 0, min(maxLineScaled, cap)); + + uint256 marketSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + + setCap(marketParams, cap); + + // Set target assets at `d3mDeposit` and exec. + vm.prank(operator); + plan.setTargetAssets(d3mDeposit); + hub.exec(ilk); + + assertEq(morpho.market(marketParams.id()).totalSupplyAssets, marketSupplyBefore + d3mDeposit); + } + + function testDepositInCappedMarketMoreThanCap(uint184 cap, uint256 d3mDeposit) public { + cap = uint184(bound(d3mDeposit, 0, maxLineScaled)); + d3mDeposit = bound(d3mDeposit, cap, maxLineScaled); + + uint256 marketSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + + setCap(marketParams, cap); + + // Set target assets at `d3mDeposit` and exec. + vm.prank(operator); + plan.setTargetAssets(d3mDeposit); + hub.exec(ilk); + + assertEq(morpho.market(marketParams.id()).totalSupplyAssets, marketSupplyBefore + cap); + } + + function testDepositInTwoMarketsLessThanCapLow(uint184 capLow, uint256 d3mDeposit) public { + d3mDeposit = bound(d3mDeposit, 0, min(maxLineScaled, capLow)); + + setCap(marketParams, type(uint184).max); + setCap(marketParamsHighLltv, type(uint184).max); + Id[] memory newSupplyQueue = new Id[](2); + newSupplyQueue[0] = marketParams.id(); + newSupplyQueue[1] = marketParamsHighLltv.id(); + vm.prank(spDai.owner()); + spDai.setSupplyQueue(newSupplyQueue); + setCap(marketParams, capLow); + + morpho.accrueInterest(marketParams); + morpho.accrueInterest(marketParamsHighLltv); + uint256 lowSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 highSupplyBefore = morpho.market(marketParamsHighLltv.id()).totalSupplyAssets; + + // Set target assets at `d3mDeposit` and exec. + vm.prank(operator); + plan.setTargetAssets(d3mDeposit); + hub.exec(ilk); + + uint256 lowSupplyAfter = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 highSupplyAfter = morpho.market(marketParamsHighLltv.id()).totalSupplyAssets; + + uint256 expectedDepositedInLow = d3mDeposit; + uint256 expectedDepositedInHigh = 0; + + assertEq(lowSupplyAfter, lowSupplyBefore + expectedDepositedInLow, "lowSupplyAfter"); + assertEq(highSupplyAfter, highSupplyBefore + expectedDepositedInHigh, "highSupplyAfter"); + } + + function testDepositInTwoMarketsMoreThanCapLow(uint184 capLow, uint256 d3mDeposit) public { + capLow = uint184(bound(capLow, 0, maxLineScaled)); + d3mDeposit = bound(d3mDeposit, capLow, maxLineScaled); + + setCap(marketParams, type(uint184).max); + setCap(marketParamsHighLltv, type(uint184).max); + Id[] memory newSupplyQueue = new Id[](2); + newSupplyQueue[0] = marketParams.id(); + newSupplyQueue[1] = marketParamsHighLltv.id(); + vm.prank(spDai.owner()); + spDai.setSupplyQueue(newSupplyQueue); + setCap(marketParams, capLow); + + morpho.accrueInterest(marketParams); + morpho.accrueInterest(marketParamsHighLltv); + uint256 lowSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 highSupplyBefore = morpho.market(marketParamsHighLltv.id()).totalSupplyAssets; + + // Set target assets at `d3mDeposit` and exec. + vm.prank(operator); + plan.setTargetAssets(d3mDeposit); + hub.exec(ilk); + + uint256 lowSupplyAfter = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 highSupplyAfter = morpho.market(marketParamsHighLltv.id()).totalSupplyAssets; + + uint256 expectedDepositedInLow = capLow; + uint256 expectedDepositedInHigh = d3mDeposit - capLow; + + assertEq(lowSupplyAfter, lowSupplyBefore + expectedDepositedInLow, "lowSupplyAfter"); + assertEq(highSupplyAfter, highSupplyBefore + expectedDepositedInHigh, "highSupplyAfter"); + } + + function testReallocate(int256 d3mDeposit, uint256 reallocation) public { + d3mDeposit = bound(d3mDeposit, 0, type(int256).max); + + setCap(marketParamsHighLltv, type(uint184).max); + + adjustDebt(d3mDeposit); + + uint256 vaultSupplyAssets = pool.assetBalance(); + reallocation = bound(reallocation, 0, vaultSupplyAssets); + + morpho.accrueInterest(marketParams); + morpho.accrueInterest(marketParamsHighLltv); + uint256 lowSupplyBefore = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 highSupplyBefore = morpho.market(marketParamsHighLltv.id()).totalSupplyAssets; + + // Reallocate from low to high lltv. + MarketAllocation[] memory allocations = new MarketAllocation[](2); + allocations[0] = MarketAllocation({marketParams: marketParams, assets: vaultSupplyAssets - reallocation}); + allocations[1] = MarketAllocation({marketParams: marketParamsHighLltv, assets: type(uint256).max}); + vm.prank(IMetaMorpho(spDai).owner()); + spDai.reallocate(allocations); + + uint256 lowSupplyAfter = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 highSupplyAfter = morpho.market(marketParamsHighLltv.id()).totalSupplyAssets; + + assertEq(lowSupplyAfter, lowSupplyBefore - reallocation); + assertEq(highSupplyAfter, highSupplyBefore + reallocation); + } + + function testWithdrawLiquid(uint256 target1, uint256 target2) public { + target1 = bound(target1, 0, uint256(type(int256).max)); + + uint256 marketSupplyStart = morpho.market(marketParams.id()).totalSupplyAssets; + + adjustDebt(int256(target1)); + + target2 = bound(target2, 0, pool.assetBalance()); + + uint256 marketSupplyMiddle = morpho.market(marketParams.id()).totalSupplyAssets; + + uint256 depositedAssets1 = min(target1, maxLineScaled); + uint256 expectedWithdraw = min(pool.maxWithdraw(), pool.assetBalance() - target2); + uint256 depositedAssets2 = depositedAssets1 - expectedWithdraw; + + // Set target assets at `target2` and exec. + vm.prank(operator); + plan.setTargetAssets(target2); + hub.exec(ilk); + + uint256 marketSupplyEnd = morpho.market(marketParams.id()).totalSupplyAssets; + + assertEq(marketSupplyMiddle, marketSupplyStart + depositedAssets1); + assertEq(marketSupplyEnd, marketSupplyMiddle - expectedWithdraw); + assertEq(marketSupplyEnd, marketSupplyStart + depositedAssets2); + } + + function testWithdrawIlliquid(uint256 target1, uint256 target2, uint256 borrow) public { + target1 = bound(target1, 1, uint256(type(int256).max)); + uint256 marketSupplyStart = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 liquidityStart = marketSupplyStart - morpho.market(marketParams.id()).totalBorrowAssets; + uint256 depositedAssets1 = min(target1, maxLineScaled); + borrow = bound(borrow, liquidityStart + 1, liquidityStart + depositedAssets1); + + adjustDebt(int256(target1)); + adjustLiquidity(-int256(borrow)); + + uint256 marketSupplyMiddle = morpho.market(marketParams.id()).totalSupplyAssets; + uint256 liquidityMiddle = marketSupplyMiddle - morpho.market(marketParams.id()).totalBorrowAssets; + target2 = bound(target2, 0, pool.assetBalance() - liquidityMiddle); + + uint256 expectedWithdraw = pool.maxWithdraw(); + + // Set target assets at `target2` and exec. + vm.prank(operator); + plan.setTargetAssets(target2); + hub.exec(ilk); + + uint256 marketSupplyEnd = morpho.market(marketParams.id()).totalSupplyAssets; + + assertEq(marketSupplyMiddle, marketSupplyStart + depositedAssets1, "middle"); + assertEq(marketSupplyEnd, marketSupplyMiddle - expectedWithdraw, "end"); + } +} + +function min(uint256 x, uint256 y) pure returns (uint256) { + return x < y ? x : y; +} + +function zeroFloorSub(uint256 x, uint256 y) pure returns (uint256) { + return x > y ? x - y : 0; +} + +function id(MarketParams memory marketParams) pure returns (Id) { + return Id.wrap(keccak256(abi.encode(marketParams))); +} diff --git a/src/tests/plans/D3MOperatorPlan.t.sol b/src/tests/plans/D3MOperatorPlan.t.sol new file mode 100644 index 00000000..f784b6a1 --- /dev/null +++ b/src/tests/plans/D3MOperatorPlan.t.sol @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: © 2022 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import { D3MPlanBaseTest } from "./D3MPlanBase.t.sol"; +import { D3MOperatorPlan } from "../../plans/D3MOperatorPlan.sol"; + +contract D3MOperatorPlanTest is D3MPlanBaseTest { + + D3MOperatorPlan plan; + + address operator = makeAddr("operator"); + address randomAddress = makeAddr("randomAddress"); + + event Disable(); + + function setUp() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + plan = new D3MOperatorPlan(); + + baseInit(plan, "D3MOperatorPlan"); + } + + function test_constructor() public { + assertEq(plan.enabled(), 1); + assertEq(plan.targetAssets(), 0); + } + + function test_file() public { + // File checks will increment the current value by 1 so + // just set it to 0 to start with so there is no revert. + plan.file("enabled", 0); + checkFileUint(address(plan), contractName, ["enabled"]); + checkFileAddress(address(plan), contractName, ["operator"]); + } + + function test_file_bad_value() public { + vm.expectRevert("D3MOperatorPlan/invalid-value"); + plan.file("enabled", 2); + } + + function _setupOperatorAndTargetAssets() internal { + plan.file("operator", operator); + vm.prank(operator); + plan.setTargetAssets(100e18); + } + + function test_setTargetAssets() public { + _setupOperatorAndTargetAssets(); + + assertEq(plan.targetAssets(), 100e18); + + vm.prank(operator); + plan.setTargetAssets(200e18); + + assertEq(plan.targetAssets(), 200e18); + } + + function test_setTargetAssets_onlyOperator() public { + _setupOperatorAndTargetAssets(); + + vm.prank(randomAddress); + vm.expectRevert("D3MOperatorPlan/not-operator"); + plan.setTargetAssets(200e18); + } + + function test_implements_getTargetAssets() public { + _setupOperatorAndTargetAssets(); + + uint256 result = plan.getTargetAssets(123e18); // argument doesn't matter + + assertEq(result, 100e18); + } + + function test_disable() public { + _setupOperatorAndTargetAssets(); + + assertEq(plan.enabled(), 1); + assertTrue(plan.active()); + assertEq(plan.getTargetAssets(0), 100e18); + + vm.expectEmit(true, true, true, true); + emit Disable(); + plan.disable(); + + assertTrue(!plan.active()); + assertEq(plan.enabled(), 0); + assertEq(plan.getTargetAssets(0), 0); // returns 0 when disabled + } + +} diff --git a/src/tests/pools/D3M4626TypePool.t.sol b/src/tests/pools/D3M4626TypePool.t.sol new file mode 100644 index 00000000..f6eadbe5 --- /dev/null +++ b/src/tests/pools/D3M4626TypePool.t.sol @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: © 2022 Dai Foundation +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.14; + +import "./D3MPoolBase.t.sol"; +import "../../pools/D3M4626TypePool.sol"; +import {ERC20, ERC4626 as ERC4626Abstract} from "solmate/tokens/ERC4626.sol"; + +contract ERC4626 is ERC4626Abstract { + constructor( + ERC20 _asset, + string memory _name, + string memory _symbol + ) ERC4626Abstract(_asset, _name, _symbol) {} + + function totalAssets() public view virtual override returns (uint256) { + return asset.balanceOf(address(this)); + } +} + +contract D3M4626TypePoolTest is D3MPoolBaseTest { + + D3M4626TypePool pool; + ERC4626 vault; + bytes32 constant ILK = "TEST-ILK"; + + function setUp() public { + baseInit("D3M4626TypePool"); + + vault = new ERC4626(ERC20(address(dai)), "dai vault", "DV"); + + setPoolContract(pool = new D3M4626TypePool(ILK, address(hub), address(dai), address(vault))); + + dai.approve(address(vault), type(uint256).max); + } + + function invariant_dai_value() public { + assertEq(address(pool.dai()), address(dai)); + } + + function invariant_vault_value() public { + assertEq(address(pool.vault()), address(vault)); + } + + function invariant_vat_value() public { + assertEq(address(pool.vat()), address(vat)); + } + + function invariant_ilk_value() public { + assertEq(pool.ilk(), ILK); + } + + function test_deposit_calls_vault_deposit() public { + deal(address(dai), address(pool), 1); + vm.prank(address(hub)); pool.deposit(1); + + assertEq(pool.assetBalance(), 1); + assertEq(dai.balanceOf(address(pool)), 0); + } + + function test_withdraw_calls_vault_withdraw() public { + deal(address(dai), address(pool), 1); + vm.prank(address(hub)); pool.deposit(1); + + vm.prank(address(hub)); pool.withdraw(1); + + assertEq(pool.assetBalance(), 0); + assertEq(dai.balanceOf(address(hub)), 1); + } + + function test_withdraw_calls_vault_withdraw_vat_caged() public { + deal(address(dai), address(pool), 1); + vm.prank(address(hub)); pool.deposit(1); + + vat.cage(); + vm.prank(address(hub)); pool.withdraw(1); + + assertEq(pool.assetBalance(), 0); + assertEq(dai.balanceOf(address(hub)), 1); + } + + function test_redeemable_returns_vault() public { + assertEq(pool.redeemable(), address(vault)); + } + + function test_exit_vault_tokens() public { + deal(address(dai), address(this), 1e18); + vault.deposit(1e18, address(this)); + uint256 balanceVault = vault.totalSupply(); + vault.transfer(address(pool), balanceVault); + assertEq(vault.balanceOf(address(this)), 0); + assertEq(vault.balanceOf(address(pool)), balanceVault); + + end.setArt(100 * WAD); + vm.prank(address(hub)); pool.exit(address(this), 50 * WAD); + + assertEq(vault.balanceOf(address(this)), balanceVault / 2); + assertEq(vault.balanceOf(address(pool)), balanceVault / 2); + } + + function test_quit_moves_balance() public { + deal(address(dai), address(this), 1e18); + vault.deposit(1e18, address(this)); + vault.transfer(address(pool), vault.balanceOf(address(this))); + assertEq(vault.balanceOf(address(this)), 0); + assertEq(vault.balanceOf(address(pool)), 1e18); + + pool.quit(address(this)); + + assertEq(vault.balanceOf(address(this)), 1e18); + assertEq(vault.balanceOf(address(pool)), 0); + } + + function test_assetBalance_gets_vault_balanceOf_pool() public { + deal(address(dai), address(this), 1e18); + vault.deposit(0.7e18, address(this)); + dai.transfer(address(vault), 0.3e18); + assertEq(pool.assetBalance(), 0); + assertEq(vault.balanceOf(address(pool)), 0); + + vault.transfer(address(pool), 0.7e18); + + assertEq(pool.assetBalance(), 1e18); + assertEq(vault.balanceOf(address(pool)), 0.7e18); + } + + function test_maxWithdraw_gets_available_assets_assetBal() public { + deal(address(dai), address(this), 1e18); + dai.transfer(address(vault), 1e18); + assertEq(dai.balanceOf(address(vault)), 1e18); + assertEq(vault.balanceOf(address(pool)), 0); + + assertEq(pool.maxWithdraw(), 0); + } + + function test_maxWithdraw_gets_available_assets_daiBal() public { + deal(address(dai), address(this), 1e18); + vault.deposit(1e18, address(this)); + vault.transfer(address(pool), 1e18); + assertEq(dai.balanceOf(address(vault)), 1e18); + assertEq(vault.balanceOf(address(pool)), 1e18); + + assertEq(pool.maxWithdraw(), 1e18); + } + + function invariant_pool_maxDeposit() public { + assertEq(pool.maxDeposit(), type(uint256).max); + } + + function invariant_vault_maxDeposit() public { + assertEq(vault.maxDeposit(address(pool)), type(uint256).max); + } +}