diff --git a/Makefile b/Makefile index 583770d1..4493f010 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ all :; FOUNDRY_OPTIMIZER=true FOUNDRY_OPTIMIZER_RUNS=200 forge build --use solc:0.8.14 clean :; forge clean -certora-hub :; PATH=~/.solc-select/artifacts/solc-0.8.14:~/.solc-select/artifacts/solc-0.5.12:~/.solc-select/artifacts:${PATH} certoraRun --solc_map D3MHub=solc-0.8.14,Vat=solc-0.5.12,DaiJoin=solc-0.5.12,Dai=solc-0.5.12,End=solc-0.5.12,D3MTestPlan=solc-0.8.14,D3MTestPool=solc-0.8.14,TokenMock=solc-0.8.14 --optimize_map D3MHub=200,Vat=0,DaiJoin=0,Dai=0,End=0,D3MTestPlan=200,D3MTestPool=200,TokenMock=200 --rule_sanity basic src/D3MHub.sol certora/dss/Vat.sol certora/dss/DaiJoin.sol certora/dss/Dai.sol certora/dss/End.sol certora/d3m/D3MTestPlan.sol certora/d3m/D3MTestPool.sol src/tests/mocks/TokenMock.sol --link D3MHub:vat=Vat D3MHub:daiJoin=DaiJoin D3MHub:end=End DaiJoin:vat=Vat DaiJoin:dai=Dai End:vat=Vat D3MTestPlan:dai=Dai D3MTestPool:hub=D3MHub D3MTestPool:vat=Vat D3MTestPool:dai=Dai D3MTestPool:share=TokenMock --verify D3MHub:certora/D3MHub.spec --settings -mediumTimeout=1200,-solver=z3,-adaptiveSolverConfig=false,-smt_nonLinearArithmetic=true$(if $(short), --short_output,)$(if $(rule), --rule $(rule),)$(if $(multi), --multi_assert_check,) +certora-hub :; PATH=~/.solc-select/artifacts/solc-0.8.14:~/.solc-select/artifacts/solc-0.5.12:~/.solc-select/artifacts:${PATH} certoraRun --solc_map D3MHub=solc-0.8.14,Vat=solc-0.5.12,DaiJoin=solc-0.5.12,Dai=solc-0.5.12,End=solc-0.5.12,D3MTestPlan=solc-0.8.14,D3MTestPool=solc-0.8.14,TokenMock=solc-0.8.14,D3MForwardFees=solc-0.8.14 --optimize_map D3MHub=200,Vat=0,DaiJoin=0,Dai=0,End=0,D3MTestPlan=200,D3MTestPool=200,TokenMock=200,D3MForwardFees=200 --rule_sanity basic src/D3MHub.sol certora/dss/Vat.sol certora/dss/DaiJoin.sol certora/dss/Dai.sol certora/dss/End.sol certora/d3m/D3MTestPlan.sol certora/d3m/D3MTestPool.sol src/tests/mocks/TokenMock.sol src/fees/D3MForwardFees.sol --link D3MHub:vat=Vat D3MHub:daiJoin=DaiJoin D3MHub:end=End DaiJoin:vat=Vat DaiJoin:dai=Dai End:vat=Vat D3MTestPlan:dai=Dai D3MTestPool:hub=D3MHub D3MTestPool:vat=Vat D3MTestPool:dai=Dai D3MTestPool:share=TokenMock D3MForwardFees:vat=Vat --verify D3MHub:certora/D3MHub.spec --settings -mediumTimeout=1200,-solver=z3,-adaptiveSolverConfig=false,-smt_nonLinearArithmetic=true$(if $(short), --short_output,)$(if $(rule), --rule $(rule),)$(if $(multi), --multi_assert_check,) deploy :; ./deploy.sh config="$(config)" deploy-core :; ./deploy-core.sh diff --git a/README.md b/README.md index c480ccd1..b17ebd92 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The Dai Direct Deposit Module (D3M) is a tool for directly injecting DAI into th As seen in the image above, external protocols are viewed under the simplified ERC-4626-like interface. Pool adapters are used to convert protocol complexity into simplified concepts of Excess Capacity + DAI liquidity + DAI outstanding. How DAI is converted between these states is completely protocol-specific and mostly irrelevant to the D3M. -The D3M is made of 3 components on the Maker side: +The D3M is made of 4 components on the Maker side: ### D3MHub @@ -23,6 +23,10 @@ A "dumb" adapter which is responsible for depositing or withdrawing DAI from the The D3MPlan can be viewed as the targeting logic for D3M instances. The plan is responsible for reading all relevant information from its state (i.e. the target rate) and possibly from the external protocol (i.e. current balance of supply and borrowing in the market) and condensing this down to a debt target. This desired target debt is forwarded to the Hub to take action to reach this target debt level asap. +### D3MFees + +D3MFees is a contract to deal with revenue coming in from a protocol. Default implementation `D3MForwardFees` with the target set to the `vow` will just send all revenue to the surplus buffer. + ### General Configuration The below parameter exists for each D3M implementation: @@ -31,25 +35,7 @@ The below parameter exists for each D3M implementation: # Specific Implementations -## Aave D3M - -### Configuration - -Below is a configurable parameter for the Aave DAI D3M: - -- `bar` [ray] - The target borrow rate on Aave for the DAI market. This module will aim to enforce that borrow limit. - -Any stkAave that is accured can be permissionlessly collected into the pause proxy by calling `collect()`. - -## Compound D3M - -### Configuration - -Below is a configurable parameter for the Compound DAI D3M: - -- `barb` [wad] - The target borrow rate per block on Compound for the DAI market. This module will aim to enforce that borrow limit. - -Any Comp that is accured can be permissionlessly collected into the pause proxy by calling `collect()`. +There are many pool/plan/fee combinations. Please review the specific contract and configurations to set all the appropriate parameters. ### Setup and Testing diff --git a/certora/D3MHub.spec b/certora/D3MHub.spec index 23234f5f..5ea73765 100644 --- a/certora/D3MHub.spec +++ b/certora/D3MHub.spec @@ -6,6 +6,7 @@ using DaiJoin as daiJoin using End as end using D3MTestPool as pool using D3MTestPlan as plan +using D3MForwardFees as fees using TokenMock as share methods { @@ -13,10 +14,11 @@ methods { daiJoin() returns (address) envfree vow() returns (address) envfree end() returns (address) envfree - ilks(bytes32) returns (address, address, uint256, uint256, uint256) envfree + ilks(bytes32) returns (address, address, address, uint256, uint256, uint256) envfree locked() returns (uint256) envfree plan(bytes32) returns (address) envfree => DISPATCHER(true) pool(bytes32) returns (address) envfree => DISPATCHER(true) + fees(bytes32) returns (address) envfree => DISPATCHER(true) tic(bytes32) returns (uint256) envfree tau(bytes32) returns (uint256) envfree culled(bytes32) returns (uint256) envfree @@ -52,10 +54,11 @@ methods { share.balanceOf(address) returns (uint256) envfree share.totalSupply() returns (uint256) envfree share.wards(address) returns (uint256) envfree + fees.target() returns (address) envfree debt() returns (uint256) => DISPATCHER(true) skim(bytes32, address) => DISPATCHER(true) active() returns (bool) => DISPATCHER(true) - getTargetAssets(uint256) returns (uint256) => DISPATCHER(true) + getTargetAssets(bytes32,uint256) returns (uint256) => DISPATCHER(true) assetBalance() returns (uint256) => DISPATCHER(true) maxDeposit() returns (uint256) => DISPATCHER(true) maxWithdraw() returns (uint256) => DISPATCHER(true) @@ -67,6 +70,7 @@ methods { balanceOf(address) returns (uint256) => DISPATCHER(true) burn(address, uint256) => DISPATCHER(true) mint(address, uint256) => DISPATCHER(true) + feesCollected(bytes32, uint256) => DISPATCHER(true) } definition WAD() returns uint256 = 10^18; @@ -172,6 +176,7 @@ rule file_ilk_address(bytes32 ilk, bytes32 what, address data) { assert(what == 0x706f6f6c00000000000000000000000000000000000000000000000000000000 => pool(ilk) == data, "file did not set pool as expected"); assert(what == 0x706c616e00000000000000000000000000000000000000000000000000000000 => plan(ilk) == data, "file did not set plan as expected"); + assert(what == 0x6665657300000000000000000000000000000000000000000000000000000000 => fees(ilk) == data, "file did not set fees as expected"); } rule file_ilk_address_revert(bytes32 ilk, bytes32 what, address data) { @@ -187,7 +192,9 @@ rule file_ilk_address_revert(bytes32 ilk, bytes32 what, address data) { bool revert2 = ward != 1; bool revert3 = vatLive != 1; bool revert4 = tic != 0; - bool revert5 = what != 0x706f6f6c00000000000000000000000000000000000000000000000000000000 && what != 0x706c616e00000000000000000000000000000000000000000000000000000000; + bool revert5 = what != 0x706f6f6c00000000000000000000000000000000000000000000000000000000 && + what != 0x706c616e00000000000000000000000000000000000000000000000000000000 && + what != 0x6665657300000000000000000000000000000000000000000000000000000000; assert(revert1 => lastReverted, "revert1 failed"); assert(revert2 => lastReverted, "revert2 failed"); @@ -201,10 +208,11 @@ rule file_ilk_address_revert(bytes32 ilk, bytes32 what, address data) { rule ilk_getters() { bytes32 ilk; - address pool_; address plan_; uint256 tau; uint256 culled; uint256 tic; - pool_, plan_, tau, culled, tic = ilks(ilk); + address pool_; address plan_; address fees_; uint256 tau; uint256 culled; uint256 tic; + pool_, plan_, fees_, tau, culled, tic = ilks(ilk); assert(pool_ == pool(ilk), "pool getter did not return ilk.pool"); assert(plan_ == plan(ilk), "plan getter did not return ilk.plan"); + assert(fees_ == fees(ilk), "fees getter did not return ilk.fees"); assert(tau == tau(ilk), "tau getter did not return ilk.tau"); assert(culled == culled(ilk), "culled getter did not return ilk.culled"); assert(tic == tic(ilk), "tic getter did not return ilk.tic"); @@ -219,6 +227,7 @@ rule exec_normal(bytes32 ilk) { require(daiJoin() == daiJoin); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(vow != daiJoin); require(daiJoin.dai() == dai); require(daiJoin.vat() == vat); @@ -226,6 +235,8 @@ rule exec_normal(bytes32 ilk) { require(pool.hub() == currentContract); require(pool.vat() == vat); require(pool.dai() == dai); + require(fees.target() == vow); + require(vat.dai(fees) == 0); uint256 tic = tic(ilk); uint256 culled = culled(ilk); @@ -246,7 +257,7 @@ rule exec_normal(bytes32 ilk) { uint256 maxDeposit = pool.maxDeposit(e); mathint maxWithdraw = min(to_mathint(pool.maxWithdraw(e)), safe_max()); uint256 assetsBefore = pool.assetBalance(e); - uint256 targetAssets = plan.getTargetAssets(e, assetsBefore); + uint256 targetAssets = plan.getTargetAssets(e, ilk, assetsBefore); uint256 vatDaiVowBefore = vat.dai(vow); require(vat.live() == 1); @@ -511,6 +522,7 @@ rule exec_normal_revert(bytes32 ilk) { require(daiJoin() == daiJoin); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(vow != currentContract); require(vow != daiJoin); require(daiJoin.dai() == dai); @@ -519,6 +531,8 @@ rule exec_normal_revert(bytes32 ilk) { require(pool.hub() == currentContract); require(pool.vat() == vat); require(pool.dai() == dai); + require(fees.target() == vow); + require(vat.dai(fees) == 0); uint256 locked = locked(); uint256 Line = vat.Line(); @@ -561,7 +575,7 @@ rule exec_normal_revert(bytes32 ilk) { uint256 tic = tic(ilk); bool active = plan.active(e); - uint256 targetAssets = plan.getTargetAssets(e, assets); + uint256 targetAssets = plan.getTargetAssets(e, ilk, assets); mathint toUnwindAux = max( max( @@ -729,6 +743,7 @@ rule exec_ilk_culled(bytes32 ilk) { require(daiJoin() == daiJoin); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(vow != daiJoin); require(daiJoin.dai() == dai); require(daiJoin.vat() == vat); @@ -751,7 +766,7 @@ rule exec_ilk_culled(bytes32 ilk) { uint256 maxWithdraw = pool.maxWithdraw(e); uint256 assetsBefore = pool.assetBalance(e); - uint256 targetAssets = plan.getTargetAssets(e, assetsBefore); + uint256 targetAssets = plan.getTargetAssets(e, ilk, assetsBefore); require(vat.live() == 1); require(inkBefore >= artBefore); @@ -801,6 +816,7 @@ rule exec_ilk_culled_revert(bytes32 ilk) { require(daiJoin() == daiJoin); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(vow != daiJoin); require(daiJoin.dai() == dai); require(daiJoin.vat() == vat); @@ -896,6 +912,7 @@ rule exec_vat_caged(bytes32 ilk) { require(end() == end); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(vow != daiJoin); require(daiJoin.dai() == dai); require(daiJoin.vat() == vat); @@ -919,7 +936,7 @@ rule exec_vat_caged(bytes32 ilk) { uint256 maxWithdraw = pool.maxWithdraw(e); uint256 assetsBefore = pool.assetBalance(e); - uint256 targetAssets = plan.getTargetAssets(e, assetsBefore); + uint256 targetAssets = plan.getTargetAssets(e, ilk, assetsBefore); require(vat.live() == 0); require(end.tag(ilk) == RAY()); @@ -972,6 +989,7 @@ rule exec_vat_caged_revert(bytes32 ilk) { require(end() == end); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(vow != daiJoin); require(daiJoin.dai() == dai); require(daiJoin.vat() == vat); @@ -1104,6 +1122,7 @@ rule exec_exec(bytes32 ilk) { require(daiJoin() == daiJoin); require(plan(ilk) == plan); require(pool(ilk) == pool); + require(fees(ilk) == fees); require(daiJoin.dai() == dai); require(daiJoin.vat() == vat); require(plan.dai() == dai); @@ -1113,7 +1132,7 @@ rule exec_exec(bytes32 ilk) { uint256 maxDeposit = pool.maxDeposit(e); uint256 assetsBefore = pool.assetBalance(e); - uint256 targetAssets = plan.getTargetAssets(e, assetsBefore); + uint256 targetAssets = plan.getTargetAssets(e, ilk, assetsBefore); require(maxDeposit > targetAssets - assetsBefore); require(assetsBefore <= safe_max()); diff --git a/certora/d3m/D3MTestPlan.sol b/certora/d3m/D3MTestPlan.sol index 49ab4941..14aca757 100644 --- a/certora/d3m/D3MTestPlan.sol +++ b/certora/d3m/D3MTestPlan.sol @@ -78,7 +78,7 @@ contract D3MTestPlan is ID3MPlan { return maxBar_; } - function getTargetAssets(uint256 currentAssets) external override view returns (uint256) { + function getTargetAssets(bytes32, uint256 currentAssets) external override view returns (uint256) { currentAssets; return bar > 0 ? targetAssets : 0; diff --git a/certora/d3m/D3MTestPool.sol b/certora/d3m/D3MTestPool.sol index a45c4b60..3b66ea13 100644 --- a/certora/d3m/D3MTestPool.sol +++ b/certora/d3m/D3MTestPool.sol @@ -152,6 +152,14 @@ contract D3MTestPool is ID3MPool { return _min(TokenMock(dai).balanceOf(share), assetBalance()); } + function liquidityAvailable() external view override returns (uint256) { + return TokenMock(dai).balanceOf(share); + } + + function idleLiquidity() external view override returns (uint256) { + return TokenMock(dai).balanceOf(share); + } + function shareBalance() public view returns (uint256) { return TokenMock(share).balanceOf(address(this)); } diff --git a/script/D3MCoreDeploy.s.sol b/script/D3MCoreDeploy.s.sol index 80f6c6d5..ed6f5a28 100644 --- a/script/D3MCoreDeploy.s.sol +++ b/script/D3MCoreDeploy.s.sol @@ -22,8 +22,7 @@ import { MCD, DssInstance } from "dss-test/MCD.sol"; import { ScriptTools } from "dss-test/ScriptTools.sol"; import { - D3MDeploy, - D3MCoreInstance + D3MDeploy } from "../src/deploy/D3MDeploy.sol"; contract D3MCoreDeployScript is Script { @@ -37,7 +36,6 @@ contract D3MCoreDeployScript is Script { DssInstance dss; address admin; - D3MCoreInstance d3mCore; function run() external { config = ScriptTools.loadConfig(NAME); @@ -46,15 +44,18 @@ contract D3MCoreDeployScript is Script { admin = config.readAddress(".admin"); vm.startBroadcast(); - d3mCore = D3MDeploy.deployCore( + address hub = D3MDeploy.deployHub( msg.sender, admin, address(dss.daiJoin) ); + address mom = D3MDeploy.deployMom( + admin + ); vm.stopBroadcast(); - ScriptTools.exportContract(NAME, "hub", d3mCore.hub); - ScriptTools.exportContract(NAME, "mom", d3mCore.mom); + ScriptTools.exportContract(NAME, "hub", hub); + ScriptTools.exportContract(NAME, "mom", mom); } } diff --git a/script/D3MCoreInit.s.sol b/script/D3MCoreInit.s.sol index 67d52fc2..c23885f0 100644 --- a/script/D3MCoreInit.s.sol +++ b/script/D3MCoreInit.s.sol @@ -22,8 +22,7 @@ import { MCD, DssInstance } from "dss-test/MCD.sol"; import { ScriptTools } from "dss-test/ScriptTools.sol"; import { - D3MInit, - D3MCoreInstance + D3MInit } from "../src/deploy/D3MInit.sol"; contract D3MCoreInitScript is Script { @@ -37,22 +36,19 @@ contract D3MCoreInitScript is Script { string dependencies; DssInstance dss; - D3MCoreInstance d3mCore; - function run() external { config = ScriptTools.loadConfig(NAME); dependencies = ScriptTools.loadDependencies(NAME); dss = MCD.loadFromChainlog(config.readAddress(".chainlog")); - d3mCore = D3MCoreInstance({ - hub: dependencies.readAddress(".hub"), - mom: dependencies.readAddress(".mom") - }); - vm.startBroadcast(); - D3MInit.initCore( + D3MInit.initHub( + dss, + dependencies.readAddress(".hub") + ); + D3MInit.initMom( dss, - d3mCore + dependencies.readAddress(".mom") ); vm.stopBroadcast(); } diff --git a/script/D3MDeploy.s.sol b/script/D3MDeploy.s.sol index 68a3adfb..8b7d0b0e 100644 --- a/script/D3MDeploy.s.sol +++ b/script/D3MDeploy.s.sol @@ -45,6 +45,7 @@ contract D3MDeployScript is Script { string poolType; string planType; + string feesType; address admin; address hub; bytes32 ilk; @@ -62,6 +63,7 @@ contract D3MDeployScript is Script { poolType = config.readString(".poolType"); planType = config.readString(".planType"); + feesType = config.readString(".feesType"); admin = config.readAddress(".admin"); hub = dependencies.eq("") ? dss.chainlog.getAddress("DIRECT_HUB") : dependencies.readAddress(".hub"); ilk = config.readString(".ilk").stringToBytes32(); @@ -138,10 +140,22 @@ contract D3MDeployScript is Script { } else { revert("Unknown plan type"); } + + // Fees + if (planType.eq("forward")) { + d3m.fees = D3MDeploy.deployForwardFees( + address(dss.vat), + config.readAddress(".feesForwardTo") + ); + } else { + revert("Unknown fee type"); + } + vm.stopBroadcast(); ScriptTools.exportContract("pool", d3m.pool); ScriptTools.exportContract("plan", d3m.plan); + ScriptTools.exportContract("fees", d3m.fees); ScriptTools.exportContract("oracle", d3m.oracle); } diff --git a/script/D3MInit.s.sol b/script/D3MInit.s.sol index c02d9d18..c5739a95 100644 --- a/script/D3MInit.s.sol +++ b/script/D3MInit.s.sol @@ -70,6 +70,7 @@ contract D3MInitScript is Script { d3m = D3MInstance({ pool: dependencies.readAddress(".pool"), plan: dependencies.readAddress(".plan"), + fees: dependencies.readAddress(".fees"), oracle: dependencies.readAddress(".oracle") }); cfg = D3MCommonConfig({ diff --git a/script/input/1/template-aave.json b/script/input/1/template-aave.json index 77ecd5f2..fcef05c5 100644 --- a/script/input/1/template-aave.json +++ b/script/input/1/template-aave.json @@ -3,6 +3,7 @@ "admin": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", "poolType": "aave-v2", "planType": "rate-target", + "feesType": "forward", "ilk": "DIRECT-AAVEV2-DAI", "existingIlk": false, "maxLine": 5000000, @@ -11,5 +12,6 @@ "tau": 604800, "lendingPool": "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", "king": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", - "bar": 370 + "bar": 370, + "feesForwardTo": "0xA950524441892A31ebddF91d3cEEFa04Bf454466" } diff --git a/script/input/1/template-compound.json b/script/input/1/template-compound.json index 907fbd9f..ecbf249b 100644 --- a/script/input/1/template-compound.json +++ b/script/input/1/template-compound.json @@ -3,6 +3,7 @@ "admin": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", "poolType": "compound-v2", "planType": "rate-target", + "feesType": "forward", "ilk": "DIRECT-COMPV2-DAI", "existingIlk": false, "maxLine": 5000000, @@ -11,5 +12,6 @@ "tau": 604800, "cdai": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", "king": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", - "barb": 7535450719 + "barb": 7535450719, + "feesForwardTo": "0xA950524441892A31ebddF91d3cEEFa04Bf454466" } diff --git a/script/input/1/template-spark.json b/script/input/1/template-spark.json index 73a71224..cbb63fd9 100644 --- a/script/input/1/template-spark.json +++ b/script/input/1/template-spark.json @@ -3,6 +3,7 @@ "admin": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", "poolType": "aave-v3-no-supply-cap", "planType": "liquidity-buffer", + "feesType": "forward", "ilk": "DIRECT-SPARK-DAI", "existingIlk": false, "maxLine": 5000000, @@ -12,5 +13,6 @@ "lendingPool": "0xC13e21B648A5Ee794902342038FF3aDAB66BE987", "king": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB", "adai": "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B", - "buffer": 5000000 + "buffer": 5000000, + "feesForwardTo": "0xA950524441892A31ebddF91d3cEEFa04Bf454466" } diff --git a/script/input/5/template-aave.json b/script/input/5/template-aave.json index cb0b428b..366dfe51 100644 --- a/script/input/5/template-aave.json +++ b/script/input/5/template-aave.json @@ -3,6 +3,7 @@ "admin": "0x5DCdbD3cCF9B09EAAD03bc5f50fA2B3d3ACA0121", "poolType": "aave-v2", "planType": "rate-target", + "feesType": "forward", "ilk": "DIRECT-AAVEV2-DAI", "existingIlk": false, "maxLine": 5000000, @@ -11,5 +12,6 @@ "tau": 604800, "lendingPool": "0x368EedF3f56ad10b9bC57eed4Dac65B26Bb667f6", "king": "0x5DCdbD3cCF9B09EAAD03bc5f50fA2B3d3ACA0121", - "bar": 370 + "bar": 370, + "feesForwardTo": "0x23f78612769b9013b3145E43896Fa1578cAa2c2a" } diff --git a/script/input/5/template-compound.json b/script/input/5/template-compound.json index 41493702..87721df0 100644 --- a/script/input/5/template-compound.json +++ b/script/input/5/template-compound.json @@ -3,6 +3,7 @@ "admin": "0x5DCdbD3cCF9B09EAAD03bc5f50fA2B3d3ACA0121", "poolType": "compound-v2", "planType": "rate-target", + "feesType": "forward", "ilk": "DIRECT-COMPV2-DAI", "existingIlk": false, "maxLine": 5000000, @@ -11,5 +12,6 @@ "tau": 604800, "cdai": "0x0000000000000000000000000000000000000000", "king": "0x5DCdbD3cCF9B09EAAD03bc5f50fA2B3d3ACA0121", - "barb": 7535450719 + "barb": 7535450719, + "feesForwardTo": "0x23f78612769b9013b3145E43896Fa1578cAa2c2a" } diff --git a/script/input/5/template-spark.json b/script/input/5/template-spark.json index 92d6423f..959093af 100644 --- a/script/input/5/template-spark.json +++ b/script/input/5/template-spark.json @@ -3,6 +3,7 @@ "admin": "0x5DCdbD3cCF9B09EAAD03bc5f50fA2B3d3ACA0121", "poolType": "aave-v3-no-supply-cap", "planType": "liquidity-buffer", + "feesType": "forward", "ilk": "DIRECT-SPARK-DAI", "existingIlk": false, "maxLine": 5000000, @@ -12,5 +13,6 @@ "lendingPool": "0x26ca51Af4506DE7a6f0785D20CD776081a05fF6d", "king": "0x5DCdbD3cCF9B09EAAD03bc5f50fA2B3d3ACA0121", "adai": "0x4480b29AB7a1b0761e6d0d480325de28B7266d73", - "buffer": 5000000 + "buffer": 5000000, + "feesForwardTo": "0x23f78612769b9013b3145E43896Fa1578cAa2c2a" } diff --git a/src/D3MHub.sol b/src/D3MHub.sol index f4809a43..02f2de2f 100644 --- a/src/D3MHub.sol +++ b/src/D3MHub.sol @@ -16,8 +16,9 @@ pragma solidity ^0.8.14; -import "./pools/ID3MPool.sol"; -import "./plans/ID3MPlan.sol"; +import { ID3MPool } from "./pools/ID3MPool.sol"; +import { ID3MPlan } from "./plans/ID3MPlan.sol"; +import { ID3MFees } from "./fees/ID3MFees.sol"; interface VatLike { function debt() external view returns (uint256); @@ -87,6 +88,7 @@ contract D3MHub { struct Ilk { ID3MPool pool; // Access external pool and holds balances ID3MPlan plan; // How we calculate target debt + ID3MFees fees; // Fee logic handler uint256 tau; // Time until you can write off the debt [sec] uint256 culled; // Debt write off triggered uint256 tic; // Timestamp when the d3m can be culled (tau + timestamp when caged) @@ -216,6 +218,7 @@ contract D3MHub { if (what == "pool") ilks[ilk].pool = ID3MPool(data); else if (what == "plan") ilks[ilk].plan = ID3MPlan(data); + else if (what == "fees") ilks[ilk].fees = ID3MFees(data); else revert("D3MHub/file-unrecognized-param"); emit File(ilk, what, data); } @@ -267,17 +270,19 @@ contract D3MHub { } // Get the DAI and send as surplus (if there was permissionless DAI paid or fees accounted) if (art < ink) { - address _vow = vow; uint256 fixArt; unchecked { fixArt = ink - art; // Amount of fees + permissionless DAI paid we will now transform to debt } art = ink; - vat.suck(_vow, _vow, fixArt * RAY); // This needs to be done to make sure we can deduct sin[vow] and vice in the next call + ID3MFees _fees = ilks[ilk].fees; + uint256 feesRad = fixArt * RAY; + vat.suck(address(_fees), address(_fees), feesRad); // This needs to be done to make sure we can deduct sin[_fees] and vice in the next call // No need for `fixArt <= MAXINT256` require as: // MAXINT256 >>> MAXUINT256 / RAY which is already restricted above // Also fixArt should be always <= SAFEMAX (MAXINT256 / RAY) - vat.grab(ilk, address(_pool), address(_pool), _vow, 0, int256(fixArt)); // Generating the debt + vat.grab(ilk, address(_pool), address(_pool), address(_fees), 0, int256(fixArt)); // Generating the debt + _fees.feesCollected(ilk, feesRad); } // Determine if it needs to unwind or wind @@ -292,7 +297,7 @@ contract D3MHub { } else { uint256 Line = vat.Line(); uint256 debt = vat.debt(); - uint256 targetAssets = ilks[ilk].plan.getTargetAssets(currentAssets); + uint256 targetAssets = ilks[ilk].plan.getTargetAssets(ilk, currentAssets); // Determine if it needs to unwind due to: unchecked { @@ -501,6 +506,15 @@ contract D3MHub { return address(ilks[ilk].plan); } + /** + @notice Return fees of an ilk + @param ilk bytes32 of the D3M ilk + @return fees address of fees contract + */ + function fees(bytes32 ilk) external view returns (address) { + return address(ilks[ilk].fees); + } + /** @notice Return tau of an ilk @param ilk bytes32 of the D3M ilk diff --git a/src/deploy/D3MDeploy.sol b/src/deploy/D3MDeploy.sol index f85652b8..de13c5dc 100644 --- a/src/deploy/D3MDeploy.sol +++ b/src/deploy/D3MDeploy.sol @@ -20,31 +20,41 @@ import "dss-interfaces/Interfaces.sol"; import { DssInstance } from "dss-test/MCD.sol"; import { ScriptTools } from "dss-test/ScriptTools.sol"; -import { D3MCoreInstance } from "./D3MCoreInstance.sol"; import { D3MHub } from "../D3MHub.sol"; import { D3MMom } from "../D3MMom.sol"; +import { D3MOracle } from "../D3MOracle.sol"; import { D3MInstance } from "./D3MInstance.sol"; import { D3MAaveV2TypeRateTargetPlan } from "../plans/D3MAaveV2TypeRateTargetPlan.sol"; import { D3MAaveTypeBufferPlan } from "../plans/D3MAaveTypeBufferPlan.sol"; +import { D3MCompoundV2TypeRateTargetPlan } from "../plans/D3MCompoundV2TypeRateTargetPlan.sol"; +import { D3MALMDelegateControllerPlan } from "../plans/D3MALMDelegateControllerPlan.sol"; 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 { D3MOracle } from "../D3MOracle.sol"; +import { D3MLinearFeeSwapPool } from "../pools/D3MLinearFeeSwapPool.sol"; +import { D3MGatedSwapPool } from "../pools/D3MGatedSwapPool.sol"; +import { D3MGatedOffchainSwapPool } from "../pools/D3MGatedOffchainSwapPool.sol"; +import { D3MForwardFees } from "../fees/D3MForwardFees.sol"; // Deploy a D3M instance library D3MDeploy { - function deployCore( + function deployHub( address deployer, address owner, address daiJoin - ) internal returns (D3MCoreInstance memory d3mCore) { - d3mCore.hub = address(new D3MHub(daiJoin)); - d3mCore.mom = address(new D3MMom()); + ) internal returns (address hub) { + hub = address(new D3MHub(daiJoin)); + + ScriptTools.switchOwner(hub, deployer, owner); + } - ScriptTools.switchOwner(d3mCore.hub, deployer, owner); - DSAuthAbstract(d3mCore.mom).setOwner(owner); + function deployMom( + address owner + ) internal returns (address mom) { + mom = address(new D3MMom()); + + DSAuthAbstract(mom).setOwner(owner); } function deployOracle( @@ -127,4 +137,59 @@ library D3MDeploy { ScriptTools.switchOwner(plan, deployer, owner); } + function deployLinearFeeSwapPool( + address deployer, + address owner, + bytes32 ilk, + address hub, + address dai, + address gem + ) internal returns (address pool) { + pool = address(new D3MLinearFeeSwapPool(ilk, hub, dai, gem)); + + ScriptTools.switchOwner(pool, deployer, owner); + } + + function deployGatedSwapPool( + address deployer, + address owner, + bytes32 ilk, + address hub, + address dai, + address gem + ) internal returns (address pool) { + pool = address(new D3MGatedSwapPool(ilk, hub, dai, gem)); + + ScriptTools.switchOwner(pool, deployer, owner); + } + + function deployGatedOffchainSwapPool( + address deployer, + address owner, + bytes32 ilk, + address hub, + address dai, + address gem + ) internal returns (address pool) { + pool = address(new D3MGatedOffchainSwapPool(ilk, hub, dai, gem)); + + ScriptTools.switchOwner(pool, deployer, owner); + } + + function deployALMDelegateControllerPlan( + address deployer, + address owner + ) internal returns (address plan) { + plan = address(new D3MALMDelegateControllerPlan()); + + ScriptTools.switchOwner(plan, deployer, owner); + } + + function deployForwardFees( + address vat, + address target + ) internal returns (address fees) { + fees = address(new D3MForwardFees(vat, target)); + } + } diff --git a/src/deploy/D3MInit.sol b/src/deploy/D3MInit.sol index b08f804e..8fc6d59f 100644 --- a/src/deploy/D3MInit.sol +++ b/src/deploy/D3MInit.sol @@ -24,7 +24,6 @@ import { DssInstance } from "dss-test/MCD.sol"; import { ScriptTools } from "dss-test/ScriptTools.sol"; import { D3MInstance } from "./D3MInstance.sol"; -import { D3MCoreInstance } from "./D3MCoreInstance.sol"; interface D3MAavePoolLike { function hub() external view returns (address); @@ -80,6 +79,15 @@ interface CDaiLike { function implementation() external view returns (address); } +interface D3MSwapPoolLike { + 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 gem() external view returns (address); + function file(bytes32, address) external; +} + interface D3MOracleLike { function vat() external view returns (address); function ilk() external view returns (bytes32); @@ -148,15 +156,21 @@ struct D3MCompoundRateTargetPlanConfig { address delegate; } +struct D3MSwapPoolConfig { + address gem; + address pip; + address swapDaiForGemPip; + address swapGemForDaiPip; +} + // Init a D3M instance library D3MInit { - function initCore( + function initHub( DssInstance memory dss, - D3MCoreInstance memory d3mCore + address _hub ) internal { - D3MHubLike hub = D3MHubLike(d3mCore.hub); - D3MMomLike mom = D3MMomLike(d3mCore.mom); + D3MHubLike hub = D3MHubLike(_hub); // Sanity checks require(hub.vat() == address(dss.vat), "Hub vat mismatch"); @@ -165,11 +179,30 @@ library D3MInit { hub.file("vow", address(dss.vow)); hub.file("end", address(dss.end)); - mom.setAuthority(dss.chainlog.getAddress("MCD_ADM")); - dss.vat.rely(address(hub)); dss.chainlog.setAddress("DIRECT_HUB", address(hub)); + } + + function deactivateHub( + DssInstance memory dss, + address _hub + ) internal { + D3MHubLike hub = D3MHubLike(_hub); + + dss.vat.deny(address(hub)); + + dss.chainlog.removeAddress("DIRECT_HUB"); + } + + function initMom( + DssInstance memory dss, + address _mom + ) internal { + D3MMomLike mom = D3MMomLike(_mom); + + mom.setAuthority(dss.chainlog.getAddress("MCD_ADM")); + dss.chainlog.setAddress("DIRECT_MOM", address(mom)); } @@ -190,6 +223,7 @@ library D3MInit { hub.file(ilk, "pool", d3m.pool); hub.file(ilk, "plan", d3m.plan); + hub.file(ilk, "fees", d3m.fees); hub.file(ilk, "tau", cfg.tau); oracle.file("hub", address(hub)); @@ -275,6 +309,26 @@ library D3MInit { pool.file("king", compoundCfg.king); } + function initSwapPool( + DssInstance memory dss, + D3MInstance memory d3m, + D3MCommonConfig memory cfg, + D3MSwapPoolConfig memory swapPoolCfg + ) internal { + D3MSwapPoolLike pool = D3MSwapPoolLike(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.gem() == swapPoolCfg.gem, "Pool gem mismatch"); + + pool.file("pip", swapPoolCfg.pip); + pool.file("swapDaiForGemPip", swapPoolCfg.swapDaiForGemPip); + pool.file("swapGemForDaiPip", swapPoolCfg.swapGemForDaiPip); + } + function initAaveRateTargetPlan( D3MInstance memory d3m, D3MAaveRateTargetPlanConfig memory aaveCfg diff --git a/src/deploy/D3MInstance.sol b/src/deploy/D3MInstance.sol index eb3ed5ca..e6f788a5 100644 --- a/src/deploy/D3MInstance.sol +++ b/src/deploy/D3MInstance.sol @@ -19,5 +19,6 @@ pragma solidity >=0.8.0; struct D3MInstance { address plan; address pool; + address fees; address oracle; } diff --git a/src/fees/D3MForwardFees.sol b/src/fees/D3MForwardFees.sol new file mode 100644 index 00000000..056761f5 --- /dev/null +++ b/src/fees/D3MForwardFees.sol @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: © 2023 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 "./ID3MFees.sol"; + +interface VatLike { + function dai(address) external view returns (uint256); + function move(address, address, uint256) external; +} + +/** + * @title Forward Fees + * @notice Forwards all fees to a single contract + */ +contract D3MForwardFees is ID3MFees { + + VatLike public immutable vat; + address public immutable target; + + constructor(address _vat, address _target) { + vat = VatLike(_vat); + target = _target; + } + + function feesCollected(bytes32 ilk, uint256) external { + uint256 dai = vat.dai(address(this)); // Maybe someone permissionlessly sent us DAI + vat.move(address(this), target, dai); + + emit FeesCollected(ilk, dai); + } + +} diff --git a/src/fees/ID3MFees.sol b/src/fees/ID3MFees.sol new file mode 100644 index 00000000..25fbd2c9 --- /dev/null +++ b/src/fees/ID3MFees.sol @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: © 2023 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.0; + +/** + @title D3M Fees Interface + @notice Receives fees from the Hub and distributes them +*/ +interface ID3MFees { + + /** + * @notice Emitted when fees are collected + * @param ilk The ilk where the fees were collected + * @param fees The amount of fees collected [rad] + */ + event FeesCollected(bytes32 indexed ilk, uint256 fees); + + /** + * @notice Called after fees have been received. + * @param ilk The ilk where the fees were collected + * @param fees The amount of fees collected [rad] + */ + function feesCollected(bytes32 ilk, uint256 fees) external; + +} diff --git a/src/plans/D3MALMDelegateControllerPlan.sol b/src/plans/D3MALMDelegateControllerPlan.sol new file mode 100644 index 00000000..cd25b39e --- /dev/null +++ b/src/plans/D3MALMDelegateControllerPlan.sol @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: © 2023 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"; +import "../utils/EnumerableSet.sol"; + +/** + * @title D3M ALM Delegate Controller + * @notice Allocators can set debt ceilings for target ilks. Delegate addresses can be added for custom logic. + */ +contract D3MALMDelegateControllerPlan is ID3MPlan { + + using EnumerableSet for EnumerableSet.AddressSet; + + struct IlkAllocation { + uint256 total; // The total allocated to this ilk [wad] + EnumerableSet.AddressSet allocators; // The list of allocators that have allocated to this ilk + } + + struct Allocation { + uint128 current; // The current allocation for this allocator [wad] + uint128 max; // The maximum allocation for this allocator [wad] + } + + mapping (address => uint256) public wards; + mapping (address => uint256) public allocators; // Allocators can set debt ceilings for target ilks + mapping (address => mapping (address => uint256)) public allocatorDelegates; // Allocators can delegate authority to custom logic / wallets + mapping (bytes32 => IlkAllocation) private _ilkAllocations; + mapping (address => mapping (bytes32 => Allocation)) public allocations; + + uint256 public enabled = 1; + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, uint256 data); + event AddAllocator(address indexed allocator); + event RemoveAllocator(address indexed allocator); + event SetMaxAllocation(address indexed allocator, bytes32 indexed ilk, uint128 max); + event AddAllocatorDelegate(address indexed allocator, address indexed allocatorDelegate); + event RemoveAllocatorDelegate(address indexed allocator, address indexed allocatorDelegate); + event SetAllocation(address indexed allocator, bytes32 indexed ilk, uint128 previousAllocation, uint128 newAllocation); + + constructor() { + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + modifier auth { + require(wards[msg.sender] == 1, "D3MALMDelegateControllerPlan/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, uint256 data) external auth { + if (what == "enabled") { + require(data <= 1, "D3MALMDelegateControllerPlan/invalid-value"); + enabled = data; + } else revert("D3MALMDelegateControllerPlan/file-unrecognized-param"); + emit File(what, data); + } + + function addAllocator(address allocator) external auth { + allocators[allocator] = 1; + emit AddAllocator(allocator); + } + + function removeAllocator(address allocator) external auth { + allocators[allocator] = 0; + emit RemoveAllocator(allocator); + } + + function setMaxAllocation(address allocator, bytes32 ilk, uint128 max) external auth { + allocations[allocator][ilk].max = max; + emit SetMaxAllocation(allocator, ilk, max); + + uint128 currentAllocation = allocations[allocator][ilk].current; + if (max < currentAllocation) { + _setAllocation(allocator, ilk, currentAllocation, max); + } + } + + // --- Allocator Control --- + function addAllocatorDelegate(address allocator, address allocatorDelegate) external { + require((allocator == msg.sender && allocators[allocator] == 1) || wards[msg.sender] == 1, "D3MALMDelegateControllerPlan/not-authorized"); + + allocatorDelegates[allocator][allocatorDelegate] = 1; + emit AddAllocatorDelegate(allocator, allocatorDelegate); + } + + function removeAllocatorDelegate(address allocator, address allocatorDelegate) external { + require((allocator == msg.sender && allocators[allocator] == 1) || wards[msg.sender] == 1, "D3MALMDelegateControllerPlan/not-authorized"); + + allocatorDelegates[allocator][allocatorDelegate] = 0; + emit RemoveAllocatorDelegate(allocator, allocatorDelegate); + } + + function setAllocation(address allocator, bytes32 ilk, uint128 amount) external { + require( + ( + allocators[allocator] == 1 && + (allocator == msg.sender || allocatorDelegates[allocator][msg.sender] == 1) + ) || + wards[msg.sender] == 1 + , "D3MALMDelegateControllerPlan/not-authorized"); + Allocation memory allocation = allocations[allocator][ilk]; + require(amount <= allocation.max, "D3MALMDelegateControllerPlan/amount-exceeds-max"); + + _setAllocation(allocator, ilk, allocation.current, amount); + } + + function _setAllocation(address allocator, bytes32 ilk, uint128 currentAllocation, uint128 newAllocation) internal { + allocations[allocator][ilk].current = newAllocation; + _ilkAllocations[ilk].total = _ilkAllocations[ilk].total - currentAllocation + newAllocation; + if (newAllocation > 0 && !_ilkAllocations[ilk].allocators.contains(allocator)) { + _ilkAllocations[ilk].allocators.add(allocator); + } else if (newAllocation == 0 && _ilkAllocations[ilk].allocators.contains(allocator)) { + _ilkAllocations[ilk].allocators.remove(allocator); + } + emit SetAllocation(allocator, ilk, currentAllocation, newAllocation); + } + + // --- Getter Functions --- + function totalAllocated(bytes32 ilk) external view returns (uint256) { + return _ilkAllocations[ilk].total; + } + + function numAllocations(bytes32 ilk) external view returns (uint256) { + return _ilkAllocations[ilk].allocators.length(); + } + + function allocatorAt(bytes32 ilk, uint256 index) external view returns (address) { + return _ilkAllocations[ilk].allocators.at(index); + } + + function hasAllocator(bytes32 ilk, address allocator) external view returns (bool) { + return _ilkAllocations[ilk].allocators.contains(allocator); + } + + // --- IPlan Functions --- + function getTargetAssets(bytes32 ilk, uint256) external override view returns (uint256) { + if (enabled == 0) return 0; + + return _ilkAllocations[ilk].total; + } + + function active() public view override returns (bool) { + return enabled == 1; + } + + function disable() external override auth { + enabled = 0; + emit Disable(); + } + +} diff --git a/src/plans/D3MAaveTypeBufferPlan.sol b/src/plans/D3MAaveTypeBufferPlan.sol index 55cce9a4..8ab1030b 100644 --- a/src/plans/D3MAaveTypeBufferPlan.sol +++ b/src/plans/D3MAaveTypeBufferPlan.sol @@ -74,7 +74,7 @@ contract D3MAaveTypeBufferPlan is ID3MPlan { emit File(what, data); } - function getTargetAssets(uint256 currentAssets) external override view returns (uint256) { + function getTargetAssets(bytes32, uint256 currentAssets) external override view returns (uint256) { if (buffer == 0) return 0; // De-activated // Note that this can be manipulated by flash loans diff --git a/src/plans/D3MAaveV2TypeRateTargetPlan.sol b/src/plans/D3MAaveV2TypeRateTargetPlan.sol index f54a4e5c..abb661e7 100644 --- a/src/plans/D3MAaveV2TypeRateTargetPlan.sol +++ b/src/plans/D3MAaveV2TypeRateTargetPlan.sol @@ -170,7 +170,7 @@ contract D3MAaveV2TypeRateTargetPlan is ID3MPlan { // Note: This view function has no reentrancy protection. // On chain integrations should consider verifying `hub.locked()` is zero before relying on it. - function getTargetAssets(uint256 currentAssets) external override view returns (uint256) { + function getTargetAssets(bytes32, uint256 currentAssets) external override view returns (uint256) { uint256 targetInterestRate = bar; if (targetInterestRate == 0) return 0; // De-activated diff --git a/src/plans/D3MCompoundV2TypeRateTargetPlan.sol b/src/plans/D3MCompoundV2TypeRateTargetPlan.sol index 4ca960b1..8cbe3dcb 100644 --- a/src/plans/D3MCompoundV2TypeRateTargetPlan.sol +++ b/src/plans/D3MCompoundV2TypeRateTargetPlan.sol @@ -167,7 +167,7 @@ contract D3MCompoundV2TypeRateTargetPlan is ID3MPlan { // Note: This view function has no reentrancy protection. // On chain integrations should consider verifying `hub.locked()` is zero before relying on it. - function getTargetAssets(uint256 currentAssets) external override view returns (uint256) { + function getTargetAssets(bytes32, uint256 currentAssets) external override view returns (uint256) { uint256 targetInterestRate = barb; if (targetInterestRate == 0) return 0; // De-activated diff --git a/src/plans/ID3MPlan.sol b/src/plans/ID3MPlan.sol index 5b34b926..15ce6f06 100644 --- a/src/plans/ID3MPlan.sol +++ b/src/plans/ID3MPlan.sol @@ -27,11 +27,12 @@ interface ID3MPlan { /** @notice Determines what the position should be based on current assets and the custom plan rules. + @param ilk the ilk to calculate the target assets for @param currentAssets asset balance from a specific pool in Dai [wad] denomination @return uint256 target assets the Hub should wind or unwind to in Dai */ - function getTargetAssets(uint256 currentAssets) external view returns (uint256); + function getTargetAssets(bytes32 ilk, uint256 currentAssets) external view returns (uint256); /// @notice Reports whether the plan is active function active() external view returns (bool); diff --git a/src/pools/D3MAaveV2TypePool.sol b/src/pools/D3MAaveV2TypePool.sol index 931249d9..5f5ef1bf 100644 --- a/src/pools/D3MAaveV2TypePool.sol +++ b/src/pools/D3MAaveV2TypePool.sol @@ -19,6 +19,7 @@ pragma solidity ^0.8.14; import "./ID3MPool.sol"; interface TokenLike { + function totalSupply() external view returns (uint256); function balanceOf(address) external view returns (uint256); function approve(address, uint256) external returns (bool); function transfer(address, uint256) external returns (bool); @@ -210,6 +211,17 @@ contract D3MAaveV2TypePool is ID3MPool { return _min(dai.balanceOf(address(adai)), assetBalance()); } + function liquidityAvailable() external view override returns (uint256) { + return dai.balanceOf(address(adai)); + } + + function idleLiquidity() external view override returns (uint256) { + uint256 totalDebt = stableDebt.totalSupply() + variableDebt.totalSupply(); + uint256 totalPoolSize = dai.balanceOf(address(adai)) + totalDebt; + if (totalPoolSize == 0) return adai.balanceOf(address(this)); + return adai.balanceOf(address(this)) * (totalPoolSize - totalDebt) / totalPoolSize; + } + function redeemable() external view override returns (address) { return address(adai); } diff --git a/src/pools/D3MAaveV3NoSupplyCapTypePool.sol b/src/pools/D3MAaveV3NoSupplyCapTypePool.sol index 0d2c9d34..feddf2c1 100644 --- a/src/pools/D3MAaveV3NoSupplyCapTypePool.sol +++ b/src/pools/D3MAaveV3NoSupplyCapTypePool.sol @@ -19,6 +19,7 @@ pragma solidity ^0.8.14; import "./ID3MPool.sol"; interface TokenLike { + function totalSupply() external view returns (uint256); function balanceOf(address) external view returns (uint256); function approve(address, uint256) external returns (bool); function transfer(address, uint256) external returns (bool); @@ -231,6 +232,17 @@ contract D3MAaveV3NoSupplyCapTypePool is ID3MPool { return _min(dai.balanceOf(address(adai)), assetBalance()); } + function liquidityAvailable() external view override returns (uint256) { + return dai.balanceOf(address(adai)); + } + + function idleLiquidity() external view override returns (uint256) { + uint256 totalDebt = stableDebt.totalSupply() + variableDebt.totalSupply(); + uint256 totalPoolSize = dai.balanceOf(address(adai)) + totalDebt; + if (totalPoolSize == 0) return adai.balanceOf(address(this)); + return adai.balanceOf(address(this)) * (totalPoolSize - totalDebt) / totalPoolSize; + } + function redeemable() external view override returns (address) { return address(adai); } diff --git a/src/pools/D3MCompoundV2TypePool.sol b/src/pools/D3MCompoundV2TypePool.sol index 06958559..ebfc1c1e 100644 --- a/src/pools/D3MCompoundV2TypePool.sol +++ b/src/pools/D3MCompoundV2TypePool.sol @@ -41,6 +41,8 @@ interface EndLike { // cDai - https://github.com/compound-finance/compound-protocol/blob/master/contracts/CErc20.sol interface CErc20Like is TokenLike { + function totalBorrows() external view returns (uint256); + function totalReserves() external view returns (uint256); function underlying() external view returns (address); function comptroller() external view returns (address); function exchangeRateStored() external view returns (uint256); @@ -193,6 +195,17 @@ contract D3MCompoundV2TypePool is ID3MPool { return _min(cDai.getCash(), assetBalance()); } + function liquidityAvailable() external view override returns (uint256) { + return cDai.getCash(); + } + + function idleLiquidity() external view override returns (uint256) { + uint256 totalDebt = cDai.totalBorrows(); + uint256 totalPoolSize = cDai.getCash() + totalDebt /* - cDai.totalReserves() */; // TODO - check if we need to subtract reserves + if (totalPoolSize == 0) return assetBalance(); + return assetBalance() * (totalPoolSize - totalDebt) / totalPoolSize; + } + function redeemable() external view override returns (address) { return address(cDai); } diff --git a/src/pools/D3MGatedOffchainSwapPool.sol b/src/pools/D3MGatedOffchainSwapPool.sol new file mode 100644 index 00000000..c65f7fd9 --- /dev/null +++ b/src/pools/D3MGatedOffchainSwapPool.sol @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: © 2023 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 "./D3MGatedSwapPool.sol"; + +/** + * @title D3M Gated Offchain Swap Pool + * @notice Approved operators can add/remove gems from this pool for off-chain investment. + */ +contract D3MGatedOffchainSwapPool is D3MGatedSwapPool { + + // --- Data --- + mapping (address => uint256) public operators; + + uint256 public gemsOutstanding; + + // --- Events --- + event File(bytes32 indexed what, uint256 data); + event AddOperator(address indexed operator); + event RemoveOperator(address indexed operator); + + modifier onlyOperator { + require(operators[msg.sender] == 1, "D3MSwapPool/only-operator"); + _; + } + + constructor( + bytes32 _ilk, + address _hub, + address _dai, + address _gem + ) D3MGatedSwapPool(_ilk, _hub, _dai, _gem) { + } + + // --- Administration --- + + function file(bytes32 what, uint256 data) external auth { + require(vat.live() == 1, "D3MSwapPool/no-file-during-shutdown"); + + if (what == "gemsOutstanding") { + gemsOutstanding = data; + } else revert("D3MSwapPool/file-unrecognized-param"); + + emit File(what, data); + } + + function addOperator(address operator) external auth { + require(vat.live() == 1, "D3MSwapPool/no-addOperator-during-shutdown"); + + operators[operator] = 1; + emit AddOperator(operator); + } + + function removeOperator(address operator) external auth { + require(vat.live() == 1, "D3MSwapPool/no-removeOperator-during-shutdown"); + + operators[operator] = 0; + emit RemoveOperator(operator); + } + + // --- Pool Support --- + + function assetBalance() public view override returns (uint256) { + return dai.balanceOf(address(this)) + (gem.balanceOf(address(this)) + gemsOutstanding) * GEM_CONVERSION_FACTOR * uint256(pip.read()) / WAD; + } + + // --- Offchain push/pull + helper functions --- + + /** + * @notice Pull out the gem to invest off-chain. + * @param to The address to pull the gems to. + * @param amount The amount of gems to pull. + */ + function pull(address to, uint256 amount) external onlyOperator { + require(amount <= pendingDeposits(), "D3MSwapPool/amount-exceeds-pending"); + gemsOutstanding += amount; + require(gem.transfer(to, amount), "D3MSwapPool/failed-transfer"); + } + + /** + * @notice Repay the loan with gems. + * @param amount The amount of gems to repay. + */ + function push(uint256 amount) external onlyOperator { + require(gem.transferFrom(msg.sender, address(this), amount), "D3MSwapPool/failed-transfer"); + gemsOutstanding -= amount; + } + + /** + * @notice The amount of gems that should be deployed off-chain. + * @dev It's possible gems are in this adapter, but are earmarked to be exchanged back to DAI. + */ + function pendingDeposits() public view returns (uint256 gemAmt) { + uint256 conversionFactor = GEM_CONVERSION_FACTOR * uint256(pip.read()); + uint256 gemBalance = gem.balanceOf(address(this)); + uint256 gemsPlusOutstanding = (gemBalance + gemsOutstanding) * conversionFactor / WAD; + uint256 targetAssets = ID3MPlan(hub.plan(ilk)).getTargetAssets(ilk, gemsPlusOutstanding + dai.balanceOf(address(this))); + // We can ignore the DAI as that will just be removed right away + if (targetAssets >= gemsPlusOutstanding) { + // Target debt is higher than the current exposure + // Can deploy the full amount of gems + gemAmt = gemBalance; + } else { + // Note this rounds up towards the user, but it's not a big deal as it's a whitelisted user responsible for the entire principal + uint256 toBeRemoved = (gemsPlusOutstanding - targetAssets) * WAD / conversionFactor; + if (toBeRemoved < gemBalance) { + // Part of the gems are earmarked to be removed + gemAmt = gemBalance - toBeRemoved; + } else { + // All of the gems are earmarked to be removed + gemAmt = 0; + } + } + } + + /** + * @notice The amount of gems that should be returned by liquidating the off-chain position. + */ + function pendingWithdrawals() external view returns (uint256 gemAmt) { + uint256 conversionFactor = GEM_CONVERSION_FACTOR * uint256(pip.read()); + uint256 _gemsOutstanding = gemsOutstanding; + uint256 gemBalance = (gem.balanceOf(address(this)) + gemsOutstanding) * conversionFactor / WAD; + uint256 targetAssetsInGems = ID3MPlan(hub.plan(ilk)).getTargetAssets(ilk, gemBalance + dai.balanceOf(address(this))) * WAD / conversionFactor; + if (targetAssetsInGems < _gemsOutstanding) { + // Need to liquidate + gemAmt = _gemsOutstanding - targetAssetsInGems; + } else { + gemAmt = 0; + } + } + +} diff --git a/src/pools/D3MGatedSwapPool.sol b/src/pools/D3MGatedSwapPool.sol new file mode 100644 index 00000000..a77b9d76 --- /dev/null +++ b/src/pools/D3MGatedSwapPool.sol @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: © 2023 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 "./D3MSwapPool.sol"; +import {ID3MPlan} from "../plans/ID3MPlan.sol"; + +/** + * @title D3M Gated Swap Pool + * @notice Swaps are gated by comparing target assets to the amount of gems in the pool. + * The pool will only work to fill/empty to reach the desired target allocation. + */ +contract D3MGatedSwapPool is D3MSwapPool { + + struct FeeData { + uint128 tin; // toll in [wad] + uint128 tout; // toll out [wad] + } + + // --- Data --- + FeeData public feeData; + + // --- Events --- + event File(bytes32 indexed what, uint128 tin, uint128 tout); + + constructor( + bytes32 _ilk, + address _hub, + address _dai, + address _gem + ) D3MSwapPool(_ilk, _hub, _dai, _gem) { + // Initialize all fees to zero + feeData = FeeData({ + tin: uint128(WAD), + tout: uint128(WAD) + }); + } + + // --- Administration --- + + function file(bytes32 what, uint128 _tin, uint128 _tout) external auth { + require(vat.live() == 1, "D3MSwapPool/no-file-during-shutdown"); + // Please note we allow tin and tout to be both negative fees because swaps are gated by + // the desired target debt which only allows filling/emptying this pool in one direction + // at a time. + + if (what == "fees") { + feeData.tin = _tin; + feeData.tout = _tout; + } else revert("D3MSwapPool/file-unrecognized-param"); + + emit File(what, _tin, _tout); + } + + // --- Getters --- + + function tin() external view returns (uint256) { + return feeData.tin; + } + + function tout() external view returns (uint256) { + return feeData.tout; + } + + // --- Swaps --- + + function previewSwapGemForDai(uint256 gemAmt) public view override returns (uint256 daiAmt) { + uint256 currentAssets = assetBalance(); + uint256 gemBalance = currentAssets - dai.balanceOf(address(this)); + uint256 targetAssets = ID3MPlan(hub.plan(ilk)).getTargetAssets(ilk, currentAssets); + uint256 pipValue = uint256(swapGemForDaiPip.read()); + uint256 gemValue = gemAmt * GEM_CONVERSION_FACTOR * pipValue / WAD; + require(gemBalance + gemValue <= targetAssets, "D3MSwapPool/not-accepting-gems"); + FeeData memory _feeData = feeData; + daiAmt = gemValue * _feeData.tin / WAD; + } + + function previewSwapDaiForGem(uint256 daiAmt) public view override returns (uint256 gemAmt) { + uint256 currentAssets = assetBalance(); + uint256 gemBalance = currentAssets - dai.balanceOf(address(this)); + uint256 targetAssets = ID3MPlan(hub.plan(ilk)).getTargetAssets(ilk, currentAssets); + FeeData memory _feeData = feeData; + uint256 gemValue = daiAmt * _feeData.tout / WAD; + require(targetAssets + gemValue <= gemBalance, "D3MSwapPool/not-accepting-dai"); + uint256 pipValue = uint256(swapDaiForGemPip.read()); + gemAmt = gemValue * WAD / (GEM_CONVERSION_FACTOR * pipValue); + } + +} diff --git a/src/pools/D3MLinearFeeSwapPool.sol b/src/pools/D3MLinearFeeSwapPool.sol new file mode 100644 index 00000000..f48fbc8b --- /dev/null +++ b/src/pools/D3MLinearFeeSwapPool.sol @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: © 2023 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 "./D3MSwapPool.sol"; + +/** + * @title D3M Linear Fee Swap Pool + * @notice Swap an asset for DAI. Linear bonding curve with fee1 at 0% gems and fee2 at 100% gems. + */ +contract D3MLinearFeeSwapPool is D3MSwapPool { + + struct FeeData { + uint64 tin1; // toll in at 0% gems [wad] + uint64 tout1; // toll out at 0% gems [wad] + uint64 tin2; // toll in at 100% gems [wad] + uint64 tout2; // toll out at 100% gems [wad] + } + + // --- Data --- + FeeData public feeData; + + // --- Events --- + event File(bytes32 indexed what, uint64 tin, uint64 tout); + + constructor(bytes32 _ilk, address _hub, address _dai, address _gem) D3MSwapPool(_ilk, _hub, _dai, _gem) { + // Initialize all fees to zero + feeData = FeeData({ + tin1: uint64(WAD), + tout1: uint64(WAD), + tin2: uint64(WAD), + tout2: uint64(WAD) + }); + } + + // --- Administration --- + + function file(bytes32 what, uint64 tin, uint64 tout) external auth { + require(vat.live() == 1, "D3MSwapPool/no-file-during-shutdown"); + // We need to restrict tin/tout combinations to be less than 100% to avoid arbitragers able to endlessly take money + require(uint256(tin) * uint256(tout) <= WAD * WAD, "D3MSwapPool/invalid-fees"); + + if (what == "fees1") { + feeData.tin1 = tin; + feeData.tout1 = tout; + } else if (what == "fees2") { + feeData.tin2 = tin; + feeData.tout2 = tout; + } else revert("D3MSwapPool/file-unrecognized-param"); + + emit File(what, tin, tout); + } + + // --- Getters --- + + function tin1() external view returns (uint256) { + return feeData.tin1; + } + + function tout1() external view returns (uint256) { + return feeData.tout1; + } + + function tin2() external view returns (uint256) { + return feeData.tin2; + } + + function tout2() external view returns (uint256) { + return feeData.tout2; + } + + // --- Swaps --- + + function previewSwapGemForDai(uint256 gemAmt) public view override returns (uint256 daiAmt) { + if (gemAmt == 0) return 0; + + uint256 daiBalance = dai.balanceOf(address(this)); + uint256 pipValue = uint256(swapGemForDaiPip.read()); + uint256 gemValue = gemAmt * GEM_CONVERSION_FACTOR * pipValue / WAD; + require(daiBalance >= gemValue, "D3MSwapPool/insufficient-dai-in-pool"); + FeeData memory _feeData = feeData; + uint256 gemBalance = gem.balanceOf(address(this)) * GEM_CONVERSION_FACTOR * pipValue / WAD; + // Please note the fee deduction is not included in the new total dai+gem balance to drastically simplify the calculation + uint256 totalBalanceTimesTwo = (daiBalance + gemBalance) * 2; + uint256 g = 2 * gemBalance + gemValue; + daiAmt = gemValue * (_feeData.tin1 + _feeData.tin2 * g / totalBalanceTimesTwo - _feeData.tin1 * g / totalBalanceTimesTwo) / WAD; + } + + function previewSwapDaiForGem(uint256 daiAmt) public view override returns (uint256 gemAmt) { + if (daiAmt == 0) return 0; + + FeeData memory _feeData = feeData; + uint256 pipValue = uint256(swapDaiForGemPip.read()); + uint256 gemBalance = gem.balanceOf(address(this)) * GEM_CONVERSION_FACTOR * pipValue / WAD; + require(gemBalance >= daiAmt, "D3MSwapPool/insufficient-gems-in-pool"); + uint256 daiBalance = dai.balanceOf(address(this)); + // Please note the fee deduction is not included in the new total dai+gem balance to drastically simplify the calculation + uint256 totalBalanceTimesTwo = (daiBalance + gemBalance) * 2; + uint256 g = 2 * daiBalance + daiAmt; + gemAmt = daiAmt * (_feeData.tout2 + _feeData.tout1 * g / totalBalanceTimesTwo - _feeData.tout2 * g / totalBalanceTimesTwo) / (GEM_CONVERSION_FACTOR * pipValue); + } + +} diff --git a/src/pools/D3MSwapPool.sol b/src/pools/D3MSwapPool.sol new file mode 100644 index 00000000..9e599c12 --- /dev/null +++ b/src/pools/D3MSwapPool.sol @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: © 2023 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"; + +interface VatLike { + function live() external view returns (uint256); + function hope(address) external; + function nope(address) external; +} + +interface TokenLike { + function decimals() external view returns (uint8); + function balanceOf(address) external view returns (uint256); + function transfer(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); +} + +interface PipLike { + function read() external view returns (bytes32); +} + +interface HubLike { + function vat() external view returns (VatLike); + function end() external view returns (EndLike); + function plan(bytes32) external view returns (address); +} + +interface EndLike { + function Art(bytes32) external view returns (uint256); +} + +/** + * @title D3M Swap Pool + * @notice Swap an asset for DAI. Base contract to be extended to implement fee logic. + */ +abstract contract D3MSwapPool is ID3MPool { + + // --- Data --- + mapping (address => uint256) public wards; + + HubLike public hub; + PipLike public pip; + PipLike public swapGemForDaiPip; + PipLike public swapDaiForGemPip; + uint256 public exited; + + bytes32 immutable public ilk; + VatLike immutable public vat; + TokenLike immutable public dai; + TokenLike immutable public gem; + + uint256 constant internal WAD = 10 ** 18; + + uint256 immutable internal GEM_CONVERSION_FACTOR; + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, address data); + event SwapGemForDai(address indexed owner, uint256 gems, uint256 dai); + event SwapDaiForGem(address indexed owner, uint256 dai, uint256 gems); + + modifier auth { + require(wards[msg.sender] == 1, "D3MSwapPool/not-authorized"); + _; + } + + modifier onlyHub { + require(msg.sender == address(hub), "D3MSwapPool/only-hub"); + _; + } + + constructor(bytes32 _ilk, address _hub, address _dai, address _gem) { + wards[msg.sender] = 1; + emit Rely(msg.sender); + + ilk = _ilk; + hub = HubLike(_hub); + vat = HubLike(hub).vat(); + dai = TokenLike(_dai); + gem = TokenLike(_gem); + vat.hope(_hub); + + GEM_CONVERSION_FACTOR = 10 ** (18 - gem.decimals()); + } + + // --- Administration --- + + 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, "D3MSwapPool/no-file-during-shutdown"); + + if (what == "hub") { + vat.nope(address(hub)); + hub = HubLike(data); + vat.hope(data); + } else if (what == "pip") { + pip = PipLike(data); + } else if (what == "swapGemForDaiPip") { + swapGemForDaiPip = PipLike(data); + } else if (what == "swapDaiForGemPip") { + swapDaiForGemPip = PipLike(data); + } else revert("D3MSwapPool/file-unrecognized-param"); + + emit File(what, data); + } + + // --- Pool Support --- + + function deposit(uint256) external override onlyHub { + // Nothing to do + } + + function withdraw(uint256 wad) external override onlyHub { + dai.transfer(msg.sender, wad); + } + + function quit(address dst) external override auth { + require(vat.live() == 1, "D3MSwapPool/no-quit-during-shutdown"); + require(gem.transfer(dst, gem.balanceOf(address(this))), "D3MSwapPool/transfer-failed"); + dai.transfer(dst, dai.balanceOf(address(this))); + } + + function preDebtChange() external override {} + + function postDebtChange() external override {} + + function assetBalance() public view virtual returns (uint256) { + return dai.balanceOf(address(this)) + gem.balanceOf(address(this)) * GEM_CONVERSION_FACTOR * uint256(pip.read()) / WAD; + } + + function maxDeposit() external pure override returns (uint256) { + return type(uint256).max; + } + + function maxWithdraw() external view override returns (uint256) { + return dai.balanceOf(address(this)); + } + + function liquidityAvailable() external view override returns (uint256) { + return dai.balanceOf(address(this)); + } + + function idleLiquidity() external view override returns (uint256) { + return dai.balanceOf(address(this)); + } + + function redeemable() external view override returns (address) { + return address(gem); + } + + function exit(address dst, uint256 wad) external override onlyHub { + uint256 exited_ = exited; + exited = exited_ + wad; + uint256 amt = wad * gem.balanceOf(address(this)) / (hub.end().Art(ilk) - exited_); + require(gem.transfer(dst, amt), "D3MSwapPool/transfer-failed"); + } + + // --- Swaps --- + + function previewSwapGemForDai(uint256 gemAmt) public view virtual returns (uint256 daiAmt); + + function previewSwapDaiForGem(uint256 daiAmt) public view virtual returns (uint256 gemAmt); + + function swapGemForDai(address usr, uint256 gemAmt, uint256 minDaiAmt) external returns (uint256 daiAmt) { + daiAmt = previewSwapGemForDai(gemAmt); + require(daiAmt >= minDaiAmt, "D3MSwapPool/too-little-dai"); + require(gem.transferFrom(msg.sender, address(this), gemAmt), "D3MSwapPool/failed-transfer"); + dai.transfer(usr, daiAmt); + + emit SwapGemForDai(usr, gemAmt, daiAmt); + } + + function swapDaiForGem(address usr, uint256 daiAmt, uint256 minGemAmt) external returns (uint256 gemAmt) { + gemAmt = previewSwapDaiForGem(daiAmt); + require(gemAmt >= minGemAmt, "D3MSwapPool/too-little-gems"); + dai.transferFrom(msg.sender, address(this), daiAmt); + require(gem.transfer(usr, gemAmt), "D3MSwapPool/failed-transfer"); + + emit SwapDaiForGem(usr, daiAmt, gemAmt); + } + +} diff --git a/src/pools/ID3MPool.sol b/src/pools/ID3MPool.sol index 2d1674ec..c68abe68 100644 --- a/src/pools/ID3MPool.sol +++ b/src/pools/ID3MPool.sol @@ -90,6 +90,20 @@ interface ID3MPool { */ function maxWithdraw() external view returns (uint256); + /** + @notice Return the amount of liquidity available at this moment in time. + @dev Available liquidity is Dai that be immediately withdrawn not limited by your previous deposits + @return uint256 available liquidity in Dai [wad] + */ + function liquidityAvailable() external view returns (uint256); + + /** + @notice Return what is considered idle liquidity. + @dev Idle liquidity is the amount of liquidity that is considered not being used + @return uint256 idle liquidity in Dai [wad] + */ + function idleLiquidity() external view returns (uint256); + /// @notice returns address of redeemable tokens (if any) function redeemable() external view returns (address); } diff --git a/src/tests/D3MHub.t.sol b/src/tests/D3MHub.t.sol index 369b6530..4dd21efc 100644 --- a/src/tests/D3MHub.t.sol +++ b/src/tests/D3MHub.t.sol @@ -23,6 +23,8 @@ import { D3MHub } from "../D3MHub.sol"; import { D3MOracle } from "../D3MOracle.sol"; import { ID3MPool } from "../pools/ID3MPool.sol"; import { ID3MPlan } from "../plans/ID3MPlan.sol"; +import { ID3MFees } from "../fees/ID3MFees.sol"; +import { D3MForwardFees } from "../fees/D3MForwardFees.sol"; import { TokenMock } from "./mocks/TokenMock.sol"; @@ -95,6 +97,14 @@ contract PoolMock is ID3MPool { return dai.balanceOf(address(this)); } + function liquidityAvailable() external view override returns (uint256) { + return dai.balanceOf(address(this)); + } + + function idleLiquidity() external view override returns (uint256) { + return dai.balanceOf(address(this)); + } + function redeemable() external view returns (address) { return address(gem); } @@ -123,7 +133,7 @@ contract PlanMock is ID3MPlan { targetAssets = _targetAssets; } - function getTargetAssets(uint256) external override view returns (uint256) { + function getTargetAssets(bytes32,uint256) external override view returns (uint256) { return targetAssets; } @@ -155,6 +165,7 @@ contract D3MHubTest is DssTest { D3MHub hub; PoolMock pool; PlanMock plan; + D3MForwardFees fees; D3MOracle pip; function setUp() public { @@ -180,12 +191,14 @@ contract D3MHubTest is DssTest { pool = new PoolMock(address(vat), address(hub), address(dai), address(testGem)); plan = new PlanMock(); + fees = new D3MForwardFees(address(vat), address(vow)); hub.file("vow", vow); hub.file("end", address(end)); hub.file(ilk, "pool", address(pool)); hub.file(ilk, "plan", address(plan)); + hub.file(ilk, "fees", address(fees)); hub.file(ilk, "tau", 7 days); // Init new collateral @@ -222,10 +235,10 @@ contract D3MHubTest is DssTest { } function test_can_file_tau() public { - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); assertEq(tau, 7 days); hub.file(ilk, "tau", 1 days); - (, , tau, , ) = hub.ilks(ilk); + (, , , tau, , ) = hub.ilks(ilk); assertEq(tau, 1 days); } @@ -243,27 +256,38 @@ contract D3MHubTest is DssTest { } function test_can_file_pool() public { - (ID3MPool _pool, , , , ) = hub.ilks(ilk); + (ID3MPool _pool, , , , , ) = hub.ilks(ilk); assertEq(address(_pool), address(pool)); hub.file(ilk, "pool", address(this)); - (_pool, , , , ) = hub.ilks(ilk); + (_pool, , , , , ) = hub.ilks(ilk); assertEq(address(_pool), address(this)); } function test_can_file_plan() public { - (, ID3MPlan _plan, , , ) = hub.ilks(ilk); + (, ID3MPlan _plan, , , , ) = hub.ilks(ilk); assertEq(address(_plan), address(plan)); hub.file(ilk, "plan", address(this)); - (, _plan, , , ) = hub.ilks(ilk); + (, _plan, , , , ) = hub.ilks(ilk); assertEq(address(_plan), address(this)); } + function test_can_file_fees() public { + (, , ID3MFees _fees, , , ) = hub.ilks(ilk); + + assertEq(address(_fees), address(fees)); + + hub.file(ilk, "fees", address(this)); + + (, , _fees, , , ) = hub.ilks(ilk); + assertEq(address(_fees), address(this)); + } + function test_can_file_vow() public { address setVow = hub.vow(); @@ -316,7 +340,7 @@ contract D3MHubTest is DssTest { function test_vat_not_live_ilk_address_file() public { hub.file(ilk, "pool", address(this)); - (ID3MPool _pool, , , , ) = hub.ilks(ilk); + (ID3MPool _pool, , , , , ) = hub.ilks(ilk); assertEq(address(_pool), address(this)); @@ -919,12 +943,12 @@ contract D3MHubTest is DssTest { } function test_cage_d3m_with_auth() public { - (, , uint256 tau, , uint256 tic) = hub.ilks(ilk); + (, , , uint256 tau, , uint256 tic) = hub.ilks(ilk); assertEq(tic, 0); hub.cage(ilk); - (, , , , tic) = hub.ilks(ilk); + (, , , , , tic) = hub.ilks(ilk); assertEq(tic, block.timestamp + tau); } @@ -960,7 +984,7 @@ contract D3MHubTest is DssTest { assertEq(art, 0); assertEq(vat.gem(ilk, address(pool)), 50 * WAD); assertEq(vat.sin(vow), sinBefore + 50 * RAD); - (, , , uint256 culled, ) = hub.ilks(ilk); + (, , , , uint256 culled, ) = hub.ilks(ilk); assertEq(culled, 1); } @@ -999,7 +1023,7 @@ contract D3MHubTest is DssTest { // Sin only increases by 40 WAD since 10 was covered previously assertEq(vat.sin(vow), sinBefore + 40 * RAD); assertEq(vat.dai(vow), vowDaiBefore); - (, , , uint256 culled, ) = hub.ilks(ilk); + (, , , , uint256 culled, ) = hub.ilks(ilk); assertEq(culled, 1); hub.exec(ilk); @@ -1033,7 +1057,7 @@ contract D3MHubTest is DssTest { uint256 gemAfter = vat.gem(ilk, address(pool)); assertEq(gemAfter, 50 * WAD); assertEq(vat.sin(vow), sinBefore + 50 * RAD); - (, , , uint256 culled, ) = hub.ilks(ilk); + (, , , , uint256 culled, ) = hub.ilks(ilk); assertEq(culled, 1); } @@ -1082,7 +1106,7 @@ contract D3MHubTest is DssTest { assertEq(part, 0); assertEq(vat.gem(ilk, address(pool)), 50 * WAD); uint256 sinBefore = vat.sin(vow); - (, , , uint256 culled, ) = hub.ilks(ilk); + (, , , , uint256 culled, ) = hub.ilks(ilk); assertEq(culled, 1); vat.cage(); @@ -1094,7 +1118,7 @@ contract D3MHubTest is DssTest { assertEq(vat.gem(ilk, address(pool)), 0); // Sin should not change since we suck before grabbing assertEq(vat.sin(vow), sinBefore); - (, , , culled, ) = hub.ilks(ilk); + (, , , , culled, ) = hub.ilks(ilk); assertEq(culled, 0); } @@ -1313,7 +1337,7 @@ contract D3MHubTest is DssTest { hub.file(ilk, "plan", address(newPlan)); - (, ID3MPlan _plan, , , ) = hub.ilks(ilk); + (, ID3MPlan _plan, , , , ) = hub.ilks(ilk); assertEq(address(_plan), address(newPlan)); hub.exec(ilk); @@ -1413,7 +1437,7 @@ contract D3MHubTest is DssTest { // Create D3M in New Hub newHub.file(ilk, "pool", address(newPool)); newHub.file(ilk, "plan", address(newPlan)); - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); newHub.file(ilk, "tau", tau); (uint256 opink, uint256 opart) = vat.urns(ilk, address(pool)); diff --git a/src/tests/fees/D3MForwardFees.t.sol b/src/tests/fees/D3MForwardFees.t.sol new file mode 100644 index 00000000..4da34ff4 --- /dev/null +++ b/src/tests/fees/D3MForwardFees.t.sol @@ -0,0 +1,59 @@ +// 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 "dss-test/DssTest.sol"; +import "dss-interfaces/Interfaces.sol"; +import { VatMock } from "../mocks/VatMock.sol"; + +import { D3MForwardFees } from "../../fees/D3MForwardFees.sol"; + +contract D3MForwardFeesTest is DssTest { + + VatMock vat; + + D3MForwardFees private fees; + + event FeesCollected(bytes32 indexed ilk, uint256 fees); + + function setUp() public { + vat = new VatMock(); + + fees = new D3MForwardFees(address(vat), TEST_ADDRESS); + } + + function test_forward_fees() public { + vat.suck(address(this), address(fees), 100 * RAD); + + assertEq(vat.dai(address(fees)), 100 * RAD); + assertEq(vat.dai(TEST_ADDRESS), 0); + fees.feesCollected("ETH-A", 100 * RAD); + assertEq(vat.dai(address(fees)), 0); + assertEq(vat.dai(TEST_ADDRESS), 100 * RAD); + } + + function test_forward_fees_amount_mismatch() public { + vat.suck(address(this), address(fees), 100 * RAD); + + assertEq(vat.dai(address(fees)), 100 * RAD); + assertEq(vat.dai(TEST_ADDRESS), 0); + fees.feesCollected("ETH-A", 0); + assertEq(vat.dai(address(fees)), 0); + assertEq(vat.dai(TEST_ADDRESS), 100 * RAD); + } + +} diff --git a/src/tests/integration/D3MAaveV2.t.sol b/src/tests/integration/D3MAaveV2.t.sol index b32b2e70..b7ca09b9 100644 --- a/src/tests/integration/D3MAaveV2.t.sol +++ b/src/tests/integration/D3MAaveV2.t.sol @@ -22,6 +22,7 @@ import "dss-interfaces/Interfaces.sol"; import { D3MHub } from "../../D3MHub.sol"; import { D3MMom } from "../../D3MMom.sol"; import { D3MOracle } from "../../D3MOracle.sol"; +import { D3MForwardFees } from "../../fees/D3MForwardFees.sol"; import { D3MAaveV2TypeRateTargetPlan } from "../../plans/D3MAaveV2TypeRateTargetPlan.sol"; import { D3MAaveV2TypePool } from "../../pools/D3MAaveV2TypePool.sol"; @@ -128,6 +129,7 @@ contract D3MAaveV2IntegrationTest is DssTest { d3mHub.file(ilk, "pool", address(d3mAavePool)); d3mHub.file(ilk, "plan", address(d3mAavePlan)); + d3mHub.file(ilk, "fees", address(new D3MForwardFees(address(vat), address(vow)))); d3mHub.file(ilk, "tau", 7 days); d3mHub.file("vow", vow); @@ -359,7 +361,7 @@ contract D3MAaveV2IntegrationTest is DssTest { uint256 vowDai = vat.dai(vow); d3mHub.cull(ilk); (uint256 ink2, uint256 art2) = vat.urns(ilk, address(d3mAavePool)); - (, , , uint256 culled, ) = d3mHub.ilks(ilk); + (, , , , uint256 culled, ) = d3mHub.ilks(ilk); assertEq(culled, 1); assertEq(ink2, 0); assertEq(art2, 0); @@ -697,7 +699,7 @@ contract D3MAaveV2IntegrationTest is DssTest { d3mHub.cage(ilk); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); @@ -817,7 +819,7 @@ contract D3MAaveV2IntegrationTest is DssTest { _setRelBorrowTarget(5000); d3mHub.cage(ilk); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); d3mHub.cull(ilk); @@ -944,7 +946,7 @@ contract D3MAaveV2IntegrationTest is DssTest { // Vat is caged for global settlement vat.cage(); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); assertRevert(address(d3mHub), abi.encodeWithSignature("cull(bytes32)", ilk), "D3MHub/no-cull-during-shutdown"); @@ -987,7 +989,7 @@ contract D3MAaveV2IntegrationTest is DssTest { d3mHub.cage(ilk); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); d3mHub.cull(ilk); @@ -1035,10 +1037,10 @@ contract D3MAaveV2IntegrationTest is DssTest { } function test_set_tau_not_caged() public { - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); assertEq(tau, 7 days); d3mHub.file(ilk, "tau", 1 days); - (, , tau, , ) = d3mHub.ilks(ilk); + (, , , tau, , ) = d3mHub.ilks(ilk); assertEq(tau, 1 days); } diff --git a/src/tests/integration/D3MCompoundV2.t.sol b/src/tests/integration/D3MCompoundV2.t.sol index 1455eba2..ccec025f 100644 --- a/src/tests/integration/D3MCompoundV2.t.sol +++ b/src/tests/integration/D3MCompoundV2.t.sol @@ -22,6 +22,7 @@ import "dss-interfaces/Interfaces.sol"; import { D3MHub } from "../../D3MHub.sol"; import { D3MMom } from "../../D3MMom.sol"; import { D3MOracle } from "../../D3MOracle.sol"; +import { D3MForwardFees } from "../../fees/D3MForwardFees.sol"; import { D3MCompoundV2TypeRateTargetPlan } from "../../plans/D3MCompoundV2TypeRateTargetPlan.sol"; import { D3MCompoundV2TypePool } from "../../pools/D3MCompoundV2TypePool.sol"; @@ -118,6 +119,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { d3mHub.file(ilk, "pool", address(d3mCompoundPool)); d3mHub.file(ilk, "plan", address(d3mCompoundPlan)); + d3mHub.file(ilk, "fees", address(new D3MForwardFees(address(vat), address(vow)))); d3mHub.file(ilk, "tau", 7 days); d3mHub.file("vow", vow); @@ -418,7 +420,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { uint256 vowDai = vat.dai(vow); d3mHub.cull(ilk); (uint256 ink2, uint256 art2) = vat.urns(ilk, address(d3mCompoundPool)); - (, , , uint256 culled, ) = d3mHub.ilks(ilk); + (, , , , uint256 culled, ) = d3mHub.ilks(ilk); assertEq(culled, 1); assertEq(ink2, 0); assertEq(art2, 0); @@ -771,7 +773,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { d3mHub.cage(ilk); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); vm.roll(block.number + tau / 15); @@ -895,7 +897,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { _setRelBorrowTarget(5000); d3mHub.cage(ilk); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); d3mHub.cull(ilk); @@ -1014,7 +1016,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { // Vat is caged for global settlement vat.cage(); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); assertRevert(address(d3mHub), abi.encodeWithSignature("cull(bytes32)", ilk), "D3MHub/no-cull-during-shutdown"); @@ -1058,7 +1060,7 @@ contract D3MCompoundV2IntegrationTest is DssTest { d3mHub.cage(ilk); - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); vm.warp(block.timestamp + tau); d3mHub.cull(ilk); @@ -1106,10 +1108,10 @@ contract D3MCompoundV2IntegrationTest is DssTest { } function test_set_tau_not_caged() public { - (, , uint256 tau, , ) = d3mHub.ilks(ilk); + (, , , uint256 tau, , ) = d3mHub.ilks(ilk); assertEq(tau, 7 days); d3mHub.file(ilk, "tau", 1 days); - (, , tau, , ) = d3mHub.ilks(ilk); + (, , , tau, , ) = d3mHub.ilks(ilk); assertEq(tau, 1 days); } diff --git a/src/tests/integration/GatedOffchainSwap.t.sol b/src/tests/integration/GatedOffchainSwap.t.sol new file mode 100644 index 00000000..dc29af5f --- /dev/null +++ b/src/tests/integration/GatedOffchainSwap.t.sol @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: © 2023 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 "./SwapPoolBase.t.sol"; + +import { D3MGatedOffchainSwapPool } from "../../pools/D3MGatedOffchainSwapPool.sol"; + +abstract contract GatedOffchainSwapBaseTest is SwapPoolBaseTest { + + D3MGatedOffchainSwapPool pool; + + function deployPool() internal override returns (address) { + pool = D3MGatedOffchainSwapPool(D3MDeploy.deployGatedOffchainSwapPool( + address(this), + admin, + ilk, + address(hub), + address(dai), + address(gem) + )); + return address(pool); + } + + function test_full_offchain_investment_cycle() public { + // We pay 10bps to fill or empty the gems + uint128 fee = uint128(10010 * WAD / 10000); + vm.prank(admin); pool.file("fees", fee, fee); + vm.prank(admin); pool.addOperator(TEST_ADDRESS); + + plan.setAllocation(address(this), ilk, uint128(standardDebtSize)); + hub.exec(ilk); + + assertEq(dai.balanceOf(address(pool)), standardDebtSize); + assertEq(gem.balanceOf(address(pool)), 0); + assertEq(dai.balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(pool.assetBalance(), standardDebtSize); + + // We want an arbitrager to fill the pool + uint256 gemAmt = daiToGem(standardDebtSize) * WAD / fee; + deal(address(gem), address(this), gemAmt); + pool.swapGemForDai(address(this), gemAmt, 0); + + assertLe(dai.balanceOf(address(pool)), WAD); // 1 DAI dust is fine + assertRoundingEq(gem.balanceOf(address(pool)), gemAmt); + assertRoundingEq(dai.balanceOf(address(this)), standardDebtSize); + assertEq(gem.balanceOf(address(this)), 0); + assertRoundingEq(pool.assetBalance(), standardDebtSize * WAD / fee); // Lost a bit of assets from the fees paid out + + // Top up the pool just to simplify the future calculations + // This could be done via pulling from the vow + gemAmt = daiToGem(standardDebtSize); + deal(address(gem), address(pool), gemAmt); + (, uint256 art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, pool.assetBalance()); + + // Operator queries how much can be pulled out and deploys + uint256 pendingDeposits = pool.pendingDeposits(); + assertRoundingEq(pendingDeposits, gemAmt); + assertEq(pool.pendingWithdrawals(), 0); + assertEq(pool.gemsOutstanding(), 0); + vm.prank(TEST_ADDRESS); pool.pull(TEST_ADDRESS, pendingDeposits); + assertEq(pool.pendingDeposits(), 0); + assertEq(pool.pendingWithdrawals(), 0); + assertEq(gem.balanceOf(address(pool)), 0); + assertEq(gem.balanceOf(address(TEST_ADDRESS)), gemAmt); + assertEq(pool.gemsOutstanding(), gemAmt); + + // --- Offchain deploy of funds occurs here --- + + // Some time passes and interest accumulates (5%) + uint256 positionSize = pool.gemsOutstanding() * 105 / 100; + uint256 earned = positionSize - pool.gemsOutstanding(); + deal(address(gem), TEST_ADDRESS, gemAmt + earned); + uint256 vowDai = vat.dai(address(vow)); + vm.prank(admin); pool.file("gemsOutstanding", positionSize); + (, art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, pool.assetBalance() * 100 / 105); // Debt should be about 5% less than assets + hub.exec(ilk); + (, art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, pool.assetBalance()); + assertRoundingEq(vat.dai(address(vow)), vowDai + gemToDai(earned) * RAY); // Surplus increases due to asset appreciation + assertRoundingEq(pool.assetBalance(), standardDebtSize + gemToDai(earned)); + + // Due to position size being at the target debt limit the operator is required to repay the interest + assertEq(pool.pendingWithdrawals(), earned); + vm.prank(TEST_ADDRESS); gem.approve(address(pool), type(uint256).max); + vm.prank(TEST_ADDRESS); pool.push(earned); + assertEq(pool.pendingWithdrawals(), 0); + + // Arbitrager swaps gem for dai + assertEq(gem.balanceOf(address(pool)), earned); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(dai.balanceOf(address(pool)), 0); + uint256 daiAmt = gemToDai(earned) * WAD / fee; + deal(address(dai), address(this), daiAmt); + pool.swapDaiForGem(address(this), daiAmt, 0); + assertLe(gem.balanceOf(address(pool)), daiToGem(1 ether)); // Some dust is fine + assertRoundingEq(gem.balanceOf(address(this)), earned); + assertEq(dai.balanceOf(address(pool)), daiAmt); + + // Exec should now clear out the excess debt to get us back to desired amount + (, art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, standardDebtSize * 105 / 100); + hub.exec(ilk); + (, art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, standardDebtSize); + } + +} + +// Monetalis via USDC +contract MonetalisSwapTest is GatedOffchainSwapBaseTest { + + function getGem() internal override pure returns (address) { + return 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + } + + function getPip() internal override pure returns (address) { + return 0x77b68899b99b686F415d074278a9a16b336085A0; // Hardcoded $1 pip + } + + function getSwapGemForDaiPip() internal override pure returns (address) { + return 0x77b68899b99b686F415d074278a9a16b336085A0; + } + + function getSwapDaiForGemPip() internal override pure returns (address) { + return 0x77b68899b99b686F415d074278a9a16b336085A0; + } + +} diff --git a/src/tests/integration/GatedSwap.t.sol b/src/tests/integration/GatedSwap.t.sol new file mode 100644 index 00000000..efbf9766 --- /dev/null +++ b/src/tests/integration/GatedSwap.t.sol @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: © 2023 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 "./SwapPoolBase.t.sol"; + +import { D3MGatedSwapPool } from "../../pools/D3MGatedSwapPool.sol"; + +abstract contract GatedSwapBaseTest is SwapPoolBaseTest { + + D3MGatedSwapPool pool; + + function deployPool() internal override returns (address) { + pool = D3MGatedSwapPool(D3MDeploy.deployGatedSwapPool( + address(this), + admin, + ilk, + address(hub), + address(dai), + address(gem) + )); + return address(pool); + } + + function test_full_investment_cycle() public { + // We pay 10bps to fill or empty the gems + uint128 fee = uint128(10010 * WAD / 10000); + vm.prank(admin); pool.file("fees", fee, fee); + + plan.setAllocation(address(this), ilk, uint128(standardDebtSize)); + hub.exec(ilk); + + assertEq(dai.balanceOf(address(pool)), standardDebtSize); + assertEq(gem.balanceOf(address(pool)), 0); + assertEq(dai.balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(pool.assetBalance(), standardDebtSize); + + // We want an arbitrager to fill the pool + uint256 gemAmt = daiToGem(standardDebtSize) * WAD / fee; + deal(address(gem), address(this), gemAmt); + pool.swapGemForDai(address(this), gemAmt, 0); + + assertLe(dai.balanceOf(address(pool)), WAD); // 1 DAI dust is fine + assertRoundingEq(gem.balanceOf(address(pool)), gemAmt); + assertRoundingEq(dai.balanceOf(address(this)), standardDebtSize); + assertEq(gem.balanceOf(address(this)), 0); + assertRoundingEq(pool.assetBalance(), standardDebtSize * WAD / fee); // Lost a bit of assets from the fees paid out + + // Top up the pool just to simplify the future calculations + // This could be done via pulling from the vow + gemAmt = daiToGem(standardDebtSize); + deal(address(gem), address(pool), gemAmt); + (, uint256 art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, pool.assetBalance()); + + // TODO interest accumulation + uint256 earned = 0; + + // Arbitrager swaps gem for dai + assertEq(gem.balanceOf(address(pool)), earned); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(dai.balanceOf(address(pool)), 0); + uint256 daiAmt = gemToDai(earned) * WAD / fee; + deal(address(dai), address(this), daiAmt); + pool.swapDaiForGem(address(this), daiAmt, 0); + assertLe(gem.balanceOf(address(pool)), daiToGem(1 ether)); // Some dust is fine + assertRoundingEq(gem.balanceOf(address(this)), earned); + assertEq(dai.balanceOf(address(pool)), daiAmt); + + // Exec should now clear out the excess debt to get us back to desired amount + (, art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, standardDebtSize * 105 / 100); + hub.exec(ilk); + (, art) = vat.urns(ilk, address(pool)); + assertRoundingEq(art, standardDebtSize); + } + +} + +// Arrakis DAI/USDC 1bps Tight Pool +contract ArrakisSwapTest is GatedSwapBaseTest { + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Give access to the pool to read the oracle + stdstore.target(0xcCBa43231aC6eceBd1278B90c3a44711a00F4e93).sig("bud(address)").with_key(address(pool)).checked_write(bytes32(uint256(1))); + stdstore.target(0xcCBa43231aC6eceBd1278B90c3a44711a00F4e93).sig("bud(address)").with_key(address(this)).checked_write(bytes32(uint256(1))); + } + + function getGem() internal override pure returns (address) { + return 0x50379f632ca68D36E50cfBC8F78fe16bd1499d1e; + } + + function getPip() internal override pure returns (address) { + return 0xcCBa43231aC6eceBd1278B90c3a44711a00F4e93; + } + + function getSwapGemForDaiPip() internal override pure returns (address) { + return 0xcCBa43231aC6eceBd1278B90c3a44711a00F4e93; + } + + function getSwapDaiForGemPip() internal override pure returns (address) { + return 0xcCBa43231aC6eceBd1278B90c3a44711a00F4e93; + } + +} diff --git a/src/tests/integration/IntegrationBase.t.sol b/src/tests/integration/IntegrationBase.t.sol index edc20509..14fb8753 100644 --- a/src/tests/integration/IntegrationBase.t.sol +++ b/src/tests/integration/IntegrationBase.t.sol @@ -21,6 +21,7 @@ import "dss-interfaces/Interfaces.sol"; import { ID3MPlan } from "../../plans/ID3MPlan.sol"; import { ID3MPool } from "../../pools/ID3MPool.sol"; +import { ID3MFees } from "../../fees/ID3MFees.sol"; import { D3MHub } from "../../D3MHub.sol"; import { D3MMom } from "../../D3MMom.sol"; @@ -33,7 +34,6 @@ abstract contract IntegrationBaseTest is DssTest { using stdJson for string; using MCD for *; - using GodMode for *; using ScriptTools for *; address internal admin; @@ -47,8 +47,9 @@ abstract contract IntegrationBaseTest is DssTest { EndAbstract internal end; VowAbstract internal vow; - int256 internal standardDebtSize = int256(1_000_000 * WAD); // Override if necessary - uint256 internal roundingTolerance = 1; // Override if necessary + uint256 internal standardDebtSize = 1_000_000 * WAD; // Override if necessary + uint256 internal standardDebtCeiling = 100_000_000 * WAD; // Override if necessary + uint256 internal roundingTolerance = WAD / 10000; // Override if necessary [1bps by default] bytes32 internal ilk = "EXAMPLE-ILK"; // Override if necessary D3MHub internal hub; @@ -57,13 +58,33 @@ abstract contract IntegrationBaseTest is DssTest { // These are private as inheriting contract should use a more specific type ID3MPool private pool; ID3MPlan private plan; + ID3MFees private fees; - function baseInit() internal { - vm.createSelectFork(vm.envString("ETH_RPC_URL")); - + function baseInit() internal virtual { dss = MCD.loadFromChainlog(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F); admin = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); - hub = D3MHub(dss.chainlog.getAddress("DIRECT_HUB")); + + // The Hub needs an upgrade so deactivate and replace the old one + address _hub = D3MDeploy.deployHub( + address(this), + admin, + address(dss.daiJoin) + ); + + vm.startPrank(admin); + + D3MInit.deactivateHub( + dss, + dss.chainlog.getAddress("DIRECT_HUB") + ); + D3MInit.initHub( + dss, + _hub + ); + + vm.stopPrank(); + + hub = D3MHub(_hub); mom = D3MMom(dss.chainlog.getAddress("DIRECT_MOM")); vat = dss.vat; @@ -73,44 +94,57 @@ abstract contract IntegrationBaseTest is DssTest { vow = dss.vow; } - function basePostSetup() internal { + function basePostSetup() internal virtual { pool = ID3MPool(d3m.pool); plan = ID3MPlan(d3m.plan); - - adjustLiquidity(standardDebtSize); // Ensure there is some liquidty to start with + fees = ID3MFees(d3m.fees); } // --- Helper functions --- function _min(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x <= y ? x : y; } + function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x != 0 ? ((x - 1) / y) + 1 : 0; + } + } function assertRoundingEq(uint256 a, uint256 b) internal { - assertApproxEqAbs(a, b, roundingTolerance); + assertApproxEqRel(a, b, roundingTolerance); + } + function assertRoundingEq(uint256 a, uint256 b, string memory err) internal { + assertApproxEqRel(a, b, roundingTolerance, err); } // --- Manage D3M Debt --- - function adjustDebt(int256 deltaAmount) internal virtual; - - function setDebtToZero() internal virtual { - adjustDebt(type(int256).min / int256(WAD)); // Just a really big number, but don't want to underflow + function getDebt() internal virtual view returns (uint256) { + (, uint256 art) = vat.urns(ilk, address(pool)); + return art; } + function setDebt(uint256 amount) internal virtual; + function setDebtToMaximum() internal virtual { - adjustDebt(type(int256).max / int256(WAD)); // Just a really big number, but don't want to overflow + (,,, uint256 line,) = vat.ilks(ilk); + setDebt(line / RAY); } // --- Manage Pool Liquidity --- - function getLiquidity() internal virtual view returns (uint256); + function getLiquidity() internal virtual view returns (uint256) { + return pool.liquidityAvailable(); + } - function adjustLiquidity(int256 deltaAmount) internal virtual; + function setLiquidity(uint256 amount) internal virtual; - function setLiquidityToZero() internal virtual { - adjustLiquidity(-int256(getLiquidity())); + // --- Other Overridable Functions --- + function getTokenBalance(address a) internal view virtual returns (uint256) { + DSTokenAbstract token = DSTokenAbstract(pool.redeemable()); + return token.balanceOf(a); } - // --- Other Overridable Functions --- - function getLPTokenBalanceInAssets(address a) internal view virtual returns (uint256) { - return DSTokenAbstract(pool.redeemable()).balanceOf(a); + function getTokenBalanceInAssets(address a) internal view virtual returns (uint256) { + DSTokenAbstract token = DSTokenAbstract(pool.redeemable()); + return getTokenBalance(a) * 10 ** (18 - token.decimals()); } function generateInterest() internal virtual { @@ -119,26 +153,22 @@ abstract contract IntegrationBaseTest is DssTest { // --- Tests --- function test_target_zero() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); (uint256 ink, uint256 art) = vat.urns(ilk, address(pool)); - assertGt(ink, 0); - assertGt(art, 0); + assertRoundingEq(ink, standardDebtSize); + assertRoundingEq(art, standardDebtSize); - setDebtToZero(); + setDebt(0); (ink, art) = vat.urns(ilk, address(pool)); - assertRoundingEq(ink, 0); - assertRoundingEq(art, 0); + assertEq(ink, 0); + assertEq(art, 0); } function test_cage_temp_insufficient_liquidity() public { - // Increase debt - adjustDebt(standardDebtSize); - - // Someone else borrows - int256 borrowAmount = standardDebtSize / 2 - int256(getLiquidity()); - adjustLiquidity(borrowAmount); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); // Cage the system and start unwinding (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); @@ -149,25 +179,22 @@ abstract contract IntegrationBaseTest is DssTest { (uint256 ink, uint256 art) = vat.urns(ilk, address(pool)); assertGt(ink, 0); assertGt(art, 0); - assertRoundingEq(pink - ink, uint256(standardDebtSize / 2)); - assertRoundingEq(part - art, uint256(standardDebtSize / 2)); + assertRoundingEq(pink - ink, standardDebtSize / 2); + assertRoundingEq(part - art, standardDebtSize / 2); + assertEq(getLiquidity(), 0); - // Someone else repays some Dai so we can unwind the rest - adjustLiquidity(-borrowAmount); + // Liquidity returns so we can remove the rest of the debt + setLiquidity(standardDebtSize / 2); hub.exec(ilk); (ink, art) = vat.urns(ilk, address(pool)); - assertEq(ink, 0); - assertEq(art, 0); + assertApproxEqAbs(ink, 0, 1 ether); + assertApproxEqAbs(art, 0, 1 ether); } function test_cage_perm_insufficient_liquidity() public { - // Increase debt - adjustDebt(standardDebtSize); - - // Someone else borrows - int256 borrowAmount = standardDebtSize / 2 - int256(getLiquidity()); - adjustLiquidity(borrowAmount); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); // Cage the system and start unwinding (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); @@ -178,17 +205,18 @@ abstract contract IntegrationBaseTest is DssTest { (uint256 ink, uint256 art) = vat.urns(ilk, address(pool)); assertGt(ink, 0); assertGt(art, 0); - assertRoundingEq(pink - ink, uint256(standardDebtSize / 2)); - assertRoundingEq(part - art, uint256(standardDebtSize / 2)); + assertRoundingEq(pink - ink, standardDebtSize / 2); + assertRoundingEq(part - art, standardDebtSize / 2); + assertEq(getLiquidity(), 0); - // In this case nobody deposits more DAI so we have to write off the bad debt + // In this case no DAI liquidity returns so we have to write off the bad debt vm.warp(block.timestamp + hub.tau(ilk)); uint256 sin = vat.sin(address(vow)); uint256 vowDai = vat.dai(address(vow)); hub.cull(ilk); (uint256 ink2, uint256 art2) = vat.urns(ilk, address(pool)); - (, , , uint256 culled, ) = hub.ilks(ilk); + (, , , , uint256 culled, ) = hub.ilks(ilk); assertEq(culled, 1); assertEq(ink2, 0); assertEq(art2, 0); @@ -196,50 +224,48 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.sin(address(vow)), sin + art * RAY); assertEq(vat.dai(address(vow)), vowDai); - // Some time later the pool gets some liquidity - adjustLiquidity(-borrowAmount); + // Some time later DAI liquidity returns + setLiquidity(standardDebtSize / 2); // Close out the remainder of the position - uint256 balance = getLPTokenBalanceInAssets(address(pool)); + uint256 balance = pool.assetBalance(); assertGe(balance, art); hub.exec(ilk); - assertEq(getLPTokenBalanceInAssets(address(pool)), 0); + assertApproxEqAbs(pool.assetBalance(), 0, 1 ether); assertEq(vat.sin(address(vow)), sin + art * RAY); - assertEq(vat.dai(address(vow)), vowDai + balance * RAY); + assertRoundingEq(vat.dai(address(vow)), vowDai + balance * RAY); assertEq(vat.gem(ilk, address(pool)), 0); } function test_hit_debt_ceiling() public { // Lower the debt ceiling to a small number - uint256 debtCeiling = uint256(standardDebtSize / 2); + uint256 debtCeiling = standardDebtSize / 2; vm.prank(admin); vat.file(ilk, "line", debtCeiling * RAY); // Max out the debt ceiling setDebtToMaximum(); (uint256 ink, uint256 art) = vat.urns(ilk, address(pool)); - assertEq(ink, debtCeiling); - assertEq(art, debtCeiling); - assertRoundingEq(getLPTokenBalanceInAssets(address(pool)), debtCeiling); + assertRoundingEq(ink, debtCeiling); + assertRoundingEq(art, debtCeiling); // Should be a no-op hub.exec(ilk); (ink, art) = vat.urns(ilk, address(pool)); - assertEq(ink, debtCeiling); - assertEq(art, debtCeiling); - assertRoundingEq(getLPTokenBalanceInAssets(address(pool)), debtCeiling); + assertRoundingEq(ink, debtCeiling); + assertRoundingEq(art, debtCeiling); // Raise it by a bit - debtCeiling = uint256(standardDebtSize * 2); + debtCeiling = standardDebtSize * 2; vm.prank(admin); vat.file(ilk, "line", debtCeiling * RAY); - hub.exec(ilk); + setDebtToMaximum(); (ink, art) = vat.urns(ilk, address(pool)); - assertEq(ink, debtCeiling); - assertEq(art, debtCeiling); - assertRoundingEq(getLPTokenBalanceInAssets(address(pool)), debtCeiling); + assertRoundingEq(ink, debtCeiling); + assertRoundingEq(art, debtCeiling); } function test_collect_interest() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); + setLiquidity(0); generateInterest(); @@ -250,61 +276,58 @@ abstract contract IntegrationBaseTest is DssTest { } function test_insufficient_liquidity_for_unwind_fees() public { - uint256 currentLiquidity = getLiquidity(); uint256 vowDai = vat.dai(address(vow)); - // Increase debt - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); - uint256 pAssets = getLPTokenBalanceInAssets(address(pool)); + uint256 pAssets = pool.assetBalance(); (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); assertEq(pink, part); assertRoundingEq(pink, pAssets); - // Someone else borrows the exact amount previously available - uint256 amountToBorrow = currentLiquidity; - adjustLiquidity(-int256(amountToBorrow)); + // Liquidity all goes away + setLiquidity(0); // Accumulate interest generateInterest(); - uint256 feesAccrued = getLPTokenBalanceInAssets(address(pool)) - pAssets; - currentLiquidity = getLiquidity(); + uint256 feesAccrued = pool.assetBalance() - pAssets; + setLiquidity(feesAccrued); + uint256 debt = getDebt(); assertGt(feesAccrued, 0); - assertEq(pink, currentLiquidity); - assertGt(pink + feesAccrued, currentLiquidity); + assertEq(pink, debt); + assertGt(pink + feesAccrued, debt); // Cage the system to trigger only unwinds vm.prank(admin); hub.cage(ilk); hub.exec(ilk); - uint256 assets = getLPTokenBalanceInAssets(address(pool)); + uint256 assets = pool.assetBalance(); // All the fees are accrued but what can't be withdrawn is added up to the original ink and art debt (uint256 ink, uint256 art) = vat.urns(ilk, address(pool)); assertEq(ink, art); assertRoundingEq(ink, assets); assertGt(assets, 0); - assertRoundingEq(ink, feesAccrued); - assertApproxEqAbs(vat.dai(address(vow)), vowDai + feesAccrued * RAY, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), vowDai + feesAccrued * RAY); - // Someone repays - adjustLiquidity(int256(amountToBorrow)); + // Liquidity returns + setLiquidity(assets); hub.exec(ilk); // Now the CDP completely unwinds and surplus buffer doesn't change (ink, art) = vat.urns(ilk, address(pool)); - assertRoundingEq(ink, 0); - assertRoundingEq(art, 0); - assertEq(getLPTokenBalanceInAssets(address(pool)), 0); - assertApproxEqAbs(vat.dai(address(vow)), vowDai + feesAccrued * RAY, 2 * RAY * roundingTolerance); // rounding may affect twice + assertApproxEqAbs(ink, 0, 1 ether); + assertApproxEqAbs(art, 0, 1 ether); + assertApproxEqAbs(pool.assetBalance(), 0, 1 ether); + assertRoundingEq(vat.dai(address(vow)), vowDai + feesAccrued * RAY); } function test_insufficient_liquidity_for_exec_fees() public { - // Increase debt - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); + setLiquidity(0); - uint256 pAssets = getLPTokenBalanceInAssets(address(pool)); + uint256 pAssets = pool.assetBalance(); (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); assertEq(pink, part); assertRoundingEq(pink, pAssets); @@ -312,12 +335,11 @@ abstract contract IntegrationBaseTest is DssTest { // Accumulate interest generateInterest(); - // Someone else borrows almost all the liquidity - uint256 currentLiquidity = getLiquidity(); - adjustLiquidity(-int256(currentLiquidity * 99 / 100)); - assertRoundingEq(getLiquidity(), currentLiquidity / 100); + // Some liquidity returns + setLiquidity(standardDebtSize / 100); + assertRoundingEq(getLiquidity(), standardDebtSize / 100); - uint256 feesAccrued = getLPTokenBalanceInAssets(address(pool)) - pAssets; + uint256 feesAccrued = pool.assetBalance() - pAssets; assertGt(feesAccrued, 0); // Accrue fees @@ -325,28 +347,17 @@ abstract contract IntegrationBaseTest is DssTest { vm.prank(admin); plan.disable(); // So we make sure to unwind after rebalancing hub.exec(ilk); - uint256 assets = getLPTokenBalanceInAssets(address(pool)); + uint256 assets = pool.assetBalance(); (uint256 ink, uint256 art) = vat.urns(ilk, address(pool)); assertEq(ink, art); assertRoundingEq(ink, assets); - assertRoundingEq(ink, pink + feesAccrued - currentLiquidity / 100); - assertApproxEqAbs(vat.dai(address(vow)), vowDai + feesAccrued * RAY, RAY * roundingTolerance); + assertRoundingEq(ink, pink + feesAccrued - standardDebtSize / 100); + assertRoundingEq(vat.dai(address(vow)), vowDai + feesAccrued * RAY); } function test_unwind_mcd_caged_not_skimmed() public { - uint256 currentLiquidity = getLiquidity(); - - // Increase debt - adjustDebt(standardDebtSize); - - (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); - assertGt(pink, 0); - assertGt(part, 0); - - // Someone else borrows - uint256 amountSupplied = getLPTokenBalanceInAssets(address(pool)); - uint256 amountToBorrow = currentLiquidity + amountSupplied / 2; - adjustLiquidity(-int256(amountToBorrow)); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); // MCD shutdowns vm.prank(admin); end.cage(); @@ -360,8 +371,6 @@ abstract contract IntegrationBaseTest is DssTest { uint256 prevSin = vat.sin(address(vow)); uint256 prevDai = vat.dai(address(vow)); - assertEq(prevSin, 0); - assertGt(prevDai, 0); // We try to unwind what is possible hub.exec(ilk); @@ -371,17 +380,17 @@ abstract contract IntegrationBaseTest is DssTest { (ink, art) = vat.urns(ilk, address(pool)); assertEq(ink, 0); assertEq(art, 0); - assertEq(vat.gem(ilk, address(end)), amountSupplied / 2); // Automatically skimmed when unwinding - if (prevSin + (amountSupplied / 2) * RAY >= prevDai) { - assertApproxEqAbs(vat.sin(address(vow)), prevSin + (amountSupplied / 2) * RAY - prevDai, RAY * roundingTolerance); + assertRoundingEq(vat.gem(ilk, address(end)), standardDebtSize / 2); // Automatically skimmed when unwinding + if (prevSin + (standardDebtSize / 2) * RAY >= prevDai) { + assertRoundingEq(vat.sin(address(vow)), prevSin + (standardDebtSize / 2) * RAY - prevDai); assertEq(vat.dai(address(vow)), 0); } else { - assertApproxEqAbs(vat.dai(address(vow)), prevDai - prevSin - (amountSupplied / 2) * RAY, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), prevDai - prevSin - (standardDebtSize / 2) * RAY); assertEq(vat.sin(address(vow)), 0); } // Some time later the pool gets some liquidity - adjustLiquidity(int256(amountToBorrow)); + setLiquidity(standardDebtSize / 2); // Rest of the liquidity can be withdrawn hub.exec(ilk); @@ -392,19 +401,10 @@ abstract contract IntegrationBaseTest is DssTest { } function test_unwind_mcd_caged_skimmed() public { - uint256 currentLiquidity = getLiquidity(); - - // Increase debt - adjustDebt(standardDebtSize); - - (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); - assertGt(pink, 0); - assertGt(part, 0); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); - // Someone else borrows - uint256 amountSupplied = getLPTokenBalanceInAssets(address(pool)); - uint256 amountToBorrow = currentLiquidity + amountSupplied / 2; - adjustLiquidity(-int256(amountToBorrow)); + (uint256 pink,) = vat.urns(ilk, address(pool)); // MCD shutdowns vm.prank(admin); end.cage(); @@ -418,8 +418,6 @@ abstract contract IntegrationBaseTest is DssTest { uint256 prevSin = vat.sin(address(vow)); uint256 prevDai = vat.dai(address(vow)); - assertEq(prevSin, 0); - assertGt(prevDai, 0); // Position is taken by the End module end.skim(ilk, address(pool)); @@ -428,11 +426,11 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(ink, 0); assertEq(art, 0); assertEq(vat.gem(ilk, address(end)), pink); - if (prevSin + amountSupplied * RAY >= prevDai) { - assertApproxEqAbs(vat.sin(address(vow)), prevSin + amountSupplied * RAY - prevDai, RAY * roundingTolerance); + if (prevSin + standardDebtSize * RAY >= prevDai) { + assertRoundingEq(vat.sin(address(vow)), prevSin + standardDebtSize * RAY - prevDai); assertEq(vat.dai(address(vow)), 0); } else { - assertApproxEqAbs(vat.dai(address(vow)), prevDai - prevSin - amountSupplied * RAY, RAY); + assertRoundingEq(vat.dai(address(vow)), prevDai - prevSin - standardDebtSize * RAY); assertEq(vat.sin(address(vow)), 0); } @@ -441,17 +439,17 @@ abstract contract IntegrationBaseTest is DssTest { vow.heal(_min(vat.sin(address(vow)), vat.dai(address(vow)))); // Part can't be done yet - assertEq(vat.gem(ilk, address(end)), amountSupplied / 2); - if (prevSin + (amountSupplied / 2) * RAY >= prevDai) { - assertApproxEqAbs(vat.sin(address(vow)), prevSin + (amountSupplied / 2) * RAY - prevDai, RAY * roundingTolerance); + assertRoundingEq(vat.gem(ilk, address(end)), standardDebtSize / 2); + if (prevSin + (standardDebtSize / 2) * RAY >= prevDai) { + assertRoundingEq(vat.sin(address(vow)), prevSin + (standardDebtSize / 2) * RAY - prevDai); assertEq(vat.dai(address(vow)), 0); } else { - assertApproxEqAbs(vat.dai(address(vow)), prevDai - prevSin - (amountSupplied / 2) * RAY, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), prevDai - prevSin - (standardDebtSize / 2) * RAY); assertEq(vat.sin(address(vow)), 0); } // Some time later the pool gets some liquidity - adjustLiquidity(int256(amountToBorrow)); + setLiquidity(standardDebtSize / 2); // Rest of the liquidity can be withdrawn hub.exec(ilk); @@ -462,19 +460,8 @@ abstract contract IntegrationBaseTest is DssTest { } function test_unwind_mcd_caged_wait_done() public { - uint256 currentLiquidity = getLiquidity(); - - // Increase debt - adjustDebt(standardDebtSize); - - (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); - assertGt(pink, 0); - assertGt(part, 0); - - // Someone else borrows - uint256 amountSupplied = getLPTokenBalanceInAssets(address(pool)); - uint256 amountToBorrow = currentLiquidity + amountSupplied / 2; - adjustLiquidity(-int256(amountToBorrow)); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); // MCD shutdowns vm.prank(admin); end.cage(); @@ -495,27 +482,18 @@ abstract contract IntegrationBaseTest is DssTest { } function test_unwind_culled_then_mcd_caged() public { - uint256 currentLiquidity = getLiquidity(); - - // Increase debt - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); - assertGt(pink, 0); - assertGt(part, 0); - - // Someone else borrows - uint256 amountSupplied = getLPTokenBalanceInAssets(address(pool)); - uint256 amountToBorrow = currentLiquidity + amountSupplied / 2; - adjustLiquidity(-int256(amountToBorrow)); vm.prank(admin); hub.cage(ilk); - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); vm.warp(block.timestamp + tau); - uint256 daiEarned = getLPTokenBalanceInAssets(address(pool)) - pink; + uint256 daiEarned = pool.assetBalance() - pink; vow.heal( _min( @@ -540,7 +518,7 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(ink, 0); assertEq(art, 0); assertEq(vat.gem(ilk, address(pool)), pink); - assertGe(getLPTokenBalanceInAssets(address(pool)), pink); + assertGe(pool.assetBalance(), pink); // MCD shutdowns originalDai = originalDai + vat.dai(vow.flapper()); @@ -564,7 +542,7 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(ink, pink); assertEq(art, part); assertEq(vat.gem(ilk, address(pool)), 0); - assertGe(getLPTokenBalanceInAssets(address(pool)), pink); + assertGe(pool.assetBalance(), pink); assertEq(vat.sin(address(vow)), 0); // Call skim manually (will be done through deposit anyway) @@ -577,12 +555,12 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(art, 0); assertEq(vat.gem(ilk, address(pool)), 0); assertEq(vat.gem(ilk, address(end)), pink); - assertGe(getLPTokenBalanceInAssets(address(pool)), pink); + assertGe(pool.assetBalance(), pink); if (originalSin + part * RAY >= originalDai) { - assertApproxEqAbs(vat.sin(address(vow)), originalSin + part * RAY - originalDai, RAY * roundingTolerance); + assertRoundingEq(vat.sin(address(vow)), originalSin + part * RAY - originalDai); assertEq(vat.dai(address(vow)), 0); } else { - assertApproxEqAbs(vat.dai(address(vow)), originalDai - originalSin - part * RAY, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), originalDai - originalSin - part * RAY); assertEq(vat.sin(address(vow)), 0); } @@ -591,32 +569,32 @@ abstract contract IntegrationBaseTest is DssTest { vow.heal(_min(vat.sin(address(vow)), vat.dai(address(vow)))); // A part can't be unwind yet - assertEq(vat.gem(ilk, address(end)), amountSupplied / 2); - assertGe(getLPTokenBalanceInAssets(address(pool)), amountSupplied / 2); - if (originalSin + part * RAY >= originalDai + (amountSupplied / 2) * RAY) { + assertRoundingEq(vat.gem(ilk, address(end)), standardDebtSize / 2); + assertRoundingEq(pool.assetBalance(), standardDebtSize / 2); + if (originalSin + part * RAY >= originalDai + (standardDebtSize / 2) * RAY) { // rounding may affect twice, and multiplied by RAY to be compared with sin - assertApproxEqAbs(vat.sin(address(vow)), originalSin + part * RAY - originalDai - (amountSupplied / 2) * RAY, 2 * RAY * roundingTolerance); + assertRoundingEq(vat.sin(address(vow)), originalSin + part * RAY - originalDai - (standardDebtSize / 2) * RAY); assertEq(vat.dai(address(vow)), 0); } else { // rounding may affect twice, and multiplied by RAY to be compared with sin - assertApproxEqAbs(vat.dai(address(vow)), originalDai + (amountSupplied / 2) * RAY - originalSin - part * RAY, 2 * RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), originalDai + (standardDebtSize / 2) * RAY - originalSin - part * RAY); assertEq(vat.sin(address(vow)), 0); } // Then pool gets some liquidity - adjustLiquidity(int256(amountToBorrow)); + setLiquidity(standardDebtSize / 2); // 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); - assertEq(getLPTokenBalanceInAssets(address(pool)), 0); + assertApproxEqAbs(pool.assetBalance(), 0, 1 ether); assertEq(vat.sin(address(vow)), 0); - assertApproxEqAbs(vat.dai(address(vow)), originalDai - originalSin + daiEarned * RAY, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), originalDai - originalSin + daiEarned * RAY); } function test_uncull_not_culled() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); vm.prank(admin); hub.cage(ilk); // MCD shutdowns @@ -626,10 +604,10 @@ abstract contract IntegrationBaseTest is DssTest { } function test_uncull_not_shutdown() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); vm.prank(admin); hub.cage(ilk); - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); vm.warp(block.timestamp + tau); hub.cull(ilk); @@ -638,7 +616,8 @@ abstract contract IntegrationBaseTest is DssTest { } function test_cage_exit() public { - adjustDebt(200 ether); + setDebt(standardDebtSize); + setLiquidity(0); // Vat is caged for global settlement vm.prank(admin); end.cage(); @@ -646,21 +625,21 @@ abstract contract IntegrationBaseTest is DssTest { end.skim(ilk, address(pool)); // Simulate DAI holder gets some gems from GS - vm.prank(address(end)); vat.flux(ilk, address(end), address(this), 100 ether); + uint256 takeAmount = standardDebtSize / 2; + vm.prank(address(end)); vat.flux(ilk, address(end), address(this), takeAmount); - uint256 totalArt = end.Art(ilk); + uint256 totalTokens = getTokenBalance(address(pool)); - assertEq(getLPTokenBalanceInAssets(address(this)), 0); + assertEq(getTokenBalance(address(this)), 0); - // User can exit and get the LP token - uint256 expected = 100 ether * getLPTokenBalanceInAssets(address(pool)) / totalArt; - hub.exit(ilk, address(this), 100 ether); - assertRoundingEq(expected, 100 ether); - assertRoundingEq(getLPTokenBalanceInAssets(address(this)), expected); // As the whole thing happened in a block (no fees) + // User can exit and get the Token + hub.exit(ilk, address(this), takeAmount); + assertRoundingEq(getTokenBalance(address(this)), totalTokens / 2); } function test_cage_exit_multiple() public { - adjustDebt(200 ether); + setDebt(standardDebtSize); + setLiquidity(0); // Vat is caged for global settlement vm.prank(admin); end.cage(); @@ -672,59 +651,61 @@ abstract contract IntegrationBaseTest is DssTest { // Simulate DAI holder gets some gems from GS vm.prank(address(end)); vat.flux(ilk, address(end), address(this), totalArt); - assertEq(getLPTokenBalanceInAssets(address(this)), 0); + assertEq(getTokenBalance(address(this)), 0); - // User can exit and get the LP - uint256 expectedLP = 25 ether * getLPTokenBalanceInAssets(address(pool)) / totalArt; - hub.exit(ilk, address(this), 25 ether); - assertRoundingEq(expectedLP, 25 ether); - assertRoundingEq(getLPTokenBalanceInAssets(address(this)), expectedLP); // As the whole thing happened in a block (no fees) + // User can exit and get the Token + uint256 takeAmount = standardDebtSize / 8; + uint256 expectedToken = takeAmount * getTokenBalance(address(pool)) / totalArt; + hub.exit(ilk, address(this), takeAmount); + assertRoundingEq(getTokenBalance(address(this)), expectedToken); + // Interest can come in the form of more tokens or higher value / token generateInterest(); - uint256 expectedLP2 = 25 ether * getLPTokenBalanceInAssets(address(pool)) / (totalArt - 25 ether); - assertGt(expectedLP2, expectedLP); - hub.exit(ilk, address(this), 25 ether); - assertGt(getLPTokenBalanceInAssets(address(this)), expectedLP + expectedLP2); // As fees were accrued + uint256 expectedToken2 = takeAmount * getTokenBalance(address(pool)) / (totalArt - standardDebtSize / 8); + hub.exit(ilk, address(this), takeAmount); + assertRoundingEq(getTokenBalance(address(this)), expectedToken + expectedToken2); generateInterest(); - uint256 expectedLP3 = 50 ether * getLPTokenBalanceInAssets(address(pool)) / (totalArt - 50 ether); - assertGt(expectedLP3, expectedLP + expectedLP2); - hub.exit(ilk, address(this), 50 ether); - assertGt(getLPTokenBalanceInAssets(address(this)), expectedLP + expectedLP2 + expectedLP3); // As fees were accrued + takeAmount = standardDebtSize / 4; + uint256 expectedToken3 = takeAmount * getTokenBalance(address(pool)) / (totalArt - standardDebtSize / 4); + hub.exit(ilk, address(this), takeAmount); + assertRoundingEq(getTokenBalance(address(this)), expectedToken + expectedToken2 + expectedToken3); generateInterest(); - uint256 expectedLP4 = (totalArt - 100 ether) * getLPTokenBalanceInAssets(address(pool)) / (totalArt - 100 ether); - hub.exit(ilk, address(this), (totalArt - 100 ether)); - assertGt(getLPTokenBalanceInAssets(address(this)), expectedLP + expectedLP2 + expectedLP3 + expectedLP4); // As fees were accrued - assertEq(getLPTokenBalanceInAssets(address(pool)), 0); + takeAmount = (totalArt - standardDebtSize / 2); + uint256 expectedToken4 = takeAmount * getTokenBalance(address(pool)) / (totalArt - standardDebtSize / 2); + hub.exit(ilk, address(this), takeAmount); + assertRoundingEq(getTokenBalance(address(this)), expectedToken + expectedToken2 + expectedToken3 + expectedToken4); + assertApproxEqAbs(pool.assetBalance(), 0, 1 ether); } function test_shutdown_cant_cull() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); vm.prank(admin); hub.cage(ilk); // Vat is caged for global settlement vm.prank(admin); vat.cage(); - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); vm.warp(block.timestamp + tau); assertRevert(address(hub), abi.encodeWithSignature("cull(bytes32)", ilk), "D3MHub/no-cull-during-shutdown"); } function test_quit_no_cull() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); vm.prank(admin); hub.cage(ilk); // Test that we can extract the whole position in emergency situations - // LP should be sitting in the deposit contract, urn should be owned by deposit contract + // Token should be sitting in the deposit contract, urn should be owned by deposit contract (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); - uint256 pbal = getLPTokenBalanceInAssets(address(pool)); + uint256 pbal = pool.assetBalance(); assertGt(pink, 0); assertGt(part, 0); assertGt(pbal, 0); @@ -736,32 +717,33 @@ abstract contract IntegrationBaseTest is DssTest { vm.prank(admin); vat.grab(ilk, address(receiver), address(receiver), address(receiver), int256(pink), int256(part)); (uint256 nink, uint256 nart) = vat.urns(ilk, address(pool)); - uint256 nbal = getLPTokenBalanceInAssets(address(pool)); + uint256 nbal = pool.assetBalance(); assertEq(nink, 0); assertEq(nart, 0); assertEq(nbal, 0); (uint256 ink, uint256 art) = vat.urns(ilk, receiver); - uint256 bal = getLPTokenBalanceInAssets(receiver); + uint256 bal = getTokenBalanceInAssets(receiver) + dai.balanceOf(receiver); assertEq(ink, pink); assertEq(art, part); assertEq(bal, pbal); } function test_quit_cull() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); + setLiquidity(standardDebtSize / 2); vm.prank(admin); hub.cage(ilk); - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); vm.warp(block.timestamp + tau); hub.cull(ilk); - // Test that we can extract the lp token in emergency situations - // LP token should be sitting in the deposit contract, gems should be owned by deposit contract + // Test that we can extract the token in emergency situations + // Token should be sitting in the deposit contract, gems should be owned by deposit contract uint256 pgem = vat.gem(ilk, address(pool)); - uint256 pbal = getLPTokenBalanceInAssets(address(pool)); + uint256 pbal = pool.assetBalance(); assertGt(pgem, 0); assertGt(pbal, 0); @@ -771,18 +753,18 @@ abstract contract IntegrationBaseTest is DssTest { vm.prank(admin); vat.slip(ilk, address(pool), -int256(pgem)); uint256 ngem = vat.gem(ilk, address(pool)); - uint256 nbal = getLPTokenBalanceInAssets(address(pool)); + uint256 nbal = pool.assetBalance(); assertEq(ngem, 0); assertEq(nbal, 0); uint256 gem = vat.gem(ilk, receiver); - uint256 bal = getLPTokenBalanceInAssets(receiver); + uint256 bal = getTokenBalanceInAssets(receiver) + dai.balanceOf(receiver); assertEq(gem, 0); assertEq(bal, pbal); } function test_direct_deposit_mom() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); (uint256 ink, ) = vat.urns(ilk, address(pool)); assertGt(ink, 0); @@ -801,17 +783,17 @@ abstract contract IntegrationBaseTest is DssTest { } function test_set_tau_not_caged() public { - (, , uint256 tau, , ) = hub.ilks(ilk); + (, , , uint256 tau, , ) = hub.ilks(ilk); assertEq(tau, 7 days); vm.prank(admin); hub.file(ilk, "tau", 1 days); - (, , tau, , ) = hub.ilks(ilk); + (, , , tau, , ) = hub.ilks(ilk); assertEq(tau, 1 days); } function test_fully_unwind_debt_paid_back() public { uint256 liquidityBalanceInitial = getLiquidity(); - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); uint256 gemBefore = vat.gem(ilk, address(pool)); @@ -819,10 +801,10 @@ abstract contract IntegrationBaseTest is DssTest { uint256 sinBefore = vat.sin(address(vow)); uint256 vowDaiBefore = vat.dai(address(vow)); uint256 liquidityBalanceBefore = getLiquidity(); - uint256 assetsBalanceBefore = getLPTokenBalanceInAssets(address(pool)); + uint256 assetsBalanceBefore = pool.assetBalance(); // Someone pays back our debt - dai.setBalance(address(this), 10 * WAD); + deal(address(dai), address(this), 10 * WAD); dai.approve(address(daiJoin), type(uint256).max); daiJoin.join(address(this), 10 * WAD); vat.frob( @@ -843,7 +825,7 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.sin(address(vow)), sinBefore); assertEq(vat.dai(address(vow)), vowDaiBefore); assertEq(getLiquidity(), liquidityBalanceBefore); - assertEq(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore); + assertEq(pool.assetBalance(), assetsBalanceBefore); // We should be able to close out the vault completely even though ink and art do not match vm.prank(admin); plan.disable(); @@ -856,13 +838,13 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.gem(ilk, address(pool)), 0); assertEq(vat.vice(), viceBefore); assertEq(vat.sin(address(vow)), sinBefore); - assertApproxEqAbs(vat.dai(address(vow)), vowDaiBefore + 10 * RAD, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), vowDaiBefore + 10 * RAD); assertRoundingEq(getLiquidity(), liquidityBalanceInitial); - assertEq(getLPTokenBalanceInAssets(address(pool)), 0); + assertEq(pool.assetBalance(), 0); } function test_wind_partial_unwind_wind_debt_paid_back() public { - adjustDebt(standardDebtSize); + setDebt(standardDebtSize); (uint256 pink, uint256 part) = vat.urns(ilk, address(pool)); uint256 gemBefore = vat.gem(ilk, address(pool)); @@ -870,10 +852,9 @@ abstract contract IntegrationBaseTest is DssTest { uint256 sinBefore = vat.sin(address(vow)); uint256 vowDaiBefore = vat.dai(address(vow)); uint256 liquidityBalanceBefore = getLiquidity(); - uint256 assetsBalanceBefore = getLPTokenBalanceInAssets(address(pool)); // Someone pays back our debt - dai.setBalance(address(this), 10 * WAD); + deal(address(dai), address(this), 10 * WAD); dai.approve(address(daiJoin), type(uint256).max); daiJoin.join(address(this), 10 * WAD); vat.frob( @@ -894,7 +875,6 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.sin(address(vow)), sinBefore); assertEq(vat.dai(address(vow)), vowDaiBefore); assertEq(getLiquidity(), liquidityBalanceBefore); - assertEq(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore); hub.exec(ilk); @@ -905,12 +885,11 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.gem(ilk, address(pool)), gemBefore); assertEq(vat.vice(), viceBefore); assertEq(vat.sin(address(vow)), sinBefore); - assertApproxEqAbs(vat.dai(address(vow)), vowDaiBefore + 10 * RAD, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), vowDaiBefore + 10 * RAD); assertEq(getLiquidity(), liquidityBalanceBefore); - assertEq(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore); // Decrease debt - adjustDebt(-standardDebtSize / 2); + setDebt(standardDebtSize / 2); (ink, art) = vat.urns(ilk, address(pool)); assertLt(ink, pink); @@ -918,13 +897,12 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(ink, art); assertEq(vat.vice(), viceBefore); assertEq(vat.sin(address(vow)), sinBefore); - assertApproxEqAbs(vat.dai(address(vow)), vowDaiBefore + 10 * RAD, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), vowDaiBefore + 10 * RAD); assertEq(vat.gem(ilk, address(pool)), gemBefore); assertLt(getLiquidity(), liquidityBalanceBefore); - assertLt(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore); // can re-wind and have the correct amount of debt - adjustDebt(standardDebtSize / 2); + setDebt(standardDebtSize); (ink, art) = vat.urns(ilk, address(pool)); assertRoundingEq(ink, pink); @@ -933,8 +911,7 @@ abstract contract IntegrationBaseTest is DssTest { assertEq(vat.gem(ilk, address(pool)), gemBefore); assertEq(vat.vice(), viceBefore); assertEq(vat.sin(address(vow)), sinBefore); - assertApproxEqAbs(vat.dai(address(vow)), vowDaiBefore + 10 * RAD, RAY * roundingTolerance); + assertRoundingEq(vat.dai(address(vow)), vowDaiBefore + 10 * RAD); assertRoundingEq(getLiquidity(), liquidityBalanceBefore); - assertApproxEqAbs(getLPTokenBalanceInAssets(address(pool)), assetsBalanceBefore, 2 * roundingTolerance); // rounding may affect twice } } diff --git a/src/tests/integration/LinearFeeSwap.t.sol b/src/tests/integration/LinearFeeSwap.t.sol new file mode 100644 index 00000000..ea4ff21c --- /dev/null +++ b/src/tests/integration/LinearFeeSwap.t.sol @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: © 2023 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 "./IntegrationBase.t.sol"; + +import "./SwapPoolBase.t.sol"; + +import { D3MLinearFeeSwapPool } from "../../pools/D3MLinearFeeSwapPool.sol"; + +abstract contract LinearFeeSwapBaseTest is SwapPoolBaseTest { + + D3MLinearFeeSwapPool pool; + + function deployPool() internal override returns (address) { + pool = D3MLinearFeeSwapPool(D3MDeploy.deployLinearFeeSwapPool( + address(this), + admin, + ilk, + address(hub), + address(dai), + address(gem) + )); + return address(pool); + } + +} + +contract USDCSwapTest is LinearFeeSwapBaseTest { + + function getGem() internal override pure returns (address) { + return 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + } + + function getPip() internal override pure returns (address) { + return 0x77b68899b99b686F415d074278a9a16b336085A0; // Hardcoded $1 pip + } + + function getSwapGemForDaiPip() internal override pure returns (address) { + return 0x77b68899b99b686F415d074278a9a16b336085A0; + } + + function getSwapDaiForGemPip() internal override pure returns (address) { + return 0x77b68899b99b686F415d074278a9a16b336085A0; + } + +} + +contract GUSDSwapTest is LinearFeeSwapBaseTest { + + using stdStorage for StdStorage; + + function getGem() internal override pure returns (address) { + return 0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd; + } + + function getPip() internal override pure returns (address) { + return 0xf45Ae69CcA1b9B043dAE2C83A5B65Bc605BEc5F5; // Hardcoded $1 pip + } + + function getSwapGemForDaiPip() internal override pure returns (address) { + return 0xf45Ae69CcA1b9B043dAE2C83A5B65Bc605BEc5F5; + } + + function getSwapDaiForGemPip() internal override pure returns (address) { + return 0xf45Ae69CcA1b9B043dAE2C83A5B65Bc605BEc5F5; + } + + // GUSD has a separate storage contract so we need to override deal + function deal(address token, address to, uint256 give) internal override { + if (token == getGem()) { + // Target the storage contract for GUSD + stdstore.target(0xc42B14e49744538e3C239f8ae48A1Eaaf35e68a0).sig(bytes4(abi.encodeWithSignature("balances(address)"))).with_key(to).checked_write(give); + } else { + super.deal(token, to, give); + } + } + +} + +contract USDPSwapTest is LinearFeeSwapBaseTest { + + function getGem() internal override pure returns (address) { + return 0x8E870D67F660D95d5be530380D0eC0bd388289E1; + } + + function getPip() internal override pure returns (address) { + return 0x043B963E1B2214eC90046167Ea29C2c8bDD7c0eC; // Hardcoded $1 pip + } + + function getSwapGemForDaiPip() internal override pure returns (address) { + return 0x043B963E1B2214eC90046167Ea29C2c8bDD7c0eC; + } + + function getSwapDaiForGemPip() internal override pure returns (address) { + return 0x043B963E1B2214eC90046167Ea29C2c8bDD7c0eC; + } + +} + +contract BackedIB01SwapTest is LinearFeeSwapBaseTest { + + PipMock private _pip; + + function baseInit() internal override { + super.baseInit(); + + // Setup an oracle + _pip = new PipMock(); + _pip.poke(1.2 ether); // Set to some non-$1 value + } + + function getGem() internal override pure returns (address) { + return 0xCA30c93B02514f86d5C86a6e375E3A330B435Fb5; + } + + function getPip() internal override view returns (address) { + return address(_pip); + } + + function getSwapGemForDaiPip() internal override view returns (address) { + return address(_pip); + } + + function getSwapDaiForGemPip() internal override view returns (address) { + return address(_pip); + } + + // Backed IB01 earns interest by asset value appreciation vs getting more tokens + function generateInterest() internal override { + _pip.poke(uint256(_pip.read()) * 101 / 100); + } + +} diff --git a/src/tests/integration/SparkLend.t.sol b/src/tests/integration/SparkLend.t.sol index 22538e17..2b1d3d07 100644 --- a/src/tests/integration/SparkLend.t.sol +++ b/src/tests/integration/SparkLend.t.sol @@ -18,6 +18,7 @@ pragma solidity ^0.8.14; import "./IntegrationBase.t.sol"; import { DSTokenAbstract } from "dss-interfaces/Interfaces.sol"; +import { D3MForwardFees } from "../../fees/D3MForwardFees.sol"; import { D3MAaveTypeBufferPlan } from "../../plans/D3MAaveTypeBufferPlan.sol"; import { D3MAaveV3NoSupplyCapTypePool } from "../../pools/D3MAaveV3NoSupplyCapTypePool.sol"; @@ -73,11 +74,6 @@ interface PoolLike { function mintToTreasury(address[] calldata assets) external; } -interface DaiInterestRateStrategyLike { - function recompute() external; - function performanceBonus() external view returns (uint256); -} - interface ATokenLike { function balanceOf(address) external view returns (uint256); function approve(address, uint256) external returns (bool); @@ -90,108 +86,35 @@ interface ATokenLike { function getIncentivesController() external view returns (address); } -interface TreasuryLike { - function getFundsAdmin() external view returns (TreasuryAdminLike); -} - -interface TreasuryAdminLike { - function transfer( - address collector, - address token, - address recipient, - uint256 amount - ) external; -} - -interface IERC3156FlashBorrower { - - /** - * @dev Receive a flash loan. - * @param initiator The initiator of the loan. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @param fee The additional amount of tokens to repay. - * @param data Arbitrary data structure, intended to contain user-defined parameters. - * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" - */ - function onFlashLoan( - address initiator, - address token, - uint256 amount, - uint256 fee, - bytes calldata data - ) external returns (bytes32); -} - -interface IERC3156FlashLender { - - /** - * @dev The amount of currency available to be lent. - * @param token The loan currency. - * @return The amount of `token` that can be borrowed. - */ - function maxFlashLoan( - address token - ) external view returns (uint256); - - /** - * @dev The fee to be charged for a given loan. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @return The amount of `token` to be charged for the loan, on top of the returned principal. - */ - function flashFee( - address token, - uint256 amount - ) external view returns (uint256); - - /** - * @dev Initiate a flash loan. - * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @param data Arbitrary data structure, intended to contain user-defined parameters. - */ - function flashLoan( - IERC3156FlashBorrower receiver, - address token, - uint256 amount, - bytes calldata data - ) external returns (bool); +interface DaiInterestRateStrategyLike { + function recompute() external; + function performanceBonus() external view returns (uint256); } -contract SparkLendTest is IntegrationBaseTest, IERC3156FlashBorrower { +contract SparkLendTest is IntegrationBaseTest { using stdJson for string; using MCD for *; - using GodMode for *; using ScriptTools for *; PoolLike sparkPool; DaiInterestRateStrategyLike daiInterestRateStrategy; ATokenLike adai; - TreasuryLike treasury; - TreasuryAdminLike treasuryAdmin; - uint256 buffer; - IERC3156FlashLender flashLender; + address someUser = address(0x1234); D3MAaveTypeBufferPlan plan; D3MAaveV3NoSupplyCapTypePool pool; function setUp() public { - baseInit(); - // NOTE: Adding past block until fix to work against deployed protocol is introduced. // TODO: Update the test to work against deployed protocol with latest block. vm.createSelectFork(vm.envString("ETH_RPC_URL"), 17_200_000); + baseInit(); + sparkPool = PoolLike(0xC13e21B648A5Ee794902342038FF3aDAB66BE987); daiInterestRateStrategy = DaiInterestRateStrategyLike(getInterestRateStrategy(address(dai))); adai = ATokenLike(0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B); - treasury = TreasuryLike(adai.RESERVE_TREASURY_ADDRESS()); - treasuryAdmin = treasury.getFundsAdmin(); - buffer = 5_000_000 * WAD; - flashLender = IERC3156FlashLender(dss.chainlog.getAddress("MCD_FLASH")); // Deploy d3m.oracle = D3MDeploy.deployOracle( @@ -215,6 +138,10 @@ contract SparkLendTest is IntegrationBaseTest, IERC3156FlashBorrower { address(adai) ); plan = D3MAaveTypeBufferPlan(d3m.plan); + d3m.fees = D3MDeploy.deployForwardFees( + address(vat), + address(vow) + ); // Init vm.startPrank(admin); @@ -224,8 +151,8 @@ contract SparkLendTest is IntegrationBaseTest, IERC3156FlashBorrower { mom: address(mom), ilk: ilk, existingIlk: false, - maxLine: buffer * RAY * 100000, // Set gap and max line to large number to avoid hitting limits - gap: buffer * RAY * 100000, + maxLine: standardDebtCeiling * RAY, + gap: standardDebtCeiling * RAY, ttl: 0, tau: 7 days }); @@ -248,25 +175,22 @@ contract SparkLendTest is IntegrationBaseTest, IERC3156FlashBorrower { D3MInit.initAaveBufferPlan( d3m, D3MAaveBufferPlanConfig({ - buffer: buffer, + buffer: standardDebtSize * 5, adai: address(pool.adai()) }) ); vm.stopPrank(); - // Give us some DAI - dai.setBalance(address(this), buffer * 100000000); - - // Deposit WETH into the pool + // Deposit WETH into the pool so we have effectively unlimited collateral to borrow against + vm.startPrank(someUser); uint256 amt = 1_000_000 * WAD; DSTokenAbstract weth = DSTokenAbstract(dss.getIlk("ETH", "A").gem); - weth.setBalance(address(this), amt); + deal(address(weth), someUser, amt); weth.approve(address(sparkPool), type(uint256).max); dai.approve(address(sparkPool), type(uint256).max); - sparkPool.supply(address(weth), amt, address(this), 0); - - assertGt(getDebtCeiling(), 0); + sparkPool.supply(address(weth), amt, someUser, 0); + vm.stopPrank(); // Recompute the dai interest rate strategy to ensure the new line is taken into account daiInterestRateStrategy.recompute(); @@ -275,231 +199,67 @@ contract SparkLendTest is IntegrationBaseTest, IERC3156FlashBorrower { } // --- Overrides --- - function adjustDebt(int256 deltaAmount) internal override { - if (deltaAmount == 0) return; - - int256 newBuffer = int256(plan.buffer()) + deltaAmount; - vm.prank(admin); plan.file("buffer", newBuffer >= 0 ? uint256(newBuffer) : 0); + function setDebt(uint256 amount) internal override { + vm.prank(admin); plan.file("buffer", amount); hub.exec(ilk); } - function adjustLiquidity(int256 deltaAmount) internal override { - if (deltaAmount == 0) return; - - if (deltaAmount > 0) { + function setLiquidity(uint256 amount) internal override { + vm.startPrank(someUser); + uint256 currLiquidity = getLiquidity(); + if (amount >= currLiquidity) { // Supply to increase liquidity - uint256 amt = uint256(deltaAmount); - dai.setBalance(address(this), dai.balanceOf(address(this)) + amt); - sparkPool.supply(address(dai), amt, address(0), 0); + uint256 amt = amount - currLiquidity; + deal(address(dai), someUser, dai.balanceOf(someUser) + amt); + sparkPool.supply(address(dai), amt, someUser, 0); } else { // Borrow to decrease liquidity - uint256 amt = uint256(-deltaAmount); - sparkPool.borrow(address(dai), amt, 2, 0, address(this)); + uint256 amt = currLiquidity - amount; + sparkPool.borrow(address(dai), amt, 2, 0, someUser); } + vm.stopPrank(); } function generateInterest() internal override { // Generate interest by borrowing and repaying + vm.startPrank(someUser); uint256 performanceBonus = daiInterestRateStrategy.performanceBonus(); - sparkPool.supply(address(dai), performanceBonus * 4, address(this), 0); - sparkPool.borrow(address(dai), performanceBonus * 2, 2, 0, address(this)); + if (performanceBonus == 0) performanceBonus = standardDebtSize; + deal(address(dai), someUser, dai.balanceOf(someUser) + performanceBonus * 4); + sparkPool.supply(address(dai), performanceBonus * 4, someUser, 0); + sparkPool.borrow(address(dai), performanceBonus * 2, 2, 0, someUser); vm.warp(block.timestamp + 1 days); - sparkPool.repay(address(dai), performanceBonus * 2, 2, address(this)); - sparkPool.withdraw(address(dai), performanceBonus * 4, address(this)); - } - - function getLiquidity() internal override view returns (uint256) { - return dai.balanceOf(address(adai)); + sparkPool.repay(address(dai), performanceBonus * 2, 2, someUser); + sparkPool.withdraw(address(dai), performanceBonus * 4, someUser); + vm.stopPrank(); } // --- Helper functions --- - function getDebtCeiling() internal view returns (uint256) { - (,,, uint256 line,) = dss.vat.ilks(ilk); - return line; - } - - function getDebt() internal view returns (uint256) { - (, uint256 art) = dss.vat.urns(ilk, address(pool)); - return art; - } - - function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) { - unchecked { - z = x != 0 ? ((x - 1) / y) + 1 : 0; - } - } - function getInterestRateStrategy(address asset) internal view returns (address) { PoolLike.ReserveData memory data = sparkPool.getReserveData(asset); return data.interestRateStrategyAddress; } - function forceUpdateIndicies(address asset) internal { - // Do the flashloan trick to force update indicies - sparkPool.flashLoanSimple(address(this), asset, 1, "", 0); - } - - function executeOperation( - address, - uint256, - uint256, - address, - bytes calldata - ) external pure returns (bool) { - // Flashloan callback just immediately returns - return true; - } - - function getTotalAssets(address asset) internal view returns (uint256) { - // Assets = DAI Liquidity + Total Debt - PoolLike.ReserveData memory data = sparkPool.getReserveData(asset); - return dai.balanceOf(address(adai)) + ATokenLike(data.variableDebtTokenAddress).totalSupply() + ATokenLike(data.stableDebtTokenAddress).totalSupply(); - } - - function getTotalLiabilities(address asset) internal view returns (uint256) { - // Liabilities = spDAI Supply + Amount Accrued to Treasury - PoolLike.ReserveData memory data = sparkPool.getReserveData(asset); - return _divup((adai.scaledTotalSupply() + uint256(data.accruedToTreasury)) * data.liquidityIndex, RAY); - } - - function getAccruedToTreasury(address asset) internal view returns (uint256) { - PoolLike.ReserveData memory data = sparkPool.getReserveData(asset); - return data.accruedToTreasury; - } - // --- Tests --- function test_simple_wind_unwind() public { - setLiquidityToZero(); - assertEq(getDebt(), 0); + uint256 buffer = plan.buffer(); hub.exec(ilk); - assertEq(getDebt(), buffer, "should wind up to the buffer"); + assertRoundingEq(getDebt(), buffer, "should wind up to the buffer"); // User borrows half the debt injected by the D3M - sparkPool.borrow(address(dai), buffer / 2, 2, 0, address(this)); - assertEq(getDebt(), buffer); + vm.prank(someUser); sparkPool.borrow(address(dai), buffer / 2, 2, 0, someUser); + assertRoundingEq(getDebt(), buffer); hub.exec(ilk); - assertEq(getDebt(), buffer + buffer / 2, "should have 1.5x the buffer in debt"); + assertRoundingEq(getDebt(), buffer + buffer / 2, "should have 1.5x the buffer in debt"); // User repays half their debt - sparkPool.repay(address(dai), buffer / 4, 2, address(this)); - assertEq(getDebt(), buffer + buffer / 2); + vm.prank(someUser); sparkPool.repay(address(dai), buffer / 4, 2, someUser); + assertRoundingEq(getDebt(), buffer + buffer / 2); hub.exec(ilk); - assertEq(getDebt(), buffer + buffer / 2 - buffer / 4, "should be back down to 1.25x the buffer"); - } - - /** - * The DAI market is using a new interest model which over-allocates interest to the treasury. - * This is due to the reserve factor not being flexible enough to account for this. - * Confirm that we can later correct the discrepancy by donating the excess liabilities back to the DAI pool. (This can be automated later on) - */ - function test_asset_liabilities_fix() public { - uint256 assets = getTotalAssets(address(dai)); - uint256 liabilities = getTotalLiabilities(address(dai)); - if (assets >= liabilities) { - // Force the assets to become less than the liabilities - uint256 performanceBonus = daiInterestRateStrategy.performanceBonus(); - vm.prank(admin); plan.file("buffer", performanceBonus * 4); - hub.exec(ilk); - sparkPool.borrow(address(dai), performanceBonus * 2, 2, 0, address(this)); // Supply rate should now be above 0% (we are over-allocating) - - // Warp so we gaurantee there is new interest - vm.warp(block.timestamp + 365 days); - forceUpdateIndicies(address(dai)); - - assets = getTotalAssets(address(dai)); - liabilities = getTotalLiabilities(address(dai)); - assertLe(assets, liabilities, "assets should be less than or equal to liabilities"); - } - - // Let's fix the accounting - uint256 delta = liabilities - assets; - - // First trigger all spDAI owed to the treasury to be accrued - assertGt(getAccruedToTreasury(address(dai)), 0, "accrued to treasury should be greater than 0"); - address[] memory toMint = new address[](1); - toMint[0] = address(dai); - sparkPool.mintToTreasury(toMint); - assertEq(getAccruedToTreasury(address(dai)), 0, "accrued to treasury should be 0"); - assertGe(adai.balanceOf(address(treasury)), delta, "adai treasury should have more than the delta between liabilities and assets"); - - // Donate the excess liabilities back to the pool - // This will burn the liabilities while keeping the assets the same - vm.prank(admin); treasuryAdmin.transfer(address(treasury), address(adai), address(this), delta); - sparkPool.withdraw(address(dai), delta, address(adai)); - - assets = getTotalAssets(address(dai)) + 1; // In case of rounding error we +1 - liabilities = getTotalLiabilities(address(dai)); - assertGe(assets, liabilities, "assets should be greater than or equal to liabilities"); - } - - function test_asset_liabilities_fix_full_utilization_flashloan() public { - uint256 assets = getTotalAssets(address(dai)); - uint256 liabilities = getTotalLiabilities(address(dai)); - if (assets >= liabilities) { - // Force the assets to become less than the liabilities - uint256 performanceBonus = daiInterestRateStrategy.performanceBonus(); - vm.prank(admin); plan.file("buffer", performanceBonus * 4); - hub.exec(ilk); - sparkPool.borrow(address(dai), performanceBonus * 2, 2, 0, address(this)); // Supply rate should now be above 0% (we are over-allocating) - - // Warp so we gaurantee there is new interest - vm.warp(block.timestamp + 365 days); - forceUpdateIndicies(address(dai)); - - assets = getTotalAssets(address(dai)); - liabilities = getTotalLiabilities(address(dai)); - assertLe(assets, liabilities, "assets should be less than or equal to liabilities"); - } - - // Let's fix the accounting - uint256 delta = liabilities - assets; - - // First trigger all spDAI owed to the treasury to be accrued - assertGt(getAccruedToTreasury(address(dai)), 0, "accrued to treasury should be greater than 0"); - address[] memory toMint = new address[](1); - toMint[0] = address(dai); - sparkPool.mintToTreasury(toMint); - assertEq(getAccruedToTreasury(address(dai)), 0, "accrued to treasury should be 0"); - assertGe(adai.balanceOf(address(treasury)), delta, "adai treasury should have more than the delta between liabilities and assets"); - - // Donate the excess liabilities back to the pool - // This will burn the liabilities while keeping the assets the same - vm.prank(admin); treasuryAdmin.transfer(address(treasury), address(adai), address(this), delta); - - // Remove all DAI liquidity from the pool - sparkPool.borrow(address(dai), dai.balanceOf(address(adai)), 2, 0, address(this)); - assertEq(dai.balanceOf(address(adai)), 0); - dai.setBalance(address(this), 0); // We have no DAI as well - - // Withdrawing won't work because no available DAI - vm.expectRevert(); - sparkPool.withdraw(address(dai), delta, address(adai)); - - // Flash loan to close out the liabilities - flashLender.flashLoan(this, address(dai), delta, ""); - - assets = getTotalAssets(address(dai)) + 1; // In case of rounding error we +1 - liabilities = getTotalLiabilities(address(dai)); - assertGe(assets, liabilities, "assets should be greater than or equal to liabilities"); - } - - function onFlashLoan( - address, - address token, - uint256 amount, - uint256 fee, - bytes calldata - ) external returns (bytes32) { - sparkPool.supply(address(dai), amount, address(this), 0); - sparkPool.withdraw(address(dai), amount, address(adai)); - sparkPool.withdraw(address(dai), amount, address(this)); - - ATokenLike(token).approve(address(msg.sender), amount + fee); - - return keccak256("ERC3156FlashBorrower.onFlashLoan"); + assertRoundingEq(getDebt(), buffer + buffer / 2 - buffer / 4, "should be back down to 1.25x the buffer"); } } diff --git a/src/tests/integration/SwapPoolBase.t.sol b/src/tests/integration/SwapPoolBase.t.sol new file mode 100644 index 00000000..272ad205 --- /dev/null +++ b/src/tests/integration/SwapPoolBase.t.sol @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: © 2023 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 "./IntegrationBase.t.sol"; +import { PipMock } from "../mocks/PipMock.sol"; + +import { D3MALMDelegateControllerPlan } from "../../plans/D3MALMDelegateControllerPlan.sol"; +import { D3MSwapPool } from "../../pools/D3MSwapPool.sol"; + +abstract contract SwapPoolBaseTest is IntegrationBaseTest { + + using stdJson for string; + using MCD for *; + using ScriptTools for *; + + GemAbstract gem; + DSValueAbstract pip; + DSValueAbstract swapGemForDaiPip; + DSValueAbstract swapDaiForGemPip; + uint256 gemConversionFactor; + + D3MALMDelegateControllerPlan plan; + D3MSwapPool private pool; + + function setUp() public virtual { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + + baseInit(); + + gem = GemAbstract(getGem()); + gemConversionFactor = 10 ** (18 - gem.decimals()); + pip = DSValueAbstract(getPip()); + swapGemForDaiPip = DSValueAbstract(getSwapGemForDaiPip()); + swapDaiForGemPip = DSValueAbstract(getSwapDaiForGemPip()); + + // Deploy + d3m.oracle = D3MDeploy.deployOracle( + address(this), + admin, + ilk, + address(dss.vat) + ); + d3m.pool = deployPool(); + pool = D3MSwapPool(d3m.pool); + d3m.plan = D3MDeploy.deployALMDelegateControllerPlan( + address(this), + admin + ); + plan = D3MALMDelegateControllerPlan(d3m.plan); + d3m.fees = D3MDeploy.deployForwardFees( + address(vat), + address(vow) + ); + + // Init + vm.startPrank(admin); + + D3MCommonConfig memory cfg = D3MCommonConfig({ + hub: address(hub), + mom: address(mom), + ilk: ilk, + existingIlk: false, + maxLine: standardDebtCeiling * RAY, + gap: standardDebtCeiling * RAY, + ttl: 0, + tau: 7 days + }); + D3MInit.initCommon( + dss, + d3m, + cfg + ); + D3MInit.initSwapPool( + dss, + d3m, + cfg, + D3MSwapPoolConfig({ + gem: address(gem), + pip: address(pip), + swapGemForDaiPip: address(swapGemForDaiPip), + swapDaiForGemPip: address(swapDaiForGemPip) + }) + ); + + // Add ourselves to the plan + plan.addAllocator(address(this)); + plan.setMaxAllocation(address(this), ilk, uint128(standardDebtCeiling)); + + vm.stopPrank(); + + // Give infinite approval to the pools + dai.approve(address(pool), type(uint256).max); + gem.approve(address(pool), type(uint256).max); + + basePostSetup(); + } + + // --- To Override --- + function deployPool() internal virtual returns (address); + function getGem() internal virtual view returns (address); + function getPip() internal virtual view returns (address); + function getSwapGemForDaiPip() internal virtual view returns (address); + function getSwapDaiForGemPip() internal virtual view returns (address); + + // --- Overrides --- + function setDebt(uint256 amount) internal override virtual { + plan.setAllocation(address(this), ilk, uint128(amount)); + hub.exec(ilk); + } + + function setLiquidity(uint256 amount) internal override virtual { + (uint128 currentAllocation, uint128 max) = plan.allocations(address(this), ilk); + uint256 prev = dai.balanceOf(address(pool)); + if (amount >= prev) { + // Increase dai liquidity by swapping dai for gems (or just adding it if there isn't enough gems) + uint256 delta = amount - prev; + uint256 gemBalance = gem.balanceOf(address(pool)); + uint256 gemAmount = daiToGemRoundUp(delta); + if (gemBalance < gemAmount) { + // Ensure there is enough gems to swap + deal(address(gem), address(pool), gemAmount); + } + deal(address(dai), address(this), delta); + plan.setAllocation(address(this), ilk, 0); + pool.swapDaiForGem(address(this), delta, 0); + } else { + // Decrease DAI liquidity by swapping gems for dai + uint256 delta = prev - amount; + uint256 gemAmount = daiToGem(delta); + deal(address(gem), address(this), gemAmount); + plan.setAllocation(address(this), ilk, max); + pool.swapGemForDai(address(this), gemAmount, 0); + } + plan.setAllocation(address(this), ilk, currentAllocation); + } + + function generateInterest() internal override virtual { + // Generate interest by adding more gems to the pool + deal(address(gem), address(pool), gem.balanceOf(address(pool)) + daiToGem(standardDebtSize / 100)); + } + + function getTokenBalanceInAssets(address a) internal view override returns (uint256) { + return gemToDai(gem.balanceOf(a)); + } + + // --- Helper functions --- + function daiToGem(uint256 daiAmount) internal view returns (uint256) { + return daiAmount * WAD / (gemConversionFactor * uint256(pip.read())); + } + + function daiToGemRoundUp(uint256 daiAmount) internal view returns (uint256) { + return _divup(daiAmount * WAD, gemConversionFactor * uint256(pip.read())); + } + + function gemToDai(uint256 gemAmount) internal view returns (uint256) { + return gemAmount * (gemConversionFactor * uint256(pip.read())) / WAD; + } + + function gemToDaiRoundUp(uint256 gemAmount) internal view returns (uint256) { + return _divup(gemAmount * gemConversionFactor * uint256(pip.read()), WAD); + } + + function initSwaps() internal { + plan.setAllocation(address(this), ilk, uint128(standardDebtCeiling)); + hub.exec(ilk); + deal(address(gem), address(this), daiToGem(standardDebtCeiling)); + deal(address(dai), address(this), standardDebtCeiling); + } + + // --- Tests --- + function test_swapGemForDai() public { + initSwaps(); + + assertEq(dai.balanceOf(address(pool)), standardDebtCeiling); + assertEq(gem.balanceOf(address(pool)), 0); + pool.swapGemForDai(address(this), daiToGem(standardDebtCeiling / 2), 0); + assertRoundingEq(dai.balanceOf(address(pool)), standardDebtCeiling / 2); + assertRoundingEq(gem.balanceOf(address(pool)), daiToGem(standardDebtCeiling / 2)); + } + + function test_swapDaiForGem() public { + initSwaps(); + pool.swapGemForDai(address(this), daiToGem(standardDebtCeiling), 0); + + assertApproxEqAbs(dai.balanceOf(address(pool)), 0, 1 ether); + assertRoundingEq(gem.balanceOf(address(pool)), daiToGem(standardDebtCeiling)); + plan.setAllocation(address(this), ilk, 0); + pool.swapDaiForGem(address(this), standardDebtCeiling / 2, 0); + assertRoundingEq(dai.balanceOf(address(pool)), standardDebtCeiling / 2); + assertRoundingEq(gem.balanceOf(address(pool)), daiToGem(standardDebtCeiling / 2)); + } + +} diff --git a/src/tests/mocks/HubMock.sol b/src/tests/mocks/HubMock.sol index 7044bd44..f822f83c 100644 --- a/src/tests/mocks/HubMock.sol +++ b/src/tests/mocks/HubMock.sol @@ -21,9 +21,18 @@ import { EndMock } from "./EndMock.sol"; contract HubMock { address public immutable vat; EndMock public immutable end; + address public _plan; constructor(address vat_, address end_) { vat = vat_; end = EndMock(end_); } + + function setPlan(address plan_) external { + _plan = plan_; + } + + function plan(bytes32) external view returns (address) { + return _plan; + } } diff --git a/src/deploy/D3MCoreInstance.sol b/src/tests/mocks/PipMock.sol similarity index 74% rename from src/deploy/D3MCoreInstance.sol rename to src/tests/mocks/PipMock.sol index a9a4a9ae..ed313ead 100644 --- a/src/deploy/D3MCoreInstance.sol +++ b/src/tests/mocks/PipMock.sol @@ -14,9 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pragma solidity >=0.8.0; +pragma solidity ^0.8.14; -struct D3MCoreInstance { - address hub; - address mom; +contract PipMock { + uint256 public val; + function read() external view returns (bytes32) { + return bytes32(val); + } + function void() external { + val = 0; + } + function poke(uint256 wut) external { + val = wut; + } } diff --git a/src/tests/mocks/VatMock.sol b/src/tests/mocks/VatMock.sol index cd0b42df..3eebcc6b 100644 --- a/src/tests/mocks/VatMock.sol +++ b/src/tests/mocks/VatMock.sol @@ -17,9 +17,315 @@ pragma solidity ^0.8.14; contract VatMock { - uint256 public live = 1; - mapping(address => mapping (address => uint)) public can; - function cage() external { live = 0; } - function hope(address usr) external { can[msg.sender][usr] = 1; } - function nope(address usr) external { can[msg.sender][usr] = 0; } + // --- Data --- + mapping (address => uint256) public wards; + + mapping(address => mapping (address => uint256)) public can; + + struct Ilk { + uint256 Art; // Total Normalised Debt [wad] + uint256 rate; // Accumulated Rates [ray] + uint256 spot; // Price with Safety Margin [ray] + uint256 line; // Debt Ceiling [rad] + uint256 dust; // Urn Debt Floor [rad] + } + struct Urn { + uint256 ink; // Locked Collateral [wad] + uint256 art; // Normalised Debt [wad] + } + + mapping (bytes32 => Ilk) public ilks; + mapping (bytes32 => mapping (address => Urn)) public urns; + mapping (bytes32 => mapping (address => uint256)) public gem; // [wad] + mapping (address => uint256) public dai; // [rad] + mapping (address => uint256) public sin; // [rad] + + uint256 public debt; // Total Dai Issued [rad] + uint256 public vice; // Total Unbacked Dai [rad] + uint256 public Line; // Total Debt Ceiling [rad] + uint256 public live; // Active Flag + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + event Init(bytes32 indexed ilk); + event File(bytes32 indexed what, uint256 data); + event File(bytes32 indexed ilk, bytes32 indexed what, uint256 data); + event Cage(); + event Hope(address indexed from, address indexed to); + event Nope(address indexed from, address indexed to); + event Slip(bytes32 indexed ilk, address indexed usr, int256 wad); + event Flux(bytes32 indexed ilk, address indexed src, address indexed dst, uint256 wad); + event Move(address indexed src, address indexed dst, uint256 rad); + event Frob(bytes32 indexed i, address indexed u, address v, address w, int256 dink, int256 dart); + event Fork(bytes32 indexed ilk, address indexed src, address indexed dst, int256 dink, int256 dart); + event Grab(bytes32 indexed i, address indexed u, address v, address w, int256 dink, int256 dart); + event Heal(address indexed u, uint256 rad); + event Suck(address indexed u, address indexed v, uint256 rad); + event Fold(bytes32 indexed i, address indexed u, int256 rate); + + modifier auth { + require(wards[msg.sender] == 1, "Vat/not-authorized"); + _; + } + + function wish(address bit, address usr) internal view returns (bool) { + return either(bit == usr, can[bit][usr] == 1); + } + + // --- Init --- + constructor() { + wards[msg.sender] = 1; + live = 1; + emit Rely(msg.sender); + } + + // --- Math --- + string private constant ARITHMETIC_ERROR = string(abi.encodeWithSignature("Panic(uint256)", 0x11)); + function _add(uint256 x, int256 y) internal pure returns (uint256 z) { + unchecked { + z = x + uint256(y); + } + require(y >= 0 || z <= x, ARITHMETIC_ERROR); + require(y <= 0 || z >= x, ARITHMETIC_ERROR); + } + function _sub(uint256 x, int256 y) internal pure returns (uint256 z) { + unchecked { + z = x - uint256(y); + } + require(y <= 0 || z <= x, ARITHMETIC_ERROR); + require(y >= 0 || z >= x, ARITHMETIC_ERROR); + } + function _int256(uint256 x) internal pure returns (int256 y) { + require((y = int256(x)) >= 0, ARITHMETIC_ERROR); + } + + // --- Administration --- + function rely(address usr) external auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 0; + emit Deny(usr); + } + + function init(bytes32 ilk) external auth { + require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); + ilks[ilk].rate = 10 ** 27; + emit Init(ilk); + } + + function file(bytes32 what, uint256 data) external auth { + require(live == 1, "Vat/not-live"); + if (what == "Line") Line = data; + else revert("Vat/file-unrecognized-param"); + emit File(what, data); + } + + function file(bytes32 ilk, bytes32 what, uint256 data) external auth { + require(live == 1, "Vat/not-live"); + if (what == "spot") ilks[ilk].spot = data; + else if (what == "line") ilks[ilk].line = data; + else if (what == "dust") ilks[ilk].dust = data; + else revert("Vat/file-unrecognized-param"); + emit File(ilk, what, data); + } + + function cage() external auth { + live = 0; + emit Cage(); + } + + // --- Structs getters --- + function Art(bytes32 ilk) external view returns (uint256 Art_) { + Art_ = ilks[ilk].Art; + } + + function rate(bytes32 ilk) external view returns (uint256 rate_) { + rate_ = ilks[ilk].rate; + } + + function spot(bytes32 ilk) external view returns (uint256 spot_) { + spot_ = ilks[ilk].spot; + } + + function line(bytes32 ilk) external view returns (uint256 line_) { + line_ = ilks[ilk].line; + } + + function dust(bytes32 ilk) external view returns (uint256 dust_) { + dust_ = ilks[ilk].dust; + } + + function ink(bytes32 ilk, address urn) external view returns (uint256 ink_) { + ink_ = urns[ilk][urn].ink; + } + + function art(bytes32 ilk, address urn) external view returns (uint256 art_) { + art_ = urns[ilk][urn].art; + } + + // --- Allowance --- + function hope(address usr) external { + can[msg.sender][usr] = 1; + emit Hope(msg.sender, usr); + } + + function nope(address usr) external { + can[msg.sender][usr] = 0; + emit Nope(msg.sender, usr); + } + + // --- Fungibility --- + function slip(bytes32 ilk, address usr, int256 wad) external auth { + gem[ilk][usr] = _add(gem[ilk][usr], wad); + emit Slip(ilk, usr, wad); + } + + function flux(bytes32 ilk, address src, address dst, uint256 wad) external { + require(wish(src, msg.sender), "Vat/not-allowed"); + gem[ilk][src] = gem[ilk][src] - wad; + gem[ilk][dst] = gem[ilk][dst] + wad; + emit Flux(ilk, src, dst, wad); + } + + function move(address src, address dst, uint256 rad) external { + require(wish(src, msg.sender), "Vat/not-allowed"); + dai[src] = dai[src] - rad; + dai[dst] = dai[dst] + rad; + emit Move(src, dst, rad); + } + + function either(bool x, bool y) internal pure returns (bool z) { + assembly{ z := or(x, y)} + } + + function both(bool x, bool y) internal pure returns (bool z) { + assembly{ z := and(x, y)} + } + + // --- CDP Manipulation --- + function frob(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external { + // system is live + require(live == 1, "Vat/not-live"); + + uint256 rate_ = ilks[i].rate; + // ilk has been initialised + require(rate_ != 0, "Vat/ilk-not-init"); + + Urn memory urn = urns[i][u]; + urn.ink = _add(urn.ink, dink); + urn.art = _add(urn.art, dart); + + uint256 Art_ = _add(ilks[i].Art, dart); + int256 dtab = _int256(rate_) * dart; + uint256 debt_ = _add(debt, dtab); + + // either debt has decreased, or debt ceilings are not exceeded + require(either(dart <= 0, both(Art_ * rate_ <= ilks[i].line, debt_ <= Line)), "Vat/ceiling-exceeded"); + uint256 tab = rate_ * urn.art; + // urn is either less risky than before, or it is safe + require(either(both(dart <= 0, dink >= 0), tab <= urn.ink * ilks[i].spot), "Vat/not-safe"); + + // urn is either more safe, or the owner consents + require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u"); + // collateral src consents + require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v"); + // debt dst consents + require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w"); + + // urn has no debt, or a non-dusty amount + require(either(urn.art == 0, tab >= ilks[i].dust), "Vat/dust"); + + // update storage values + gem[i][v] = _sub(gem[i][v], dink); + dai[w] = _add(dai[w], dtab); + urns[i][u] = urn; + ilks[i].Art = Art_; + debt = debt_; + + emit Frob(i, u, v, w, dink, dart); + } + + // --- CDP Fungibility --- + function fork(bytes32 ilk, address src, address dst, int256 dink, int256 dart) external { + Urn storage u = urns[ilk][src]; + Urn storage v = urns[ilk][dst]; + Ilk storage i = ilks[ilk]; + + u.ink = _sub(u.ink, dink); + u.art = _sub(u.art, dart); + v.ink = _add(v.ink, dink); + v.art = _add(v.art, dart); + + uint256 utab = u.art * i.rate; + uint256 vtab = v.art * i.rate; + + // both sides consent + require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed"); + + // both sides safe + require(utab <= u.ink * i.spot, "Vat/not-safe-src"); + require(vtab <= v.ink * i.spot, "Vat/not-safe-dst"); + + // both sides non-dusty + require(either(utab >= i.dust, u.art == 0), "Vat/dust-src"); + require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); + + emit Fork(ilk, src, dst, dink, dart); + } + + // --- CDP Confiscation --- + function grab(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external auth { + Urn storage urn = urns[i][u]; + Ilk storage ilk = ilks[i]; + + urn.ink = _add(urn.ink, dink); + urn.art = _add(urn.art, dart); + ilk.Art = _add(ilk.Art, dart); + + int256 dtab = _int256(ilk.rate) * dart; + + gem[i][v] = _sub(gem[i][v], dink); + sin[w] = _sub(sin[w], dtab); + vice = _sub(vice, dtab); + + emit Grab(i, u, v, w, dink, dart); + } + + // --- Settlement --- + function heal(uint256 rad) external { + address u = msg.sender; + sin[u] = sin[u] - rad; + dai[u] = dai[u] - rad; + vice = vice - rad; + debt = debt - rad; + + emit Heal(msg.sender, rad); + } + + function suck(address u, address v, uint256 rad) external auth { + sin[u] = sin[u] + rad; + dai[v] = dai[v] + rad; + vice = vice + rad; + debt = debt + rad; + + emit Suck(u, v, rad); + } + + // --- Rates --- + function fold(bytes32 i, address u, int256 rate_) external auth { + require(live == 1, "Vat/not-live"); + Ilk storage ilk = ilks[i]; + ilk.rate = _add(ilk.rate, rate_); + int256 rad = _int256(ilk.Art) * rate_; + dai[u] = _add(dai[u], rad); + debt = _add(debt, rad); + + emit Fold(i, u, rate_); + } } diff --git a/src/tests/plans/D3MALMDelegateControllerPlan.t.sol b/src/tests/plans/D3MALMDelegateControllerPlan.t.sol new file mode 100644 index 00000000..a530c858 --- /dev/null +++ b/src/tests/plans/D3MALMDelegateControllerPlan.t.sol @@ -0,0 +1,368 @@ +// 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 "./D3MPlanBase.t.sol"; + +import { D3MALMDelegateControllerPlan } from "../../plans/D3MALMDelegateControllerPlan.sol"; + +contract D3MALMDelegateControllerPlanTest is D3MPlanBaseTest { + + D3MALMDelegateControllerPlan plan; + + address constant ALLOCATOR1 = address(1); + address constant ALLOCATOR2 = address(2); + + address constant ALLOCATORDELEGATE1 = address(4); + + bytes32 constant ILK1 = "ILK1"; + bytes32 constant ILK2 = "ILK2"; + + event AddAllocator(address indexed allocator); + event RemoveAllocator(address indexed allocator); + event SetMaxAllocation(address indexed allocator, bytes32 indexed ilk, uint128 max); + event AddAllocatorDelegate(address indexed allocator, address indexed allocatorDelegate); + event RemoveAllocatorDelegate(address indexed allocator, address indexed allocatorDelegate); + event SetAllocation(address indexed allocator, bytes32 indexed ilk, uint128 previousAllocation, uint128 newAllocation); + + function setUp() public { + vm.expectEmit(true, true, true, true); + emit Rely(address(this)); + plan = new D3MALMDelegateControllerPlan(); + + baseInit(plan, "D3MALMDelegateControllerPlan"); + } + + function test_auth_modifiers() public override { + super.test_auth_modifiers(); + + checkModifier(address(plan), string(abi.encodePacked(contractName, "/not-authorized")), [ + D3MALMDelegateControllerPlan.addAllocator.selector, + D3MALMDelegateControllerPlan.removeAllocator.selector, + D3MALMDelegateControllerPlan.setMaxAllocation.selector + ]); + } + + function test_constructor() public { + assertEq(plan.enabled(), 1); + } + + function test_file() public { + vm.expectRevert(abi.encodePacked(contractName, "/file-unrecognized-param")); + plan.file("an invalid value", 1); + assertEq(plan.enabled(), 1); + vm.expectEmit(true, false, false, true); + emit File("enabled", 0); + plan.file("enabled", 0); + assertEq(plan.enabled(), 0); + vm.expectRevert(abi.encodePacked(contractName, "/invalid-value")); + plan.file("enabled", 2); + plan.deny(address(this)); + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + plan.file("some value", 1); + } + + function test_disable_unauthed() public { + plan.deny(address(this)); + + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + plan.disable(); + } + + function test_addAllocator() public { + assertEq(plan.allocators(TEST_ADDRESS), 0); + vm.expectEmit(true, true, true, true); + emit AddAllocator(TEST_ADDRESS); + plan.addAllocator(TEST_ADDRESS); + assertEq(plan.allocators(TEST_ADDRESS), 1); + } + + function test_removeAllocator() public { + plan.addAllocator(TEST_ADDRESS); + + assertEq(plan.allocators(TEST_ADDRESS), 1); + vm.expectEmit(true, true, true, true); + emit RemoveAllocator(TEST_ADDRESS); + plan.removeAllocator(TEST_ADDRESS); + assertEq(plan.allocators(TEST_ADDRESS), 0); + } + + function test_setMaxAllocation() public { + assertEqAllocation(ALLOCATOR1, ILK1, 0, 0); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + } + + function test_setMaxAllocation_existing_allocator_under_new_limit() public { + vm.expectEmit(true, true, true, true); + emit SetMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + vm.expectEmit(true, true, true, true); + emit SetAllocation(ALLOCATOR1, ILK1, 0, 75 ether); + plan.setAllocation(ALLOCATOR1, ILK1, 75 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 75 ether, 100 ether); + vm.expectEmit(true, true, true, true); + emit SetAllocation(ALLOCATOR1, ILK1, 75 ether, 50 ether); // Note we are testing the set allocation event + plan.setMaxAllocation(ALLOCATOR1, ILK1, 50 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 50 ether, 50 ether); + } + + function _initAllocators() internal { + plan.addAllocator(ALLOCATOR1); + plan.addAllocator(ALLOCATOR2); + } + + function test_addAllocatorDelegate_ward_any() public { + _initAllocators(); + + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 0); + vm.expectEmit(true, true, true, true); + emit AddAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 1); + } + + function test_addAllocatorDelegate_allocator_self() public { + _initAllocators(); + plan.deny(address(this)); + + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 0); + vm.prank(ALLOCATOR1); plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 1); + } + + function test_addAllocatorDelegate_allocator_other() public { + _initAllocators(); + plan.deny(address(this)); + + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + } + + function test_removeAllocatorDelegate_ward_any() public { + _initAllocators(); + plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 1); + vm.expectEmit(true, true, true, true); + emit RemoveAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + plan.removeAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 0); + } + + function test_removeAllocatorDelegate_allocator_self() public { + _initAllocators(); + plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + plan.deny(address(this)); + + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 1); + vm.prank(ALLOCATOR1); plan.removeAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + assertEq(plan.allocatorDelegates(ALLOCATOR1, ALLOCATORDELEGATE1), 0); + } + + function test_removeAllocatorDelegate_allocator_other() public { + _initAllocators(); + plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + plan.deny(address(this)); + + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + plan.removeAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + } + + function test_setAllocation() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.setMaxAllocation(ALLOCATOR2, ILK1, 150 ether); + + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + assertEqAllocation(ALLOCATOR2, ILK1, 0, 150 ether); + assertEq(plan.totalAllocated(ILK1), 0); + assertEq(plan.numAllocations(ILK1), 0); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR1), false); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR2), false); + + plan.setAllocation(ALLOCATOR1, ILK1, 50 ether); + + assertEqAllocation(ALLOCATOR1, ILK1, 50 ether, 100 ether); + assertEqAllocation(ALLOCATOR2, ILK1, 0, 150 ether); + assertEq(plan.totalAllocated(ILK1), 50 ether); + assertEq(plan.numAllocations(ILK1), 1); + assertEq(plan.allocatorAt(ILK1, 0), ALLOCATOR1); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR1), true); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR2), false); + + plan.setAllocation(ALLOCATOR2, ILK1, 75 ether); + + assertEqAllocation(ALLOCATOR1, ILK1, 50 ether, 100 ether); + assertEqAllocation(ALLOCATOR2, ILK1, 75 ether, 150 ether); + assertEq(plan.totalAllocated(ILK1), 125 ether); + assertEq(plan.numAllocations(ILK1), 2); + assertEq(plan.allocatorAt(ILK1, 0), ALLOCATOR1); + assertEq(plan.allocatorAt(ILK1, 1), ALLOCATOR2); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR1), true); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR2), true); + + plan.setAllocation(ALLOCATOR2, ILK1, 25 ether); + + assertEqAllocation(ALLOCATOR1, ILK1, 50 ether, 100 ether); + assertEqAllocation(ALLOCATOR2, ILK1, 25 ether, 150 ether); + assertEq(plan.totalAllocated(ILK1), 75 ether); + assertEq(plan.numAllocations(ILK1), 2); + assertEq(plan.allocatorAt(ILK1, 0), ALLOCATOR1); + assertEq(plan.allocatorAt(ILK1, 1), ALLOCATOR2); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR1), true); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR2), true); + + plan.setAllocation(ALLOCATOR1, ILK1, 0); + + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + assertEqAllocation(ALLOCATOR2, ILK1, 25 ether, 150 ether); + assertEq(plan.totalAllocated(ILK1), 25 ether); + assertEq(plan.numAllocations(ILK1), 1); + assertEq(plan.allocatorAt(ILK1, 0), ALLOCATOR2); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR1), false); + assertEq(plan.hasAllocator(ILK1, ALLOCATOR2), true); + } + + function test_setAllocation_ward_any() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + vm.expectEmit(true, true, true, true); + emit SetAllocation(ALLOCATOR1, ILK1, 0, 75 ether); + plan.setAllocation(ALLOCATOR1, ILK1, 75 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 75 ether, 100 ether); + } + + function test_setAllocation_allocator_self() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.deny(address(this)); + + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + vm.prank(ALLOCATOR1); plan.setAllocation(ALLOCATOR1, ILK1, 75 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 75 ether, 100 ether); + } + + function test_setAllocation_allocator_other() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.deny(address(this)); + + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + vm.prank(ALLOCATOR2); plan.setAllocation(ALLOCATOR1, ILK1, 75 ether); + } + + function test_setAllocation_allocator_delegate_approved() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.addAllocatorDelegate(ALLOCATOR1, ALLOCATORDELEGATE1); + plan.deny(address(this)); + + assertEqAllocation(ALLOCATOR1, ILK1, 0, 100 ether); + vm.prank(ALLOCATORDELEGATE1); plan.setAllocation(ALLOCATOR1, ILK1, 75 ether); + assertEqAllocation(ALLOCATOR1, ILK1, 75 ether, 100 ether); + } + + function test_setAllocation_allocator_delegate_not_approved() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.deny(address(this)); + + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + vm.prank(ALLOCATORDELEGATE1); plan.setAllocation(ALLOCATOR1, ILK1, 75 ether); + } + + function test_setAllocation_above_max() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + + vm.expectRevert(abi.encodePacked(contractName, "/amount-exceeds-max")); + plan.setAllocation(ALLOCATOR1, ILK1, 100 ether + 1); + } + + function test_getTargetAssets() public { + _initAllocators(); + plan.setMaxAllocation(ALLOCATOR1, ILK1, 100 ether); + plan.setMaxAllocation(ALLOCATOR2, ILK1, 50 ether); + plan.setMaxAllocation(ALLOCATOR1, ILK2, 150 ether); + plan.setMaxAllocation(ALLOCATOR2, ILK2, 200 ether); + + assertEq(plan.getTargetAssets(ILK1, 0), 0); + assertEq(plan.getTargetAssets(ILK2, 0), 0); + + plan.setAllocation(ALLOCATOR1, ILK1, 100 ether); + + assertEq(plan.getTargetAssets(ILK1, 0), 100 ether); + assertEq(plan.getTargetAssets(ILK2, 0), 0); + + plan.setAllocation(ALLOCATOR2, ILK1, 25 ether); + + assertEq(plan.getTargetAssets(ILK1, 0), 125 ether); + assertEq(plan.getTargetAssets(ILK2, 0), 0); + + plan.setAllocation(ALLOCATOR2, ILK2, 125 ether); + + assertEq(plan.getTargetAssets(ILK1, 0), 125 ether); + assertEq(plan.getTargetAssets(ILK2, 0), 125 ether); + + plan.setAllocation(ALLOCATOR1, ILK2, 75 ether); + + assertEq(plan.getTargetAssets(ILK1, 0), 125 ether); + assertEq(plan.getTargetAssets(ILK2, 0), 200 ether); + + plan.setMaxAllocation(ALLOCATOR1, ILK1, 25 ether); + + assertEq(plan.getTargetAssets(ILK1, 0), 50 ether); + assertEq(plan.getTargetAssets(ILK2, 0), 200 ether); + + plan.disable(); + + assertEq(plan.getTargetAssets(ILK1, 0), 0); + assertEq(plan.getTargetAssets(ILK2, 0), 0); + } + + function test_active_enabled_set() public { + assertEq(plan.enabled(), 1); + assertTrue(plan.active()); + plan.file("enabled", 0); + assertEq(plan.enabled(), 0); + assertTrue(!plan.active()); + plan.file("enabled", 1); + assertEq(plan.enabled(), 1); + assertTrue(plan.active()); + } + + function test_disable() public { + assertEq(plan.enabled(), 1); + assertTrue(plan.active()); + vm.expectEmit(true, true, true, true); + emit Disable(); + plan.disable(); + assertTrue(!plan.active()); + assertEq(plan.enabled(), 0); + } + + function assertEqAllocation(address allocator, bytes32 ilk, uint256 current, uint256 max) internal { + (uint128 _current, uint128 _max) = plan.allocations(allocator, ilk); + assertEq(_current, current, "current does not match"); + assertEq(_max, max, "max does not match"); + } + +} diff --git a/src/tests/plans/D3MAaveTypeBufferPlan.t.sol b/src/tests/plans/D3MAaveTypeBufferPlan.t.sol index 65542aa0..79a31bc7 100644 --- a/src/tests/plans/D3MAaveTypeBufferPlan.t.sol +++ b/src/tests/plans/D3MAaveTypeBufferPlan.t.sol @@ -54,9 +54,7 @@ contract D3MAaveTypeBufferPlanTest is D3MPlanBaseTest { DaiMock dai; D3MAaveTypeBufferPlan plan; - - event Disable(); - + function setUp() public { dai = new DaiMock(); adai = new ADaiMock(address(dai)); @@ -101,59 +99,59 @@ contract D3MAaveTypeBufferPlanTest is D3MPlanBaseTest { function test_liquidity_less_than_buffer() public { dai.setLiquidity(40 ether); plan.file("buffer", 100 ether); - assertEq(plan.getTargetAssets(0), 60 ether); + assertEq(plan.getTargetAssets("", 0), 60 ether); } function test_liquidity_equals_buffer() public { dai.setLiquidity(100 ether); plan.file("buffer", 100 ether); - assertEq(plan.getTargetAssets(0), 0); + assertEq(plan.getTargetAssets("", 0), 0); } function test_liquidity_greater_than_buffer_partial_unwind() public { dai.setLiquidity(100 ether); plan.file("buffer", 40 ether); - assertEq(plan.getTargetAssets(200 ether), 140 ether); + assertEq(plan.getTargetAssets("", 200 ether), 140 ether); } function test_liquidity_greater_than_buffer_full_unwind() public { dai.setLiquidity(100 ether); plan.file("buffer", 40 ether); - assertEq(plan.getTargetAssets(40 ether), 0); + assertEq(plan.getTargetAssets("", 40 ether), 0); } function test_buffer_equals_zero() public { dai.setLiquidity(100 ether); plan.file("buffer", 0); - assertEq(plan.getTargetAssets(10000 ether), 0); + assertEq(plan.getTargetAssets("", 10000 ether), 0); } function test_increase_liquidity() public { plan.file("buffer", 100 ether); - assertEq(plan.getTargetAssets(20 ether), 120 ether); + assertEq(plan.getTargetAssets("", 20 ether), 120 ether); } function test_decrease_liquidity_sole_provider() public { plan.file("buffer", 100 ether); - assertEq(plan.getTargetAssets(0), 100 ether); + assertEq(plan.getTargetAssets("", 0), 100 ether); dai.setLiquidity(100 ether); // Simulate adding liquidity dai.setLiquidity(80 ether); // Simulate someone borrowed 20 DAI - assertEq(plan.getTargetAssets(100 ether), 120 ether); + assertEq(plan.getTargetAssets("", 100 ether), 120 ether); dai.setLiquidity(100 ether); // Topped back up to 100 DAI dai.setLiquidity(120 ether); // User returned the 20 DAI - assertEq(plan.getTargetAssets(120 ether), 100 ether); + assertEq(plan.getTargetAssets("", 120 ether), 100 ether); dai.setLiquidity(100 ether); // Liquidity goes back to 100 DAI } function test_decrease_liquidity_multiple_providers() public { plan.file("buffer", 100 ether); - assertEq(plan.getTargetAssets(0), 100 ether); + assertEq(plan.getTargetAssets("", 0), 100 ether); dai.setLiquidity(100 ether); // Simulate adding liquidity dai.setLiquidity(150 ether); // Someone else adds 50 DAI - assertEq(plan.getTargetAssets(100 ether), 50 ether); + assertEq(plan.getTargetAssets("", 100 ether), 50 ether); dai.setLiquidity(100 ether); // Simulate removing liquidity dai.setLiquidity(300 ether); // Someone else adds 200 DAI - assertEq(plan.getTargetAssets(50 ether), 0); // Plan will remove all liquidity + assertEq(plan.getTargetAssets("", 50 ether), 0); // Plan will remove all liquidity } function test_active_buffer_set() public { diff --git a/src/tests/plans/D3MAaveV2TypeRateTargetPlan.t.sol b/src/tests/plans/D3MAaveV2TypeRateTargetPlan.t.sol index 1588e9f1..b0b1edc7 100644 --- a/src/tests/plans/D3MAaveV2TypeRateTargetPlan.t.sol +++ b/src/tests/plans/D3MAaveV2TypeRateTargetPlan.t.sol @@ -161,12 +161,12 @@ contract D3MAaveV2TypeRateTargetPlanTest is D3MPlanBaseTest { function test_set_bar_too_high_unwinds() public { plan.file("bar", interestStrategy.getMaxVariableBorrowRate() + 1); - assertEq(plan.getTargetAssets(1), 0); + assertEq(plan.getTargetAssets("", 1), 0); } function test_set_bar_too_low_unwinds() public { plan.file("bar", interestStrategy.baseVariableBorrowRate()); - assertEq(plan.getTargetAssets(1), 0); + assertEq(plan.getTargetAssets("", 1), 0); } function test_interest_rate_calc() public { @@ -188,18 +188,18 @@ contract D3MAaveV2TypeRateTargetPlanTest is D3MPlanBaseTest { function test_getTargetAssets() public { plan.file("bar", interestStrategy.baseVariableBorrowRate() + 2 * RAY / 100); - uint256 initialTargetAssets = plan.getTargetAssets(0); + uint256 initialTargetAssets = plan.getTargetAssets("", 0); // Reduce target rate (increase needed number of target Assets) plan.file("bar", interestStrategy.baseVariableBorrowRate() + 1 * RAY / 100); - uint256 newTargetAssets = plan.getTargetAssets(0); + uint256 newTargetAssets = plan.getTargetAssets("", 0); assertGt(newTargetAssets, initialTargetAssets); } function test_getTargetAssets_bar_zero() public { assertEq(plan.bar(), 0); - assertEq(plan.getTargetAssets(0), 0); + assertEq(plan.getTargetAssets("", 0), 0); } function test_interestStrategy_changed_not_active() public { diff --git a/src/tests/plans/D3MCompoundV2TypeRateTargetPlan.t.sol b/src/tests/plans/D3MCompoundV2TypeRateTargetPlan.t.sol index c730a1c4..00fef3cb 100644 --- a/src/tests/plans/D3MCompoundV2TypeRateTargetPlan.t.sol +++ b/src/tests/plans/D3MCompoundV2TypeRateTargetPlan.t.sol @@ -302,19 +302,19 @@ contract D3MCompoundV2TypeRateTargetPlanTest is D3MPlanBaseTest { plan.file("barb", initialRatePerBlock - (1 * WAD / 1000) / model.blocksPerYear()); // minus 0.1% from current yearly rate - uint256 initialTargetAssets = plan.getTargetAssets(0); + uint256 initialTargetAssets = plan.getTargetAssets("", 0); assertGt(initialTargetAssets, 0); // Reduce target rate (increase needed number of target Assets) plan.file("barb", initialRatePerBlock - (2 * WAD / 1000) / model.blocksPerYear()); // minus 0.2% from current yearly rate - uint256 newTargetAssets = plan.getTargetAssets(0); + uint256 newTargetAssets = plan.getTargetAssets("", 0); assertGt(newTargetAssets, initialTargetAssets); } function test_getTargetAssets_barb_zero() public { assertEq(plan.barb(), 0); - assertEq(plan.getTargetAssets(0), 0); + assertEq(plan.getTargetAssets("", 0), 0); } function test_getTargetAssets_current_rate() public { @@ -322,7 +322,7 @@ contract D3MCompoundV2TypeRateTargetPlanTest is D3MPlanBaseTest { uint256 borrowRatePerBlock = cDai.borrowRatePerBlock(); plan.file("barb", borrowRatePerBlock); - uint256 targetAssets = plan.getTargetAssets(0); + uint256 targetAssets = plan.getTargetAssets("", 0); assertEqAbsolute(targetAssets, 0, WAD); } diff --git a/src/tests/plans/D3MPlanBase.t.sol b/src/tests/plans/D3MPlanBase.t.sol index 13c2b39e..11bec10e 100644 --- a/src/tests/plans/D3MPlanBase.t.sol +++ b/src/tests/plans/D3MPlanBase.t.sol @@ -27,6 +27,8 @@ abstract contract D3MPlanBaseTest is DssTest { ID3MPlan private plan; + event Disable(); + function baseInit(ID3MPlan _plan, string memory _contractName) internal { plan = _plan; contractName = _contractName; diff --git a/src/tests/pools/D3MAaveV2TypePool.t.sol b/src/tests/pools/D3MAaveV2TypePool.t.sol index 490f0f4c..13c9ea8f 100644 --- a/src/tests/pools/D3MAaveV2TypePool.t.sol +++ b/src/tests/pools/D3MAaveV2TypePool.t.sol @@ -159,7 +159,7 @@ contract D3MAaveV2TypePoolTest is D3MPoolBaseTest { aavePool = LendingPoolLike(address(new LendingPoolMock(address(adai)))); adai.rely(address(aavePool)); - setPoolContract(pool = new D3MAaveV2TypePool(ILK, address(hub), address(dai), address(aavePool))); + setPoolContract(address(pool = new D3MAaveV2TypePool(ILK, address(hub), address(dai), address(aavePool)))); } function test_sets_dai_value() public { diff --git a/src/tests/pools/D3MAaveV3NoSupplyCapTypePool.t.sol b/src/tests/pools/D3MAaveV3NoSupplyCapTypePool.t.sol index 603e73ee..c71f7691 100644 --- a/src/tests/pools/D3MAaveV3NoSupplyCapTypePool.t.sol +++ b/src/tests/pools/D3MAaveV3NoSupplyCapTypePool.t.sol @@ -101,6 +101,8 @@ contract FakeLendingPool { } address public adai; + address public variableDebtToken; + address public stableDebtToken; address public dai; struct DepositCall { @@ -118,8 +120,10 @@ contract FakeLendingPool { } WithdrawCall public lastWithdraw; - constructor(address adai_, address dai_) { + constructor(address adai_, address variableDebtToken_, address stableDebtToken_, address dai_) { adai = adai_; + variableDebtToken = variableDebtToken_; + stableDebtToken = stableDebtToken_; dai = dai_; } @@ -127,8 +131,8 @@ contract FakeLendingPool { ReserveData memory result ) { result.aTokenAddress = adai; - result.stableDebtTokenAddress = address(2); - result.variableDebtTokenAddress = address(3); + result.stableDebtTokenAddress = stableDebtToken; + result.variableDebtTokenAddress = variableDebtToken; result.interestRateStrategyAddress = address(4); } @@ -139,6 +143,7 @@ contract FakeLendingPool { forWhom, code ); + TokenMock(dai).transferFrom(msg.sender, address(adai), amt); TokenMock(adai).mint(forWhom, amt); } @@ -148,7 +153,7 @@ contract FakeLendingPool { amt, dst ); - TokenMock(asset).transfer(dst, amt); + TokenMock(asset).transferFrom(address(adai), dst, amt); } function getReserveNormalizedIncome(address asset) external pure returns (uint256) { @@ -160,6 +165,8 @@ contract FakeLendingPool { contract D3MAaveV3NoSupplyCapTypePoolTest is D3MPoolBaseTest { AToken adai; + TokenMock variableDebtToken; + TokenMock stableDebtToken; FakeLendingPool aavePool; D3MAaveV3NoSupplyCapTypePool pool; @@ -169,10 +176,13 @@ contract D3MAaveV3NoSupplyCapTypePoolTest is D3MPoolBaseTest { adai = new AToken(18); adai.mint(address(this), 1_000_000 ether); - aavePool = new FakeLendingPool(address(adai), address(dai)); + variableDebtToken = new TokenMock(18); + stableDebtToken = new TokenMock(18); + aavePool = new FakeLendingPool(address(adai), address(variableDebtToken), address(stableDebtToken), address(dai)); adai.rely(address(aavePool)); + vm.prank(address(adai)); dai.approve(address(aavePool), type(uint256).max); - setPoolContract(pool = new D3MAaveV3NoSupplyCapTypePool("", address(hub), address(dai), address(aavePool))); + setPoolContract(address(pool = new D3MAaveV3NoSupplyCapTypePool("", address(hub), address(dai), address(aavePool)))); } function test_sets_dai_value() public { @@ -198,7 +208,7 @@ contract D3MAaveV3NoSupplyCapTypePoolTest is D3MPoolBaseTest { } function test_deposit_calls_lending_pool_deposit() public { - TokenMock(address(adai)).rely(address(aavePool)); + dai.mint(address(pool), 1); vm.prank(address(hub)); pool.deposit(1); (address asset, uint256 amt, address dst, uint256 code) = FakeLendingPool(address(aavePool)).lastDeposit(); assertEq(asset, address(dai)); @@ -209,7 +219,7 @@ contract D3MAaveV3NoSupplyCapTypePoolTest is D3MPoolBaseTest { function test_withdraw_calls_lending_pool_withdraw() public { // make sure we have Dai to withdraw - TokenMock(address(dai)).mint(address(aavePool), 1); + TokenMock(address(dai)).mint(address(adai), 1); vm.prank(address(hub)); pool.withdraw(1); (address asset, uint256 amt, address dst) = FakeLendingPool(address(aavePool)).lastWithdraw(); @@ -220,7 +230,7 @@ contract D3MAaveV3NoSupplyCapTypePoolTest is D3MPoolBaseTest { function test_withdraw_calls_lending_pool_withdraw_vat_caged() public { // make sure we have Dai to withdraw - TokenMock(address(dai)).mint(address(aavePool), 1); + TokenMock(address(dai)).mint(address(adai), 1); vat.cage(); vm.prank(address(hub)); pool.withdraw(1); @@ -312,4 +322,28 @@ contract D3MAaveV3NoSupplyCapTypePoolTest is D3MPoolBaseTest { function test_maxDeposit_returns_max_uint() public { assertEq(pool.maxDeposit(), type(uint256).max); } + + function test_liquidityAvailable() public { + dai.mint(address(adai), 100 ether); + assertEq(dai.balanceOf(address(adai)), 100 ether); + assertEq(pool.liquidityAvailable(), 100 ether); + } + + function test_idleLiquidity() public { + dai.mint(address(pool), 100 ether); + vm.prank(address(hub)); pool.deposit(100 ether); + assertEq(pool.idleLiquidity(), 100 ether); + + // Simulate a variable borrow + aavePool.withdraw(address(dai), 50 ether, address(this)); + variableDebtToken.mint(address(this), 50 ether); + + assertEq(pool.idleLiquidity(), 50 ether); + + // Simulate a stable borrow + aavePool.withdraw(address(dai), 25 ether, address(this)); + stableDebtToken.mint(address(this), 25 ether); + + assertEq(pool.idleLiquidity(), 25 ether); + } } diff --git a/src/tests/pools/D3MCompoundV2TypePool.t.sol b/src/tests/pools/D3MCompoundV2TypePool.t.sol index 5c784a1a..35a2f098 100644 --- a/src/tests/pools/D3MCompoundV2TypePool.t.sol +++ b/src/tests/pools/D3MCompoundV2TypePool.t.sol @@ -77,7 +77,7 @@ contract D3MCompoundV2TypePoolTest is D3MPoolBaseTest { comp = GemAbstract(0xc00e94Cb662C3520282E6f5717214004A7f26888); lens = LensLike(0xdCbDb7306c6Ff46f77B349188dC18cEd9DF30299); - setPoolContract(pool = new D3MCompoundV2TypePool(ILK, address(hub), address(cDai))); + setPoolContract(address(pool = new D3MCompoundV2TypePool(ILK, address(hub), address(cDai)))); // allocate some dai for the pool GodMode.setBalance(address(dai), address(pool), 100 * WAD); diff --git a/src/tests/pools/D3MGatedOffchainSwapPool.t.sol b/src/tests/pools/D3MGatedOffchainSwapPool.t.sol new file mode 100644 index 00000000..1a94019d --- /dev/null +++ b/src/tests/pools/D3MGatedOffchainSwapPool.t.sol @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: © 2023 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 "./D3MGatedSwapPool.t.sol"; + +import { D3MGatedOffchainSwapPool } from "../../pools/D3MGatedOffchainSwapPool.sol"; + +contract D3MGatedOffchainSwapPoolTest is D3MGatedSwapPoolTest { + + using stdStorage for StdStorage; + + D3MGatedOffchainSwapPool private pool; + + event AddOperator(address indexed operator); + event RemoveOperator(address indexed operator); + + function setUp() public override { + baseInit("D3MSwapPool"); + + pool = new D3MGatedOffchainSwapPool(ILK, address(hub), address(dai), address(gem)); + + setPoolContract(address(pool)); + } + + function _ensureRatio(uint256 ratioInBps, uint256 totalBalance, bool isSellingGem) internal override virtual { + super._ensureRatio(ratioInBps, totalBalance, isSellingGem); + plan.setTargetAssets(isSellingGem ? totalBalance : 0); + } + + function test_operator_authModifier() public { + pool.deny(address(this)); + + checkModifier(address(pool), string(abi.encodePacked(contractName, "/not-authorized")), [ + D3MGatedOffchainSwapPool.addOperator.selector, + D3MGatedOffchainSwapPool.removeOperator.selector + ]); + } + + function test_file_uint() public { + checkFileUint(address(pool), contractName, ["gemsOutstanding"]); + + vat.cage(); + vm.expectRevert(abi.encodePacked(contractName, "/no-file-during-shutdown")); + pool.file("some value", 1); + } + + function test_addOperator() public { + assertEq(pool.operators(TEST_ADDRESS), 0); + vm.expectEmit(true, true, true, true); + emit AddOperator(TEST_ADDRESS); + pool.addOperator(TEST_ADDRESS); + assertEq(pool.operators(TEST_ADDRESS), 1); + + vat.cage(); + vm.expectRevert(abi.encodePacked(contractName, "/no-addOperator-during-shutdown")); + pool.addOperator(address(1)); + } + + function test_removeOperator() public { + pool.addOperator(TEST_ADDRESS); + + assertEq(pool.operators(TEST_ADDRESS), 1); + vm.expectEmit(true, true, true, true); + emit RemoveOperator(TEST_ADDRESS); + pool.removeOperator(TEST_ADDRESS); + assertEq(pool.operators(TEST_ADDRESS), 0); + + vat.cage(); + vm.expectRevert(abi.encodePacked(contractName, "/no-removeOperator-during-shutdown")); + pool.removeOperator(address(1)); + } + + function test_assetBalance() public override { + dai.transfer(address(pool), 50 ether); + assertEq(pool.assetBalance(), 50 ether); + gem.transfer(address(pool), 25 * 1e6); + assertEq(pool.assetBalance(), 100 ether); + stdstore.target(address(pool)).sig("gemsOutstanding()").checked_write(bytes32(uint256(10 * 1e6))); + assertEq(pool.assetBalance(), 120 ether); + } + + function _initPushPull() public { + pool.addOperator(address(this)); + plan.setTargetAssets(50 ether); + gem.transfer(address(pool), 25 * 1e6); + gem.burn(address(this), gem.balanceOf(address(this))); // Burn the rest for easy accounting + } + + function test_pull() public { + _initPushPull(); + + assertEq(gem.balanceOf(address(pool)), 25 * 1e6); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(pool.gemsOutstanding(), 0); + assertEq(pool.assetBalance(), 50 ether); + pool.pull(address(this), 25 * 1e6); + assertEq(gem.balanceOf(address(pool)), 0 * 1e6); + assertEq(gem.balanceOf(address(this)), 25 * 1e6); + assertEq(pool.gemsOutstanding(), 25 * 1e6); + assertEq(pool.assetBalance(), 50 ether); + } + + function test_pull_partial() public { + _initPushPull(); + + assertEq(gem.balanceOf(address(pool)), 25 * 1e6); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(pool.gemsOutstanding(), 0); + pool.pull(address(this), 10 * 1e6); + assertEq(gem.balanceOf(address(pool)), 15 * 1e6); + assertEq(gem.balanceOf(address(this)), 10 * 1e6); + assertEq(pool.gemsOutstanding(), 10 * 1e6); + } + + function test_pull_amount_exceeds_pending() public { + _initPushPull(); + + plan.setTargetAssets(0); + vm.expectRevert(abi.encodePacked(contractName, "/amount-exceeds-pending")); + pool.pull(address(this), 10 * 1e6); + } + + function test_push_principal() public { + _initPushPull(); + + pool.pull(address(this), 25 * 1e6); + assertEq(gem.balanceOf(address(pool)), 0); + assertEq(gem.balanceOf(address(this)), 25 * 1e6); + assertEq(pool.gemsOutstanding(), 25 * 1e6); + assertEq(pool.assetBalance(), 50 ether); + gem.approve(address(pool), 25 * 1e6); + pool.push(25 * 1e6); + assertEq(gem.balanceOf(address(pool)), 25 * 1e6); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(pool.gemsOutstanding(), 0); + assertEq(pool.assetBalance(), 50 ether); + } + + function test_push_principal_plus_interest() public { + _initPushPull(); + + pool.pull(address(this), 25 * 1e6); + assertEq(gem.balanceOf(address(pool)), 0); + assertEq(gem.balanceOf(address(this)), 25 * 1e6); + assertEq(pool.gemsOutstanding(), 25 * 1e6); + assertEq(pool.assetBalance(), 50 ether); + gem.mint(address(pool), 10 * 1e6); + gem.approve(address(pool), 25 * 1e6); + pool.push(25 * 1e6); + assertEq(gem.balanceOf(address(pool)), 35 * 1e6); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(pool.gemsOutstanding(), 0); + assertEq(pool.assetBalance(), 70 ether); + } + + function test_pendingDeposits_target_above_gems() public { + _initPushPull(); + + assertEq(pool.pendingDeposits(), 25 * 1e6); + } + + function test_pendingDeposits_target_below_gems() public { + _initPushPull(); + + plan.setTargetAssets(40 ether); + assertEq(pool.pendingDeposits(), 20 * 1e6); + } + + function test_pendingDeposits_outstanding_target_above_gems() public { + _initPushPull(); + + pool.pull(address(this), 10 * 1e6); + assertEq(pool.pendingDeposits(), 15 * 1e6); + } + + function test_pendingDeposits_outstanding_target_below_gems() public { + _initPushPull(); + + plan.setTargetAssets(40 ether); + pool.pull(address(this), 10 * 1e6); + assertEq(pool.pendingDeposits(), 10 * 1e6); + } + + function test_pendingDeposits_too_much_outstanding() public { + _initPushPull(); + + pool.pull(address(this), 15 * 1e6); + assertEq(pool.pendingDeposits(), 10 * 1e6); + plan.setTargetAssets(30 ether); + assertEq(pool.pendingDeposits(), 0); + } + + function test_pendingDeposits_target_zero() public { + _initPushPull(); + + plan.setTargetAssets(0); + assertEq(pool.pendingDeposits(), 0); + } + + function test_pendingWithdrawals_gems_outstanding_above_target() public { + _initPushPull(); + + pool.pull(address(this), 25 * 1e6); + assertEq(pool.pendingWithdrawals(), 0); + } + + function test_pendingWithdrawals_gems_outstanding_below_target() public { + _initPushPull(); + + pool.pull(address(this), 25 * 1e6); + plan.setTargetAssets(40 ether); + assertEq(pool.pendingWithdrawals(), 5 * 1e6); + } + + function test_pendingWithdrawals_none_outstanding() public { + _initPushPull(); + + assertEq(pool.pendingWithdrawals(), 0); + } + +} diff --git a/src/tests/pools/D3MGatedSwapPool.t.sol b/src/tests/pools/D3MGatedSwapPool.t.sol new file mode 100644 index 00000000..963ec98c --- /dev/null +++ b/src/tests/pools/D3MGatedSwapPool.t.sol @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: © 2023 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 "./D3MSwapPool.t.sol"; + +import { D3MGatedSwapPool } from "../../pools/D3MGatedSwapPool.sol"; + +contract PlanMock { + + uint256 public targetAssets; + + function setTargetAssets(uint256 _targetAssets) external { + targetAssets = _targetAssets; + } + + function getTargetAssets(bytes32, uint256) external view returns (uint256) { + return targetAssets; + } + +} + +contract D3MGatedSwapPoolTest is D3MSwapPoolTest { + + PlanMock internal plan; + + D3MGatedSwapPool private pool; + + event File(bytes32 indexed what, uint128 tin, uint128 tout); + + function setUp() public virtual { + baseInit("D3MSwapPool"); + + plan = new PlanMock(); + hub.setPlan(address(plan)); + + pool = new D3MGatedSwapPool(ILK, address(hub), address(dai), address(gem)); + + // Fees set to tin=-5bps, tout=10bps + + setPoolContract(address(pool)); + } + + function setPoolContract(address _pool) internal virtual override { + super.setPoolContract(_pool); + pool = D3MGatedSwapPool(_pool); + + pool.file("fees", 1.0005 ether, 0.9990 ether); + plan = new PlanMock(); + hub.setPlan(address(plan)); + } + + function _ensureRatio(uint256 ratioInBps, uint256 totalBalance, bool isSellingGem) internal override virtual { + super._ensureRatio(ratioInBps, totalBalance, isSellingGem); + plan.setTargetAssets(isSellingGem ? totalBalance : 0); + } + + function test_authModifier() public { + pool.deny(address(this)); + + checkModifier(address(pool), string(abi.encodePacked(contractName, "/not-authorized")), [ + bytes4(keccak256("file(bytes32,uint128,uint128)")) + ]); + } + + function test_file_fees() public { + vm.expectRevert(abi.encodePacked(contractName, "/file-unrecognized-param")); + pool.file("an invalid value", 1, 2); + + vm.expectEmit(true, true, true, true); + emit File("fees", 1, 2); + pool.file("fees", 1, 2); + + assertEq(pool.tin(), 1); + assertEq(pool.tout(), 2); + + vat.cage(); + vm.expectRevert(abi.encodePacked(contractName, "/no-file-during-shutdown")); + pool.file("some value", 1, 2); + } + + function test_previewSwapGemForDai() public { + _ensureRatio(5000, 100 ether, true); + + assertEq(pool.previewSwapGemForDai(10 * 1e6), 20.01 ether); + } + + function test_previewSwapGemForDai_not_accepting_gems() public { + _ensureRatio(5000, 100 ether, false); + + vm.expectRevert(abi.encodePacked(contractName, "/not-accepting-gems")); + pool.previewSwapGemForDai(10 * 1e6); + } + + function test_previewSwapDaiForGem() public { + _ensureRatio(5000, 100 ether, false); + + assertEq(pool.previewSwapDaiForGem(20 ether), 9.99 * 1e6); + } + + function test_previewSwapDaiForGem_not_accepting_gems() public { + _ensureRatio(5000, 100 ether, true); + + vm.expectRevert(abi.encodePacked(contractName, "/not-accepting-dai")); + pool.previewSwapDaiForGem(20 ether); + } + +} diff --git a/src/tests/pools/D3MLinearFeeSwapPool.t.sol b/src/tests/pools/D3MLinearFeeSwapPool.t.sol new file mode 100644 index 00000000..b9ed3f8b --- /dev/null +++ b/src/tests/pools/D3MLinearFeeSwapPool.t.sol @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: © 2023 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 "./D3MSwapPool.t.sol"; + +import { D3MLinearFeeSwapPool } from "../../pools/D3MLinearFeeSwapPool.sol"; + +contract D3MLinearFeeSwapPoolTest is D3MSwapPoolTest { + + D3MLinearFeeSwapPool internal pool; + + event File(bytes32 indexed what, uint64 tin, uint64 tout); + + function setUp() public { + baseInit("D3MSwapPool"); + + pool = new D3MLinearFeeSwapPool(ILK, address(hub), address(dai), address(gem)); + + // 0% usage has tin=5bps (negative fee), tout=20bps (positive fee) + pool.file("fees1", 1.0005 ether, 0.9980 ether); + // 100% usage has tin=10bps (negative fee), tout=8bps (negative fee) + pool.file("fees2", 0.9990 ether, 1.0008 ether); + + setPoolContract(address(pool)); + } + + function test_file_fees() public { + vm.expectRevert(abi.encodePacked(contractName, "/file-unrecognized-param")); + pool.file("an invalid value", 1, 2); + + vm.expectEmit(true, true, true, true); + emit File("fees1", 1, 2); + pool.file("fees1", 1, 2); + + assertEq(pool.tin1(), 1); + assertEq(pool.tout1(), 2); + + vm.expectEmit(true, true, true, true); + emit File("fees2", 3, 4); + pool.file("fees2", 3, 4); + + assertEq(pool.tin2(), 3); + assertEq(pool.tout2(), 4); + + vat.cage(); + vm.expectRevert(abi.encodePacked(contractName, "/no-file-during-shutdown")); + pool.file("some value", 1, 2); + + pool.deny(address(this)); + vm.expectRevert(abi.encodePacked(contractName, "/not-authorized")); + pool.file("some value", 1, 2); + } + + function test_file_invalid_fees() public { + vm.expectRevert(abi.encodePacked(contractName, "/invalid-fees")); + pool.file("fees1", uint64(WAD + 1), uint64(WAD)); + } + + function test_previewSwapGemForDai_tin1_edge() public { + dai.transfer(address(pool), 100 ether); + // 10% between 1.0005 and 0.9990 (average of 0% and 20% endpoints) + assertEq(pool.previewSwapGemForDai(10 * 1e6), 20.007 ether); + } + + function test_previewSwapGemForDai_tin2_edge() public { + dai.transfer(address(pool), 20 ether); + gem.transfer(address(pool), 40 * 1e6); + // 90% between 1.0005 and 0.9990 (average of 80% and 100% endpoints) + assertEq(pool.previewSwapGemForDai(10 * 1e6), 19.983 ether); + } + + function test_previewSwapGemForDai_middle() public { + dai.transfer(address(pool), 80 ether); + gem.transfer(address(pool), 10 * 1e6); + // 30% between 1.0005 and 0.9990 (average of 20% and 40% endpoints) + assertEq(pool.previewSwapGemForDai(10 * 1e6), 20.001 ether); + } + + function test_previewSwapGemForDai_take_all_dai() public { + dai.transfer(address(pool), 100 ether); + // 50% between 1.0005 and 0.9990 (average of 0% and 100% endpoints) + assertEq(pool.previewSwapGemForDai(50 * 1e6), 99.975 ether); + } + + function test_previewSwapGemForDai_cant_take_all_dai_plus_one() public { + dai.transfer(address(pool), 100 ether); + // Even though the fee makes it so you could take more in theory + // the linear function ends at 100% of the pre-fee amount + vm.expectRevert(abi.encodePacked(contractName, "/insufficient-dai-in-pool")); + pool.previewSwapGemForDai(50 * 1e6 + 1); + } + + function test_previewSwapGemForDai_empty() public { + vm.expectRevert(abi.encodePacked(contractName, "/insufficient-dai-in-pool")); + pool.previewSwapGemForDai(10 * 1e6); + } + + function test_previewSwapGemForDai_zero() public { + dai.transfer(address(pool), 100 ether); + assertEq(pool.previewSwapGemForDai(0), 0); + } + + function test_previewSwapDaiForGem_tout2_edge() public { + gem.transfer(address(pool), 50 * 1e6); + // 10% between 1.0008 and 0.9980 (average of 0% and 20% endpoints) + assertEq(pool.previewSwapDaiForGem(20 ether), 10.0052 * 1e6); + } + + function test_previewSwapDaiForGem_tout1_edge() public { + dai.transfer(address(pool), 80 ether); + gem.transfer(address(pool), 10 * 1e6); + // 90% between 1.0008 and 0.9980 (average of 80% and 100% endpoints) + assertEq(pool.previewSwapDaiForGem(20 ether), 9.9828 * 1e6); + } + + function test_previewSwapDaiForGem_middle() public { + dai.transfer(address(pool), 20 ether); + gem.transfer(address(pool), 40 * 1e6); + // 30% between 1.0008 and 0.9980 (average of 20% and 40% endpoints) + assertEq(pool.previewSwapDaiForGem(20 ether), 9.9996 * 1e6); + } + + function test_previewSwapDaiForGem_take_all_gems() public { + gem.transfer(address(pool), 50 * 1e6); + // 50% between 1.0008 and 0.9980 (average of 0% and 100% endpoints) + assertEq(pool.previewSwapDaiForGem(100 ether), 49.97 * 1e6); + } + + function test_previewSwapDaiForGem_cant_take_all_gems_plus_one() public { + gem.transfer(address(pool), 50 * 1e6); + // Even though the fee makes it so you could take more in theory + // the linear function ends at 100% of the pre-fee amount + vm.expectRevert(abi.encodePacked(contractName, "/insufficient-gems-in-pool")); + pool.previewSwapDaiForGem(100 ether + 1); + } + + function test_previewSwapDaiForGem_empty() public { + vm.expectRevert(abi.encodePacked(contractName, "/insufficient-gems-in-pool")); + pool.previewSwapDaiForGem(20 ether); + } + + function test_previewSwapDaiForGem_zero() public { + gem.transfer(address(pool), 50 * 1e6); + assertEq(pool.previewSwapDaiForGem(0), 0); + } + +} diff --git a/src/tests/pools/D3MPoolBase.t.sol b/src/tests/pools/D3MPoolBase.t.sol index af5ff6e2..e299fc67 100644 --- a/src/tests/pools/D3MPoolBase.t.sol +++ b/src/tests/pools/D3MPoolBase.t.sol @@ -41,7 +41,7 @@ abstract contract D3MPoolBaseTest is DssTest { ID3MPool private pool; // Override with stronger type in child contract - function baseInit(string memory _contractName) internal { + function baseInit(string memory _contractName) internal virtual { vat = new VatMock(); end = new EndMock(); hub = new HubMock(address(vat), address(end)); @@ -49,8 +49,8 @@ abstract contract D3MPoolBaseTest is DssTest { contractName = _contractName; } - function setPoolContract(ID3MPool _pool) internal { - pool = _pool; + function setPoolContract(address _pool) internal virtual { + pool = ID3MPool(_pool); } function test_auth() public virtual { diff --git a/src/tests/pools/D3MSwapPool.t.sol b/src/tests/pools/D3MSwapPool.t.sol new file mode 100644 index 00000000..afb62d38 --- /dev/null +++ b/src/tests/pools/D3MSwapPool.t.sol @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: © 2023 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 { PipMock } from "../mocks/PipMock.sol"; + +import { D3MSwapPool } from "../../pools/D3MSwapPool.sol"; + +abstract contract D3MSwapPoolTest is D3MPoolBaseTest { + + bytes32 constant ILK = "TEST-ILK"; + + D3MSwapPool private pool; + TokenMock internal gem; + PipMock internal pip; + + event SwapGemForDai(address indexed owner, uint256 gems, uint256 dai); + event SwapDaiForGem(address indexed owner, uint256 dai, uint256 gems); + + function baseInit(string memory _contractName) internal virtual override { + super.baseInit(_contractName); + gem = new TokenMock(6); + + pip = new PipMock(); + pip.poke(2e18); // Gem is worth $2 / unit + + gem.mint(address(this), 1_000_000 * 1e6); + dai.mint(address(this), 1_000_000 * 1e18); + } + + function setPoolContract(address _pool) internal virtual override { + super.setPoolContract(_pool); + pool = D3MSwapPool(_pool); + gem.approve(_pool, type(uint256).max); + dai.approve(_pool, type(uint256).max); + + pool.file("pip", address(pip)); + pool.file("swapGemForDaiPip", address(pip)); + pool.file("swapDaiForGemPip", address(pip)); + } + + function _ensureRatio(uint256 ratioInBps, uint256 totalBalance, bool isSellingGem) internal virtual { + isSellingGem; + dai.transfer(address(pool), totalBalance * (BPS - ratioInBps) / BPS); + gem.transfer(address(pool), totalBalance * ratioInBps * WAD / (BPS * uint256(pip.read()) * 1e12)); + } + + function test_constructor() public { + assertEq(address(pool.hub()), address(hub)); + assertEq(pool.ilk(), ILK); + assertEq(address(pool.vat()), address(vat)); + assertEq(address(pool.dai()), address(dai)); + assertEq(address(pool.gem()), address(gem)); + } + + function test_file_addresses() public { + checkFileAddress(address(pool), contractName, ["hub", "pip", "swapGemForDaiPip", "swapDaiForGemPip"]); + + vat.cage(); + vm.expectRevert(abi.encodePacked(contractName, "/no-file-during-shutdown")); + pool.file("some value", address(1)); + } + + function test_withdraw() public { + uint256 startingBal = dai.balanceOf(address(this)); + dai.transfer(address(pool), 100 ether); + assertEq(dai.balanceOf(address(pool)), 100 ether); + assertEq(dai.balanceOf(address(this)), startingBal - 100 ether); + assertEq(dai.balanceOf(address(hub)), 0); + vm.prank(address(hub)); pool.withdraw(50 ether); + assertEq(dai.balanceOf(address(pool)), 50 ether); + assertEq(dai.balanceOf(address(this)), startingBal - 100 ether); + assertEq(dai.balanceOf(address(hub)), 50 ether); + } + + function test_redeemable_returns_gem() public { + assertEq(pool.redeemable(), address(gem)); + } + + function test_exit_gem() public { + uint256 tokens = gem.balanceOf(address(this)); + gem.transfer(address(pool), tokens); + assertEq(gem.balanceOf(address(this)), 0); + assertEq(gem.balanceOf(address(pool)), tokens); + + end.setArt(tokens); + vm.prank(address(hub)); pool.exit(address(this), tokens); + + assertEq(gem.balanceOf(address(this)), tokens); + assertEq(gem.balanceOf(address(pool)), 0); + } + + function test_quit_moves_balance() public { + gem.transfer(address(pool), 50 * 1e6); + dai.transfer(address(pool), 100 ether); + assertEq(gem.balanceOf(address(pool)), 50 * 1e6); + assertEq(dai.balanceOf(address(pool)), 100 ether); + + pool.quit(TEST_ADDRESS); + + assertEq(gem.balanceOf(address(pool)), 0); + assertEq(dai.balanceOf(address(pool)), 0); + assertEq(gem.balanceOf(TEST_ADDRESS), 50 * 1e6); + assertEq(dai.balanceOf(TEST_ADDRESS), 100 ether); + } + + function test_assetBalance() public virtual { + assertEq(pool.assetBalance(), 0); + gem.transfer(address(pool), 10 * 1e6); + assertEq(pool.assetBalance(), 20 ether); // 10 tokens @ $2 / unit + dai.transfer(address(pool), 30 ether); + assertEq(pool.assetBalance(), 50 ether); // 10 tokens @ $2 / unit + 30 dai + } + + function test_assetBalance_uses_market_pip() public { + _ensureRatio(5000, 100 ether, true); + + assertEq(pool.assetBalance(), 100 ether); + PipMock pip2 = new PipMock(); + pip2.poke(4 * WAD); // Double the price of the gems + pool.file("pip", address(pip2)); + assertEq(pool.assetBalance(), 150 ether); + } + + function test_maxDeposit() public { + assertEq(pool.maxDeposit(), type(uint256).max); + } + + function test_maxWithdraw() public { + dai.transfer(address(pool), 100 ether); + + assertEq(pool.maxWithdraw(), 100 ether); + } + + function test_liquidityAvailable() public { + dai.transfer(address(pool), 100 ether); + + assertEq(pool.liquidityAvailable(), 100 ether); + } + + function test_idleLiquidity() public { + dai.transfer(address(pool), 100 ether); + + assertEq(pool.idleLiquidity(), 100 ether); + } + + function test_swapGemForDai() public { + _ensureRatio(0, 100 ether, true); + + uint256 gemBal = gem.balanceOf(address(this)); + assertEq(dai.balanceOf(TEST_ADDRESS), 0); + + uint256 amountOut = pool.previewSwapGemForDai(10 * 1e6); + vm.expectEmit(true, true, true, true); + emit SwapGemForDai(TEST_ADDRESS, 10 * 1e6, amountOut); + pool.swapGemForDai(TEST_ADDRESS, 10 * 1e6, amountOut); + + assertEq(gem.balanceOf(address(this)), gemBal - 10 * 1e6); + assertEq(dai.balanceOf(TEST_ADDRESS), amountOut); + } + + function test_swapGemForDai_minDaiAmt_too_high() public { + _ensureRatio(0, 100 ether, true); + + uint256 amountOut = pool.previewSwapGemForDai(10 * 1e6); + vm.expectRevert("D3MSwapPool/too-little-dai"); + pool.swapGemForDai(TEST_ADDRESS, 10 * 1e6, amountOut + 1); + } + + function test_previewSwapGemForDai_uses_sellPip() public { + _ensureRatio(0, 100 ether, true); + + uint256 amountOut = pool.previewSwapGemForDai(10 * 1e6); + PipMock pip2 = new PipMock(); + pip2.poke(3 * WAD); // Gems are worth more + pool.file("swapGemForDaiPip", address(pip2)); + assertGt(pool.previewSwapGemForDai(10 * 1e6), amountOut); + } + + function test_swapDaiForGem() public { + _ensureRatio(10000, 100 ether, false); + + uint256 daiBal = dai.balanceOf(address(this)); + assertEq(gem.balanceOf(TEST_ADDRESS), 0); + + uint256 amountOut = pool.previewSwapDaiForGem(10 ether); + vm.expectEmit(true, true, true, true); + emit SwapDaiForGem(TEST_ADDRESS, 10 ether, amountOut); + pool.swapDaiForGem(TEST_ADDRESS, 10 ether, amountOut); + + assertEq(gem.balanceOf(TEST_ADDRESS), amountOut); + assertEq(dai.balanceOf(address(this)), daiBal - 10 ether); + } + + function test_swapDaiForGem_minGemAmt_too_high() public { + _ensureRatio(10000, 100 ether, false); + + uint256 amountOut = pool.previewSwapDaiForGem(10 ether); + vm.expectRevert("D3MSwapPool/too-little-gems"); + pool.swapDaiForGem(TEST_ADDRESS, 10 ether, amountOut + 1); + } + + function test_previewSwapDaiForGem_uses_buyPip() public { + _ensureRatio(10000, 100 ether, false); + + uint256 amountOut = pool.previewSwapDaiForGem(20 ether); + PipMock pip2 = new PipMock(); + pip2.poke(1 * WAD); // Gems are worth less + pool.file("swapDaiForGemPip", address(pip2)); + assertGt(pool.previewSwapDaiForGem(20 ether), amountOut); + } + +} diff --git a/src/utils/EnumerableSet.sol b/src/utils/EnumerableSet.sol new file mode 100644 index 00000000..68148e94 --- /dev/null +++ b/src/utils/EnumerableSet.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/structs/EnumerableSet.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) + * and `uint256` (`UintSet`) are supported. + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + if (lastIndex != toDeleteIndex) { + bytes32 lastvalue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastvalue; + // Update the index for the moved value + set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) private view returns (bytes32) { + return set._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function _values(Set storage set) private view returns (bytes32[] memory) { + return set._values; + } + + // Bytes32Set + + struct Bytes32Set { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _add(set._inner, value); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _remove(set._inner, value); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { + return _contains(set._inner, value); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { + return _at(set._inner, index); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { + return _values(set._inner); + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + return _add(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + return _remove(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return _contains(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) internal view returns (address) { + return address(uint160(uint256(_at(set._inner, index)))); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(AddressSet storage set) internal view returns (address[] memory) { + bytes32[] memory store = _values(set._inner); + address[] memory result; + + assembly { + result := store + } + + return result; + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) internal returns (bool) { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) internal view returns (bool) { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) internal view returns (uint256) { + return uint256(_at(set._inner, index)); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(UintSet storage set) internal view returns (uint256[] memory) { + bytes32[] memory store = _values(set._inner); + uint256[] memory result; + + assembly { + result := store + } + + return result; + } +}