diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 62376420..ccdfab21 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -258,9 +258,11 @@ contract ActivePool is IActivePool { uint256 remainderToLPs = mintedAmount - spYield; _boldToken.mint(address(interestRouter), remainderToLPs); - _boldToken.mint(address(stabilityPool), spYield); - stabilityPool.triggerBoldRewards(spYield); + if (spYield > 0) { + _boldToken.mint(address(stabilityPool), spYield); + stabilityPool.triggerBoldRewards(spYield); + } } lastAggUpdateTime = block.timestamp; diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index d4d7d567..7f894bed 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -76,6 +76,7 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function getTotalBoldDeposits() external view returns (uint256); function getYieldGainsOwed() external view returns (uint256); + function getYieldGainsPending() external view returns (uint256); /* * Calculates the Coll gain earned by the deposit since its last snapshots were taken. diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 63de2df9..ce759aca 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -146,6 +146,9 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { // TODO: from the contract's perspective, this is a write-only variable. It is only ever read in tests, so it would // be better to keep it outside the core contract. uint256 internal yieldGainsOwed; + // Total remaining Bold yield gains (from Trove interest mints) held by SP, not yet paid out to depositors, + // and not accounted for because they were received when the total deposits were too small + uint256 internal yieldGainsPending; // --- Data structures --- @@ -227,6 +230,10 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { return yieldGainsOwed; } + function getYieldGainsPending() external view override returns (uint256) { + return yieldGainsPending; + } + // --- External Depositor Functions --- /* provideToSP(): @@ -263,10 +270,16 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { _updateDepositAndSnapshots(msg.sender, newDeposit, newStashedColl); boldToken.sendToPool(msg.sender, address(this), _topUp); - _updateTotalBoldDeposits(_topUp + keptYieldGain, 0); + uint256 totalBoldDepositsCached = _updateTotalBoldDeposits(_topUp + keptYieldGain, 0); _decreaseYieldGainsOwed(currentYieldGain); _sendBoldtoDepositor(msg.sender, yieldGainToSend); _sendCollGainToDepositor(collToSend); + + // If there were pending yields and with the new deposit we are reaching the threshold, let’s move the yield to owed + uint256 yieldGainsPendingCached = yieldGainsPending; + if (yieldGainsPendingCached > 0 && totalBoldDepositsCached >= DECIMAL_PRECISION) { + _updateYieldRewardsSum(yieldGainsPendingCached, totalBoldDepositsCached); + } } function _getYieldToKeepOrSend(uint256 _currentYieldGain, bool _doClaim) internal pure returns (uint256, uint256) { @@ -319,9 +332,15 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { _updateDepositAndSnapshots(msg.sender, newDeposit, newStashedColl); _decreaseYieldGainsOwed(currentYieldGain); - _updateTotalBoldDeposits(keptYieldGain, boldToWithdraw); + uint256 totalBoldDepositsCached = _updateTotalBoldDeposits(keptYieldGain, boldToWithdraw); _sendBoldtoDepositor(msg.sender, boldToWithdraw + yieldGainToSend); _sendCollGainToDepositor(collToSend); + + // If there were pending yields and with the new deposit we are reaching the threshold, let’s move the yield to owed + uint256 yieldGainsPendingCached = yieldGainsPending; + if (yieldGainsPendingCached > 0 && totalBoldDepositsCached >= DECIMAL_PRECISION) { + _updateYieldRewardsSum(yieldGainsPendingCached, totalBoldDepositsCached); + } } function _getNewStashedCollAndCollToSend(address _depositor, uint256 _currentCollGain, bool _doClaim) @@ -358,20 +377,27 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { function triggerBoldRewards(uint256 _boldYield) external { _requireCallerIsActivePool(); + assert(_boldYield > 0); // TODO: remove before deploying uint256 totalBoldDepositsCached = totalBoldDeposits; // cached to save an SLOAD - /* - * When total deposits is 0, B is not updated. In this case, the BOLD issued can not be obtained by later - * depositors - it is missed out on, and remains in the balance of the SP. - * - */ - if (totalBoldDepositsCached == 0 || _boldYield == 0) { + + // When total deposits is very small, B is not updated. In this case, the BOLD issued is hold + // until the total deposits reach 1 BOLD (remains in the balance of the SP). + if (totalBoldDepositsCached < DECIMAL_PRECISION) { + yieldGainsPending += _boldYield; return; } - yieldGainsOwed += _boldYield; + _updateYieldRewardsSum(yieldGainsPending + _boldYield, totalBoldDepositsCached); + } - uint256 yieldPerUnitStaked = _computeYieldPerUnitStaked(_boldYield, totalBoldDepositsCached); + function _updateYieldRewardsSum(uint256 _accumulatedYield, uint256 _totalBoldDeposits) internal { + assert(_accumulatedYield > 0); // TODO: remove before deploying + + yieldGainsOwed += _accumulatedYield; + yieldGainsPending = 0; + + uint256 yieldPerUnitStaked = _computeYieldPerUnitStaked(_accumulatedYield, _totalBoldDeposits); uint256 marginalYieldGain = yieldPerUnitStaked * P; epochToScaleToB[currentEpoch][currentScale] = epochToScaleToB[currentEpoch][currentScale] + marginalYieldGain; @@ -535,11 +561,13 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { emit StabilityPoolCollBalanceUpdated(newCollBalance); } - function _updateTotalBoldDeposits(uint256 _depositIncrease, uint256 _depositDecrease) internal { - if (_depositIncrease == 0 && _depositDecrease == 0) return; + function _updateTotalBoldDeposits(uint256 _depositIncrease, uint256 _depositDecrease) internal returns (uint256) { + if (_depositIncrease == 0 && _depositDecrease == 0) return totalBoldDeposits; uint256 newTotalBoldDeposits = totalBoldDeposits + _depositIncrease - _depositDecrease; totalBoldDeposits = newTotalBoldDeposits; emit StabilityPoolBoldBalanceUpdated(newTotalBoldDeposits); + + return newTotalBoldDeposits; } function _decreaseYieldGainsOwed(uint256 _amount) internal { @@ -584,11 +612,11 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { Snapshots memory snapshots = depositSnapshots[_depositor]; - uint256 pendingSPYield = activePool.calcPendingSPYield(); + uint256 pendingSPYield = activePool.calcPendingSPYield() + yieldGainsPending; uint256 firstPortionPending; uint256 secondPortionPending; - if (pendingSPYield > 0 && snapshots.epoch == currentEpoch) { + if (pendingSPYield > 0 && snapshots.epoch == currentEpoch && totalBoldDeposits >= DECIMAL_PRECISION) { uint256 yieldNumerator = pendingSPYield * DECIMAL_PRECISION + lastYieldError; uint256 yieldPerUnitStaked = yieldNumerator / totalBoldDeposits; uint256 marginalYieldGain = yieldPerUnitStaked * P; diff --git a/contracts/src/test/AnchoredInvariantsTest.t.sol b/contracts/src/test/AnchoredInvariantsTest.t.sol new file mode 100644 index 00000000..212b0f5e --- /dev/null +++ b/contracts/src/test/AnchoredInvariantsTest.t.sol @@ -0,0 +1,483 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "./TestContracts/DevTestSetup.sol"; +import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; +import {BaseMultiCollateralTest} from "./TestContracts/BaseMultiCollateralTest.sol"; +import {AdjustedTroveProperties, InvariantsTestHandler} from "./TestContracts/InvariantsTestHandler.t.sol"; +import {Logging} from "./Utils/Logging.sol"; + +contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollateralTest { + using StringFormatting for uint256; + + InvariantsTestHandler handler; + + function setUp() public override { + super.setUp(); + + TestDeployer.TroveManagerParams[] memory p = new TestDeployer.TroveManagerParams[](4); + p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether); + TestDeployer deployer = new TestDeployer(); + Contracts memory contracts; + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth,) + = deployer.deployAndConnectContractsMultiColl(p); + setupContracts(contracts); + + handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); + vm.label(address(handler), "handler"); + + actors.push(Actor("adam", adam)); + actors.push(Actor("barb", barb)); + actors.push(Actor("carl", carl)); + actors.push(Actor("dana", dana)); + actors.push(Actor("eric", eric)); + actors.push(Actor("fran", fran)); + actors.push(Actor("gabe", gabe)); + actors.push(Actor("hope", hope)); + for (uint256 i = 0; i < actors.length; ++i) { + vm.label(actors[i].account, actors[i].label); + } + } + + function testWrongYield() external { + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(adam); + handler.registerBatchManager( + 0, + 0.257486338754888547 ether, + 0.580260126400716372 ether, + 0.474304801140122485 ether, + 0.84978254245815657 ether, + 2121012 + ); + + vm.prank(eric); + handler.registerBatchManager( + 2, + 0.995000000000011223 ether, + 0.999999999997818617 ether, + 0.999999999561578875 ether, + 0.000000000000010359 ether, + 5174410 + ); + + vm.prank(fran); + handler.warp(3_662_052); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(hope); + handler.addMeToLiquidationBatch(); + + vm.prank(barb); + handler.addMeToLiquidationBatch(); + + // upper hint: 0 + // lower hint: 0 + // upfront fee: 1_246.586073354248297808 ether + vm.prank(hope); + handler.openTrove( + 0, 99_999.999999999999999997 ether, 2.251600954885856105 ether, 0.650005595391858041 ether, 8768, 0 + ); + + vm.prank(adam); + handler.addMeToLiquidationBatch(); + + vm.prank(eric); + handler.addMeToLiquidationBatch(); + + vm.prank(hope); + handler.warp(9_396_472); + + vm.prank(gabe); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(dana); + handler.registerBatchManager( + 2, + 0.995000000000011139 ether, + 0.998635073564148166 ether, + 0.996010156573547401 ether, + 0.000000000000011577 ether, + 9078342 + ); + + vm.prank(carl); + handler.registerBatchManager( + 1, 0.995000004199127012 ether, 1 ether, 0.999139502777974999 ether, 0.059938454189132239 ether, 1706585 + ); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 897.541972815058774421 ether + vm.prank(gabe); + handler.provideToSP(0, 58_897.613356828171795189 ether, false); + } + + function testRedeemUnderflow() external { + vm.prank(fran); + handler.warp(18_162); + + vm.prank(carl); + handler.registerBatchManager( + 0, + 0.995000001857124003 ether, + 0.999999628575220679 ether, + 0.999925530120657388 ether, + 0.249999999999999999 ether, + 12664 + ); + + vm.prank(hope); + handler.addMeToLiquidationBatch(); + + vm.prank(fran); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(fran); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(gabe); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(dana); + handler.addMeToLiquidationBatch(); + + vm.prank(eric); + handler.warp(4_641_555); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(dana); + handler.addMeToLiquidationBatch(); + + vm.prank(gabe); + handler.addMeToLiquidationBatch(); + + vm.prank(fran); + handler.addMeToLiquidationBatch(); + + vm.prank(hope); + handler.registerBatchManager( + 0, + 0.739903753088089514 ether, + 0.780288740735740819 ether, + 0.767858707410717411 ether, + 0.000000000000022941 ether, + 21644 + ); + + // upper hint: 80084422859880547211683076133703299733277748156566366325829078699459944778998 + // lower hint: 104346312485569601582594868672255666718935311025283394307913733247512361320190 + // upfront fee: 290.81243876303301812 ether + vm.prank(adam); + handler.openTrove( + 3, 39_503.887731534058892956 ether, 1.6863644596244192 ether, 0.38385567397413886 ether, 1, 7433679 + ); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(hope); + handler.warp(23_201); + + vm.prank(carl); + handler.warp(18_593_995); + + // redemption rate: 0.195871664252157123 ether + // redeemed BOLD: 15_191.361299840412827416 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(carl); + handler.redeemCollateral(15_191.361299840412827416 ether, 0); + + // redemption rate: 0.195871664252157123 ether + // redeemed BOLD: 0.000000000000006302 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(dana); + handler.redeemCollateral(0.000000000000006302 ether, 1); + + vm.prank(hope); + handler.registerBatchManager( + 1, + 0.822978751289802582 ether, + 0.835495454680029657 ether, + 0.833312890646159679 ether, + 0.422857251385135959 ether, + 29470036 + ); + + vm.prank(gabe); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(barb); + handler.addMeToLiquidationBatch(); + + vm.prank(gabe); + handler.warp(31); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0.012686316538387649 ether + vm.prank(carl); + handler.provideToSP(3, 0.000000000000021916 ether, false); + + // upper hint: 0 + // lower hint: 39695913545351040647077841548061220386885435874215782275463606055905069661493 + // upfront fee: 0 ether + vm.prank(carl); + handler.setBatchManagerAnnualInterestRate(0, 0.998884384586837808 ether, 15539582, 63731457); + + vm.prank(gabe); + handler.registerBatchManager( + 0, + 0.351143076054309979 ether, + 0.467168361632094569 ether, + 0.433984569464653931 ether, + 0.000000000000000026 ether, + 16482089 + ); + + vm.prank(adam); + handler.registerBatchManager( + 3, + 0.995000000000006201 ether, + 0.996462074472343849 ether, + 0.995351673013151748 ether, + 0.045759837128294745 ether, + 10150905 + ); + + vm.prank(dana); + handler.warp(23_299); + + vm.prank(carl); + handler.warp(13_319_679); + + // redemption rate: 0.246264103698059017 ether + // redeemed BOLD: 16_223.156659761268542045 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(eric); + handler.redeemCollateral(16_223.156659761268542045 ether, 0); + } + + function testWrongYieldPrecision() external { + vm.prank(carl); + handler.addMeToLiquidationBatch(); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(barb); + handler.warp(19_326); + + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(dana); + handler.registerBatchManager( + 3, + 0.30820256993275862 ether, + 0.691797430067250243 ether, + 0.383672204747583321 ether, + 0.000000000000018015 ether, + 11403 + ); + + vm.prank(eric); + handler.registerBatchManager( + 3, + 0.018392910495297323 ether, + 0.98160708950470919 ether, + 0.963214179009414206 ether, + 0.000000000000019546 ether, + 13319597 + ); + + vm.prank(fran); + handler.warp(354); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(eric); + handler.warp(15_305_108); + + // upper hint: 84669063888545001427406517193344625874395507444463583314999084271619652858036 + // lower hint: 69042136817699606427763587628766179145825895354994492055731203083594873444699 + // upfront fee: 1_702.831959251916404109 ether + vm.prank(fran); + handler.openTrove( + 1, 99_999.999999999999999998 ether, 1.883224555937797003 ether, 0.887905235895642125 ether, 4164477, 39 + ); + + vm.prank(dana); + handler.warp(996); + + vm.prank(eric); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(barb); + handler.warp(4_143_017); + + vm.prank(fran); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0 ether + vm.prank(adam); + handler.provideToSP(0, 0.000000000000011094 ether, true); + + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); + + // upper hint: 0 + // lower hint: 0 + // upfront fee: 1_513.428916567114728229 ether + vm.prank(barb); + handler.openTrove( + 2, + 79_311.063107967331806055 ether, + 1.900000000000001559 ether, + 0.995000000000007943 ether, + 3270556590, + 1229144376 + ); + + vm.prank(fran); + handler.addMeToLiquidationBatch(); + + // price: 221.052631578948441462 ether + vm.prank(dana); + handler.setPrice(2, 2.100000000000011917 ether); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 1_226.039010661379810958 ether + // pendingInterest: 11_866.268348193546380256 ether + vm.prank(carl); + handler.provideToSP(1, 0.027362680048399155 ether, false); + + // upper hint: 0 + // lower hint: 109724453348421969168156614404527408958334892291486496459024204968877369036377 + // upfront fee: 9.807887080131946403 ether + vm.prank(eric); + handler.openTrove( + 3, 30_260.348082017558572105 ether, 1.683511222023706186 ether, 0.016900375815455486 ether, 108, 14159 + ); + + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); + + vm.prank(adam); + handler.addMeToLiquidationBatch(); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.1474722457669512 ether + // redeemed BOLD: 64_016.697525751186019703 ether + // redeemed Troves: [ + // [], + // [fran], + // [barb], + // [eric], + // ] + vm.prank(dana); + handler.redeemCollateral(64_016.697525751186019705 ether, 0); + + // upper hint: 102052496222650354016228296600262737092032771006947291868573062530791731100756 + // lower hint: 0 + vm.prank(eric); + handler.applyMyPendingDebt(3, 2542, 468); + + vm.prank(gabe); + handler.warp(20_216); + + vm.prank(carl); + handler.registerBatchManager( + 1, + 0.995000000000425732 ether, + 0.998288014105982235 ether, + 0.996095220733623871 ether, + 0.000000000000027477 ether, + 3299 + ); + + vm.prank(carl); + handler.addMeToLiquidationBatch(); + + // redemption rate: 0.108097849716691371 ether + // redeemed BOLD: 0.000151948988774207 ether + // redeemed Troves: [ + // [], + // [fran], + // [barb], + // [eric], + // ] + vm.prank(hope); + handler.redeemCollateral(0.000151948988774209 ether, 0); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0 ether + vm.prank(eric); + handler.provideToSP(0, 76_740.446487959260685533 ether, true); + + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 9_803.032557027063219919 ether + // pendingInterest: 0 ether + vm.prank(hope); + handler.provideToSP(1, 4.127947448768090932 ether, false); + } +} diff --git a/contracts/src/test/AnchoredSPInvariantsTest.t.sol b/contracts/src/test/AnchoredSPInvariantsTest.t.sol new file mode 100644 index 00000000..ee766940 --- /dev/null +++ b/contracts/src/test/AnchoredSPInvariantsTest.t.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "./TestContracts/DevTestSetup.sol"; +import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; +import {Logging} from "./Utils/Logging.sol"; + +contract AnchoredSPInvariantsTest is DevTestSetup { + using StringFormatting for uint256; + + struct Actor { + string label; + address account; + } + + SPInvariantsTestHandler handler; + + address constant adam = 0x1111111111111111111111111111111111111111; + address constant barb = 0x2222222222222222222222222222222222222222; + address constant carl = 0x3333333333333333333333333333333333333333; + address constant dana = 0x4444444444444444444444444444444444444444; + address constant eric = 0x5555555555555555555555555555555555555555; + address constant fran = 0x6666666666666666666666666666666666666666; + address constant gabe = 0x7777777777777777777777777777777777777777; + address constant hope = 0x8888888888888888888888888888888888888888; + + Actor[] actors; + + function setUp() public override { + super.setUp(); + + TestDeployer deployer = new TestDeployer(); + (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,,) = + deployer.deployAndConnectContracts(); + stabilityPool = contracts.stabilityPool; + + handler = new SPInvariantsTestHandler( + SPInvariantsTestHandler.Contracts({ + boldToken: boldToken, + borrowerOperations: contracts.borrowerOperations, + collateralToken: contracts.collToken, + priceFeed: contracts.priceFeed, + stabilityPool: contracts.stabilityPool, + troveManager: contracts.troveManager, + collSurplusPool: contracts.collSurplusPool + }), + hintHelpers + ); + + actors.push(Actor("adam", adam)); + actors.push(Actor("barb", barb)); + actors.push(Actor("carl", carl)); + actors.push(Actor("dana", dana)); + actors.push(Actor("eric", eric)); + actors.push(Actor("fran", fran)); + actors.push(Actor("gabe", gabe)); + actors.push(Actor("hope", hope)); + for (uint256 i = 0; i < actors.length; ++i) { + vm.label(actors[i].account, actors[i].label); + } + + vm.label(address(handler), "handler"); + } + + function invariant_allFundsClaimable() internal view { + uint256 stabilityPoolColl = stabilityPool.getCollBalance(); + uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); + uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); + + uint256 claimableColl = 0; + uint256 claimableBold = 0; + uint256 sumYieldGains = 0; + + for (uint256 i = 0; i < actors.length; ++i) { + claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); + claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); + sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); + //info("+sumYieldGains: ", sumYieldGains.decimal()); + } + + info("stabilityPoolColl: ", stabilityPoolColl.decimal()); + info("claimableColl: ", claimableColl.decimal()); + info("claimableColl E: ", stabilityPool.getDepositorCollGain(eric).decimal()); + info("claimableColl G: ", stabilityPool.getDepositorCollGain(gabe).decimal()); + info("stabilityPoolBold: ", stabilityPoolBold.decimal()); + info("claimableBold: ", claimableBold.decimal()); + info("claimableBold E: ", stabilityPool.getCompoundedBoldDeposit(eric).decimal()); + info("claimableBold G: ", stabilityPool.getCompoundedBoldDeposit(gabe).decimal()); + info("yieldGainsOwed: ", yieldGainsOwed.decimal()); + info("sumYieldGains: ", sumYieldGains.decimal()); + info("yieldGains E: ", stabilityPool.getDepositorYieldGain(eric).decimal()); + info("yieldGains G: ", stabilityPool.getDepositorYieldGain(gabe).decimal()); + info(""); + assertApproxEqAbsDecimal(stabilityPoolColl, claimableColl, 0.00001 ether, 18, "SP Coll !~ claimable Coll"); + assertApproxEqAbsDecimal(stabilityPoolBold, claimableBold, 0.001 ether, 18, "SP BOLD !~ claimable BOLD"); + assertApproxEqAbsDecimal(yieldGainsOwed, sumYieldGains, 0.001 ether, 18, "SP yieldGainsOwed !~= sum(yieldGain)"); + } + + function testYieldGlobalTracker() external { + vm.prank(adam); + handler.openTrove(18_250 ether); + + vm.prank(eric); + handler.openTrove(10_220 ether); + + vm.prank(gabe); + handler.provideToSp(18_251.7500000000001 ether, false); + + vm.prank(adam); + handler.liquidateMe(); + + vm.prank(adam); + handler.openTrove(18_250 ether); + + invariant_allFundsClaimable(); + } + + function testYieldGlobalTracker2() external { + // coll = 750.071917808219178083 ether, debt = 100_009.589041095890410958 ether + vm.prank(adam); + handler.openTrove(100_000 ether); + + // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808219 ether + vm.prank(eric); + handler.openTrove(2_000 ether); + + vm.prank(eric); + handler.provideToSp(2_000.19178082191781022 ether, false); + + // totalBoldDeposits = 2_000.19178082191781022 ether + + vm.prank(eric); + handler.liquidateMe(); + + // totalBoldDeposits = 0.000000000000002001 ether + // P = 1.000404070842521948 ether + + // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808219 ether + vm.prank(eric); + handler.openTrove(2_000 ether); + + vm.prank(eric); + handler.provideToSp(2_000.191780821917808219 ether, false); + + // totalBoldDeposits = 2_000.19178082191781022 ether + + vm.prank(eric); + handler.liquidateMe(); + + invariant_allFundsClaimable(); + } + + function testYieldGlobalTracker3() external { + // coll = 609.865977378640299561 ether, debt = 81_315.463650485373274767 ether + vm.prank(barb); + handler.openTrove(81_307.667024880247771557 ether); + + // coll = 735.070479452054794543 ether, debt = 98_009.397260273972605648 ether + vm.prank(dana); + handler.openTrove(98_000.000000000000002908 ether); + + // coll = 373.873319035600269508 ether, debt = 49_849.775871413369267714 ether + vm.prank(eric); + handler.openTrove(49_844.996214242140569304 ether); + + // pulling `deposited` from fixture + vm.prank(gabe); + handler.provideToSp(81_315.463650485373356083 ether, false); + + // totalBoldDeposits = 81_315.463650485373356083 ether + + // pulling `deposited` from fixture + vm.prank(eric); + handler.provideToSp(98_009.397260273972605648 ether, false); + + // totalBoldDeposits = 179_324.860910759345961731 ether + + vm.prank(barb); + handler.liquidateMe(); + + // totalBoldDeposits = 98_009.397260273972686964 ether + // P = 0.546546623610923366 ether + + vm.prank(dana); + handler.liquidateMe(); + invariant_allFundsClaimable(); + + // totalBoldDeposits = 0.000000000000081316 ether + // P = 0.000000001294434626 ether + + // coll = 448.153242320289758012 ether, debt = 59_753.765642705301068218 ether + vm.prank(gabe); + handler.openTrove(59_748.03637894293667703 ether); + + invariant_allFundsClaimable(); + + // pulling `deposited` from fixture + vm.prank(gabe); + handler.provideToSp(98_009.397260273972703658 ether, false); + // [FAIL. Reason: panic: arithmetic underflow or overflow (0x11)] + } + + function testYieldGlobalTracker4() external { + // coll = 735.07047945205479457 ether, debt = 98_009.397260273972609312 ether + vm.prank(eric); + handler.openTrove(98_000.000000000000006572 ether); + + // coll = 15.073363060611747045 ether, debt = 2_009.781741414899605927 ether + vm.prank(adam); + handler.openTrove(2_009.589041095890410957 ether); + + // coll = 674.842356181481975293 ether, debt = 89_978.98082419759670561 ether + vm.prank(gabe); + handler.openTrove(89_970.353530023484864596 ether); + + // coll = 562.802728905215994793 ether, debt = 75_040.363854028799305609 ether + vm.prank(carl); + handler.openTrove(75_033.168892628136333632 ether); + + // pulling `deposited` from fixture + vm.prank(eric); + handler.provideToSp(75_040.36385402879938065 ether, false); + + // totalBoldDeposits = 75_040.36385402879938065 ether + + vm.prank(carl); + handler.liquidateMe(); + + // totalBoldDeposits = 0.000000000000075041 ether + // P = 0.000000001000008477 ether + + // pulling `deposited` from fixture + vm.prank(gabe); + handler.provideToSp(98_009.397260273972609312 ether, false); + + // totalBoldDeposits = 98_009.397260273972684353 ether + + vm.prank(eric); + handler.liquidateMe(); + + // totalBoldDeposits = 0.000000000000075041 ether + // P = 0.000000000765657561 ether + + // coll = 456.581526480883492157 ether, debt = 60_877.53686411779895416 ether + vm.prank(hope); + handler.openTrove(60_871.699851803242478854 ether); + + invariant_allFundsClaimable(); + } +} diff --git a/contracts/src/test/Invariants.t.sol b/contracts/src/test/Invariants.t.sol index 0b6c40a9..d861a11c 100644 --- a/contracts/src/test/Invariants.t.sol +++ b/contracts/src/test/Invariants.t.sol @@ -133,7 +133,10 @@ contract InvariantsTest is Logging, BaseInvariantTest, BaseMultiCollateralTest { "Wrong StabilityPool deposits" ); assertEqDecimal( - c.stabilityPool.getYieldGainsOwed(), handler.spBoldYield(i), 18, "Wrong StabilityPool yield" + c.stabilityPool.getYieldGainsOwed() + c.stabilityPool.getYieldGainsPending(), + handler.spBoldYield(i), + 18, + "Wrong StabilityPool yield" ); assertEqDecimal(c.stabilityPool.getCollBalance(), handler.spColl(i), 18, "Wrong StabilityPool coll"); @@ -276,10 +279,10 @@ contract InvariantsTest is Logging, BaseInvariantTest, BaseMultiCollateralTest { // This only holds as long as no one sends BOLD directly to the SP's address other than ActivePool assertApproxEqAbsDecimal( boldToken.balanceOf(address(stabilityPool)), - sumBoldDeposit + sumYieldGain + handler.spUnclaimableBoldYield(j), + sumBoldDeposit + sumYieldGain + stabilityPool.getYieldGainsPending(), 1e-3 ether, 18, - "SP BOLD balance !~= claimable + unclaimable BOLD" + "SP BOLD balance !~= claimable + pending" ); } } diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index 17e0374f..9ce9d087 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -161,6 +161,22 @@ contract DevTestSetup is BaseTest { assertEq(uint8(troveManager.getTroveStatus(troveIDs.C)), uint8(ITroveManager.Status.closedByLiquidation)); } + function _setupForSPDepositAdjustmentsWithoutOwedYieldRewards() internal returns (ABCDEF memory troveIDs) { + (troveIDs.A, troveIDs.B, troveIDs.C, troveIDs.D) = _setupForBatchLiquidateTrovesPureOffset(); + + // A claims yield rewards + makeSPWithdrawalAndClaim(A, 0); + + // A liquidates C + liquidate(A, troveIDs.C); + + // D sends BOLD to A and B so they have some to use in tests + transferBold(D, A, boldToken.balanceOf(D) / 2); + transferBold(D, B, boldToken.balanceOf(D)); + + assertEq(uint8(troveManager.getTroveStatus(troveIDs.C)), uint8(ITroveManager.Status.closedByLiquidation)); + } + function _setupForPTests() internal returns (ABCDEF memory) { ABCDEF memory troveIDs; (troveIDs.A, troveIDs.B, troveIDs.C, troveIDs.D) = _setupForBatchLiquidateTrovesPureOffset(); diff --git a/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol b/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol index bd2c5031..617a0bc1 100644 --- a/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol +++ b/contracts/src/test/TestContracts/InvariantsTestHandler.t.sol @@ -198,9 +198,26 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { string errorString; } + struct ProvideToSPContext { + TestDeployer.LiquityContractsDev c; + uint256 pendingInterest; + uint256 totalBoldDeposits; + uint256 blockedSPYield; + uint256 initialBoldDeposit; + uint256 boldDeposit; + uint256 boldYield; + uint256 ethGain; + uint256 ethStash; + uint256 ethClaimed; + uint256 boldClaimed; + string errorString; + } + struct WithdrawFromSPContext { TestDeployer.LiquityContractsDev c; uint256 pendingInterest; + uint256 totalBoldDeposits; + uint256 blockedSPYield; uint256 initialBoldDeposit; uint256 boldDeposit; uint256 boldYield; @@ -333,10 +350,6 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { // Price per branch mapping(uint256 branchIdx => uint256) _price; - // Bold yield sent to the SP at a time when there are no deposits is lost forever - // We keep track of the lost amount so we can use it in invariants - mapping(uint256 branchIdx => uint256) public spUnclaimableBoldYield; - // All free-floating BOLD is kept in the handler, to be dealt out to actors as needed uint256 _handlerBold; @@ -1529,59 +1542,74 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { } function provideToSP(uint256 i, uint256 amount, bool claim) external { + ProvideToSPContext memory v; + i = _bound(i, 0, branches.length - 1); amount = _bound(amount, 0, _handlerBold); - TestDeployer.LiquityContractsDev memory c = branches[i]; - uint256 pendingInterest = c.activePool.calcPendingAggInterest(); - uint256 initialBoldDeposit = c.stabilityPool.deposits(msg.sender); - uint256 boldDeposit = c.stabilityPool.getCompoundedBoldDeposit(msg.sender); - uint256 boldYield = c.stabilityPool.getDepositorYieldGainWithPending(msg.sender); - uint256 ethGain = c.stabilityPool.getDepositorCollGain(msg.sender); - uint256 ethStash = c.stabilityPool.stashedColl(msg.sender); - uint256 ethClaimed = claim ? ethStash + ethGain : 0; - uint256 boldClaimed = claim ? boldYield : 0; - - info("initial deposit: ", initialBoldDeposit.decimal()); - info("compounded deposit: ", boldDeposit.decimal()); - info("yield gain: ", boldYield.decimal()); - info("coll gain: ", ethGain.decimal()); - info("stashed coll: ", ethStash.decimal()); + v.c = branches[i]; + v.pendingInterest = v.c.activePool.calcPendingAggInterest(); + v.totalBoldDeposits = v.c.stabilityPool.getTotalBoldDeposits(); + v.blockedSPYield = v.totalBoldDeposits < DECIMAL_PRECISION + ? v.c.activePool.calcPendingSPYield() + v.c.stabilityPool.getYieldGainsPending() + : 0; + v.initialBoldDeposit = v.c.stabilityPool.deposits(msg.sender); + v.boldDeposit = v.c.stabilityPool.getCompoundedBoldDeposit(msg.sender); + v.boldYield = v.c.stabilityPool.getDepositorYieldGainWithPending(msg.sender); + v.ethGain = v.c.stabilityPool.getDepositorCollGain(msg.sender); + v.ethStash = v.c.stabilityPool.stashedColl(msg.sender); + v.ethClaimed = claim ? v.ethStash + v.ethGain : 0; + v.boldClaimed = claim ? v.boldYield : 0; + + info("initial deposit: ", v.initialBoldDeposit.decimal()); + info("compounded deposit: ", v.boldDeposit.decimal()); + info("yield gain: ", v.boldYield.decimal()); + info("coll gain: ", v.ethGain.decimal()); + info("stashed coll: ", v.ethStash.decimal()); + info("blocked SP yield: ", v.blockedSPYield.decimal()); logCall("provideToSP", i.toString(), amount.decimal(), claim.toString()); // TODO: randomly deal less than amount? _dealBold(msg.sender, amount); - string memory errorString; vm.prank(msg.sender); - - try c.stabilityPool.provideToSP(amount, claim) { + try v.c.stabilityPool.provideToSP(amount, claim) { // Preconditions assertGtDecimal(amount, 0, 18, "Should have failed as amount was zero"); // Effects (deposit) - ethStash += ethGain; - ethStash -= ethClaimed; + v.ethStash += v.ethGain; + v.ethStash -= v.ethClaimed; - boldDeposit += amount; - boldDeposit += boldYield; - boldDeposit -= boldClaimed; + v.boldDeposit += amount; + v.boldDeposit += v.boldYield; + v.boldDeposit -= v.boldClaimed; - assertEqDecimal(c.stabilityPool.getCompoundedBoldDeposit(msg.sender), boldDeposit, 18, "Wrong deposit"); - assertEqDecimal(c.stabilityPool.getDepositorYieldGain(msg.sender), 0, 18, "Wrong yield gain"); - assertEqDecimal(c.stabilityPool.getDepositorCollGain(msg.sender), 0, 18, "Wrong coll gain"); - assertEqDecimal(c.stabilityPool.stashedColl(msg.sender), ethStash, 18, "Wrong stashed coll"); + // See if the change unblocked any pending yield + v.totalBoldDeposits += amount; + v.totalBoldDeposits += v.boldYield; + v.totalBoldDeposits -= v.boldClaimed; + + uint256 newBoldYield = + v.totalBoldDeposits >= DECIMAL_PRECISION ? v.blockedSPYield * v.boldDeposit / v.totalBoldDeposits : 0; + + assertEqDecimal(v.c.stabilityPool.getCompoundedBoldDeposit(msg.sender), v.boldDeposit, 18, "Wrong deposit"); + assertApproxEqAbsDecimal( + v.c.stabilityPool.getDepositorYieldGain(msg.sender), newBoldYield, 1e6, 18, "Wrong yield gain" + ); + assertEqDecimal(v.c.stabilityPool.getDepositorCollGain(msg.sender), 0, 18, "Wrong coll gain"); + assertEqDecimal(v.c.stabilityPool.stashedColl(msg.sender), v.ethStash, 18, "Wrong stashed coll"); // Effects (system) - _mintYield(i, pendingInterest, 0); + _mintYield(i, v.pendingInterest, 0); - spColl[i] -= ethClaimed; + spColl[i] -= v.ethClaimed; spBoldDeposits[i] += amount; - spBoldDeposits[i] += boldYield; - spBoldDeposits[i] -= boldClaimed; - spBoldYield[i] -= boldYield; + spBoldDeposits[i] += v.boldYield; + spBoldDeposits[i] -= v.boldClaimed; + spBoldYield[i] -= v.boldYield; } catch Error(string memory reason) { - errorString = reason; + v.errorString = reason; // Justify failures if (reason.equals("StabilityPool: Amount must be non-zero")) { @@ -1591,18 +1619,18 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { } } - if (bytes(errorString).length > 0) { + if (bytes(v.errorString).length > 0) { if (_assumeNoExpectedFailures) vm.assume(false); - info("Expected error: ", errorString); + info("Expected error: ", v.errorString); _log(); // Cleanup (failure) _sweepBold(msg.sender, amount); // Take back the BOLD that was dealt } else { // Cleanup (success) - _sweepBold(msg.sender, boldClaimed); - _sweepColl(i, msg.sender, ethClaimed); + _sweepBold(msg.sender, v.boldClaimed); + _sweepColl(i, msg.sender, v.ethClaimed); } } @@ -1613,6 +1641,10 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { v.c = branches[i]; v.pendingInterest = v.c.activePool.calcPendingAggInterest(); + v.totalBoldDeposits = v.c.stabilityPool.getTotalBoldDeposits(); + v.blockedSPYield = v.totalBoldDeposits < DECIMAL_PRECISION + ? v.c.activePool.calcPendingSPYield() + v.c.stabilityPool.getYieldGainsPending() + : 0; v.initialBoldDeposit = v.c.stabilityPool.deposits(msg.sender); v.boldDeposit = v.c.stabilityPool.getCompoundedBoldDeposit(msg.sender); v.boldYield = v.c.stabilityPool.getDepositorYieldGainWithPending(msg.sender); @@ -1629,6 +1661,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { info("yield gain: ", v.boldYield.decimal()); info("coll gain: ", v.ethGain.decimal()); info("stashed coll: ", v.ethStash.decimal()); + info("blocked SP yield: ", v.blockedSPYield.decimal()); logCall("withdrawFromSP", i.toString(), amount.decimal(), claim.toString()); vm.prank(msg.sender); @@ -1644,8 +1677,18 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { v.boldDeposit -= v.boldClaimed; v.boldDeposit -= v.withdrawn; + // See if the change unblocked any pending yield + v.totalBoldDeposits += v.boldYield; + v.totalBoldDeposits -= v.boldClaimed; + v.totalBoldDeposits -= v.withdrawn; + + uint256 newBoldYield = + v.totalBoldDeposits >= DECIMAL_PRECISION ? v.blockedSPYield * v.boldDeposit / v.totalBoldDeposits : 0; + assertEqDecimal(v.c.stabilityPool.getCompoundedBoldDeposit(msg.sender), v.boldDeposit, 18, "Wrong deposit"); - assertEqDecimal(v.c.stabilityPool.getDepositorYieldGain(msg.sender), 0, 18, "Wrong yield gain"); + assertApproxEqAbsDecimal( + v.c.stabilityPool.getDepositorYieldGain(msg.sender), newBoldYield, 1e6, 18, "Wrong yield gain" + ); assertEqDecimal(v.c.stabilityPool.getDepositorCollGain(msg.sender), 0, 18, "Wrong coll gain"); assertEqDecimal(v.c.stabilityPool.stashedColl(msg.sender), v.ethStash, 18, "Wrong stashed coll"); @@ -2445,12 +2488,7 @@ contract InvariantsTestHandler is BaseHandler, BaseMultiCollateralTest { uint256 mintedYield = pendingInterest + upfrontFee; uint256 mintedSPBoldYield = mintedYield * SP_YIELD_SPLIT / DECIMAL_PRECISION; - if (spBoldDeposits[i] == 0) { - spUnclaimableBoldYield[i] += mintedSPBoldYield; - } else { - spBoldYield[i] += mintedSPBoldYield; - } - + spBoldYield[i] += mintedSPBoldYield; _pendingInterest[i] = 0; } diff --git a/contracts/src/test/events.t.sol b/contracts/src/test/events.t.sol index 7e50b090..93f8490c 100644 --- a/contracts/src/test/events.t.sol +++ b/contracts/src/test/events.t.sol @@ -674,6 +674,7 @@ contract StabilityPoolEventsTest is EventsTest, IStabilityPoolEvents { // Increase epoch makeSPDepositNoClaim(A, liquidatedDebt); + makeSPWithdrawalAndClaim(A, 0); // Claim yield from first troves troveManager.liquidate(liquidatedTroveId[0]); current.epoch = stabilityPool.currentEpoch(); diff --git a/contracts/src/test/interestRateAggregate.t.sol b/contracts/src/test/interestRateAggregate.t.sol index 3c8d8f0c..cc760cd8 100644 --- a/contracts/src/test/interestRateAggregate.t.sol +++ b/contracts/src/test/interestRateAggregate.t.sol @@ -476,6 +476,8 @@ contract InterestRateAggregate is DevTestSetup { priceFeed.setPrice(2000e18); openTroveNoHints100pct(A, 2 ether, troveDebtRequest, 25e16); makeSPDepositAndClaim(A, sPdeposit); + // claim gains from first trove + makeSPWithdrawalAndClaim(A, 0); // fast-forward time vm.warp(block.timestamp + 1 days); @@ -2148,7 +2150,7 @@ contract InterestRateAggregate is DevTestSetup { // --- claimALLCollGains --- function testClaimAllCollGainsIncreasesAggRecordedDebtByPendingAggInterest() public { - _setupForSPDepositAdjustments(); + _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws depsoiit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); @@ -2171,7 +2173,7 @@ contract InterestRateAggregate is DevTestSetup { } function testClaimAllCollGainsReducesPendingAggInterestTo0() public { - _setupForSPDepositAdjustments(); + _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws depsoiit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); @@ -2192,7 +2194,7 @@ contract InterestRateAggregate is DevTestSetup { // // Update last agg. update time to now function testClaimAllCollGainsUpdatesLastAggUpdateTimeToNow() public { - _setupForSPDepositAdjustments(); + _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws deposit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); @@ -2216,7 +2218,7 @@ contract InterestRateAggregate is DevTestSetup { // mints interest to SP function testClaimAllCollGainsMintsAggInterestToSP() public { ABCDEF memory troveIDs; - troveIDs = _setupForSPDepositAdjustments(); + troveIDs = _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws depsoiit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); diff --git a/contracts/src/test/stabilityPool.t.sol b/contracts/src/test/stabilityPool.t.sol index 7c161e89..be7ef79c 100644 --- a/contracts/src/test/stabilityPool.t.sol +++ b/contracts/src/test/stabilityPool.t.sol @@ -71,6 +71,8 @@ contract SPTest is DevTestSetup { uint256 totalDepositsBefore; uint256 spEthBal1; uint256 spEthBal2; + uint256 initialBoldGainA; + uint256 initialBoldGainB; } function _setupStashedAndCurrentCollGains() internal { @@ -777,7 +779,7 @@ contract SPTest is DevTestSetup { } function testClaimAllCollGainsDoesntChangeCurrentCollGain() public { - _setupForSPDepositAdjustments(); + _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws deposit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); @@ -795,7 +797,7 @@ contract SPTest is DevTestSetup { } function testClaimAllCollGainsZerosStashedCollGain() public { - _setupForSPDepositAdjustments(); + _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws deposit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); @@ -813,7 +815,7 @@ contract SPTest is DevTestSetup { } function testClaimAllCollGainsIncreasesUserBalanceByStashedCollGain() public { - _setupForSPDepositAdjustments(); + _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // A withdraws deposit and stashes gain uint256 deposit_A = stabilityPool.getCompoundedBoldDeposit(A); @@ -859,16 +861,16 @@ contract SPTest is DevTestSetup { ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); uint256 pendingAggInterest = activePool.calcPendingAggInterest(); - assertEq(pendingAggInterest, 0); + assertEq(pendingAggInterest, 0, "Pending interest should be zero"); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0, "BOLD reward sum 1"); // Adjust a Trove in a way that doesn't incur an upfront fee repayBold(B, troveIDs.B, 1_000 ether); uint256 boldRewardSum_2 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_2, boldRewardSum_1); + assertEq(boldRewardSum_2, boldRewardSum_1, "BOLD reward sum 2"); } function testBoldRewardSumIncreasesWhenTroveOpened() public { @@ -880,7 +882,7 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0); openTroveNoHints100pct(E, 3 ether, 2000e18, 25e16); @@ -897,7 +899,7 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0); changeInterestRateNoHints(B, troveIDs.B, 75e16); @@ -937,7 +939,7 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0); adjustTrove100pct(A, troveIDs.A, 1, 1, true, true); @@ -954,7 +956,7 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0); // B applies A's pending interest applyPendingDebt(B, troveIDs.A); @@ -972,7 +974,7 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0); // A liquidates D liquidate(A, troveIDs.D); @@ -991,7 +993,7 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 boldRewardSum_1 = stabilityPool.epochToScaleToB(0, 0); - assertEq(boldRewardSum_1, 0); + assertGt(boldRewardSum_1, 0); uint256 wethBalBefore_A = collToken.balanceOf(A); // A redeems @@ -1089,12 +1091,16 @@ contract SPTest is DevTestSetup { assertEq(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); (, uint256 upfrontFee) = openTroveHelper(E, 0, 3 ether, 2000e18, 25e16); uint256 yieldGainsOwed_2 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_2, yieldGainsOwed_1 + _getSPYield(upfrontFee)); + uint256 yieldGainsPending_2 = stabilityPool.getYieldGainsPending(); + assertEq(yieldGainsOwed_2, yieldGainsOwed_1 + _getSPYield(upfrontFee), "Yield owed mismatch 2"); + assertEq(yieldGainsPending_2, 0, "Yield pending mismatch 2"); } function testBoldRewardsOwedIncreasesWhenTroveOpened() public { @@ -1106,7 +1112,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); openTroveNoHints100pct(E, 3 ether, 2000e18, 25e16); @@ -1123,7 +1131,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); changeInterestRateNoHints(B, troveIDs.B, 75e16); @@ -1142,7 +1152,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertGt(yieldGainsOwed_1, 0); // yield from upfront fee + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); // F sends E his bold so he can close vm.startPrank(F); @@ -1163,7 +1175,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); adjustTrove100pct(A, troveIDs.A, 1, 1, true, true); @@ -1180,7 +1194,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); // B applies A's pending interest applyPendingDebt(B, troveIDs.A); @@ -1198,7 +1214,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); // A liquidates D liquidate(A, troveIDs.D); @@ -1217,8 +1235,11 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertEq(yieldGainsOwed_1, 0); + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); uint256 wethBalBefore_A = collToken.balanceOf(A); + // A redeems redeem(A, 1e18); assertGt(collToken.balanceOf(A), wethBalBefore_A); @@ -1237,7 +1258,9 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 yieldGainsOwed_1 = stabilityPool.getYieldGainsOwed(); - assertGt(yieldGainsOwed_1, 0); // yield from upfront fee + uint256 yieldGainsPending_1 = stabilityPool.getYieldGainsPending(); + assertGt(yieldGainsOwed_1, 0, "Yield owed mismatch 1"); + assertEq(yieldGainsPending_1, 0, "Yield pending mismatch 1"); // E Makes deposit makeSPDepositAndClaim(E, 1e18); @@ -1287,7 +1310,7 @@ contract SPTest is DevTestSetup { // --- depositor BOLD rewards tests --- function testGetDepositorBoldGain_1SPDepositor1RewardEvent_EarnsAllSPYield() public { - ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // B withdraws entirely makeSPWithdrawalAndClaim(B, stabilityPool.getCompoundedBoldDeposit(B)); @@ -1302,14 +1325,14 @@ contract SPTest is DevTestSetup { assertGt(pendingAggInterest, 0); uint256 expectedSpYield = SP_YIELD_SPLIT * pendingAggInterest / 1e18; - // A trove gets poked, interst minted and yield paid to SP + // A trove gets poked, interest minted and yield paid to SP applyPendingDebt(B, troveIDs.A); assertApproximatelyEqual(stabilityPool.getDepositorYieldGain(A), expectedSpYield, 1e4); } function testGetDepositorBoldGain_2SPDepositor1RewardEvent_EarnFairShareOfSPYield() public { - ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); @@ -1332,7 +1355,7 @@ contract SPTest is DevTestSetup { } function testGetDepositorBoldGain_1SPDepositor2RewardEvent_EarnsAllSPYield() public { - ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); // B withdraws entirely makeSPWithdrawalAndClaim(B, stabilityPool.getCompoundedBoldDeposit(B)); @@ -1367,7 +1390,7 @@ contract SPTest is DevTestSetup { } function testGetDepositorBoldGain_2SPDepositor2RewardEvent_EarnFairShareOfSPYield() public { - ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); @@ -1414,7 +1437,7 @@ contract SPTest is DevTestSetup { } function testGetDepositorBoldGain_2SPDepositor1Liq1FreshDeposit_EarnFairShareOfSPYield() public { - ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsWithoutOwedYieldRewards(); vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); @@ -1781,13 +1804,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(9)), // 9th storage slot where P is stored + bytes32(uint256(10)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 9 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(9)))); - assertEq(storedVal, _cheatP, "value of slot 9 is not set"); + // Confirm that storage slot 10 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); + assertEq(storedVal, _cheatP, "value of slot 10 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -1826,13 +1849,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(9)), // 9th storage slot where P is stored + bytes32(uint256(10)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 9 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(9)))); - assertEq(storedVal, _cheatP, "value of slot 9 is not set"); + // Confirm that storage slot 10 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); + assertEq(storedVal, _cheatP, "value of slot 10 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -1946,17 +1969,19 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(9)), // 9th storage slot where P is stored + bytes32(uint256(10)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 9 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(9)))); - assertEq(storedVal, _cheatP, "value of slot 9 is not set"); + // Confirm that storage slot 10 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); + assertEq(storedVal, _cheatP, "value of slot 10 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); ABCDEF memory troveIDs = _setupForPTests(); + testVars.initialBoldGainA = stabilityPool.getDepositorYieldGain(A); + testVars.initialBoldGainB = stabilityPool.getDepositorYieldGain(B); uint256 troveDebt = troveManager.getTroveEntireDebt(troveIDs.D); uint256 debtDelta = troveDebt - stabilityPool.getTotalBoldDeposits(); @@ -1974,6 +1999,7 @@ contract SPTest is DevTestSetup { uint256 spEthBalBefore = collToken.balanceOf(address(stabilityPool)); liquidate(A, troveIDs.C); + bool spTooLowAfterLiquidateA = stabilityPool.getTotalBoldDeposits() < DECIMAL_PRECISION; uint256 spEthBalAfter = collToken.balanceOf(address(stabilityPool)); testVars.spEthGain1 = spEthBalAfter - spEthBalBefore; @@ -2010,9 +2036,6 @@ contract SPTest is DevTestSetup { testVars.expectedShareOfYield1_A = getShareofSPReward(A, expectedSpYield1); testVars.expectedShareOfYield1_B = getShareofSPReward(B, expectedSpYield1); - assertGt(testVars.expectedShareOfYield1_A, 0); - assertGt(testVars.expectedShareOfYield1_B, 0); - testVars.troveDebt_D = troveManager.getTroveEntireDebt(troveIDs.D); // D makes fresh deposit so that SP can cover the liq @@ -2021,6 +2044,14 @@ contract SPTest is DevTestSetup { testVars.totalSPBeforeLiq_D = stabilityPool.getTotalBoldDeposits(); assertGt(testVars.totalSPBeforeLiq_D, testVars.troveDebt_D); + if (spTooLowAfterLiquidateA) { + testVars.expectedShareOfYield1_A = getShareofSPReward(A, expectedSpYield1); + testVars.expectedShareOfYield1_B = getShareofSPReward(B, expectedSpYield1); + } + + assertGt(testVars.expectedShareOfYield1_A, 0); + assertGt(testVars.expectedShareOfYield1_B, 0); + // D's trove liquidated spEthBalBefore = collToken.balanceOf(address(stabilityPool)); liquidate(A, troveIDs.D); @@ -2045,8 +2076,18 @@ contract SPTest is DevTestSetup { // Check all BOLD and Coll gains are as expected testVars.boldGainA = stabilityPool.getDepositorYieldGain(A); testVars.boldGainB = stabilityPool.getDepositorYieldGain(B); - assertApproximatelyEqual(testVars.expectedShareOfYield1_A, testVars.boldGainA, 1e4); - assertApproximatelyEqual(testVars.expectedShareOfYield1_B, testVars.boldGainB, 1e4); + assertApproximatelyEqual( + testVars.initialBoldGainA + testVars.expectedShareOfYield1_A, + testVars.boldGainA, + 1e4, + "A yield gain mismatch" + ); + assertApproximatelyEqual( + testVars.initialBoldGainB + testVars.expectedShareOfYield1_B, + testVars.boldGainB, + 1e4, + "B yield gain mismatch" + ); uint256 ethGainA = stabilityPool.getDepositorCollGain(A); uint256 ethGainB = stabilityPool.getDepositorCollGain(B); @@ -2063,13 +2104,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(9)), // 9th storage slot where P is stored + bytes32(uint256(10)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 9 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(9)))); - assertEq(storedVal, _cheatP, "value of slot 9 is not set"); + // Confirm that storage slot 10 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); + assertEq(storedVal, _cheatP, "value of slot 10 is not set"); // Confirm that P specfically is set console2.log(stabilityPool.P(), "stabilityPool.P()"); console2.log(_cheatP, "_cheatP"); @@ -2103,13 +2144,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(9)), // 9th storage slot where P is stored + bytes32(uint256(10)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 9 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(9)))); - assertEq(storedVal, _cheatP, "value of slot 9 is not set"); + // Confirm that storage slot 10 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); + assertEq(storedVal, _cheatP, "value of slot 10 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set");