From 3456d1db501bcb6931d0d19d4ea69da8b33486d9 Mon Sep 17 00:00:00 2001 From: Ed Noepel <46749157+EdNoepel@users.noreply.github.com> Date: Tue, 18 Apr 2023 08:22:03 -0400 Subject: [PATCH] Resolve DIV/0 unstaking in a burn epoch with no burn (#748) * created unit test which reproduces div/0 error upon unstake * add FIXME comment * ported tests to RC4 * brute-force zero checks (#745) * removed FIXME which was fixed --- src/RewardsManager.sol | 4 +- tests/forge/unit/Rewards/RewardsManager.t.sol | 117 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/RewardsManager.sol b/src/RewardsManager.sol index aca8dad8c..28c4b55ec 100644 --- a/src/RewardsManager.sol +++ b/src/RewardsManager.sol @@ -525,7 +525,7 @@ contract RewardsManager is IRewardsManager, ReentrancyGuard { ) = _getPoolAccumulators(ajnaPool_, nextEpoch_, epoch_); // calculate rewards earned - newRewards_ = Maths.wmul( + newRewards_ = totalInterestEarnedInPeriod == 0 ? 0 : Maths.wmul( REWARD_FACTOR, Maths.wdiv( Maths.wmul(interestEarned_, totalBurnedInPeriod), @@ -783,7 +783,7 @@ contract RewardsManager is IRewardsManager, ReentrancyGuard { (, , , uint256 bucketDeposit, ) = IPool(pool_).bucketInfo(bucketIndex_); uint256 burnFactor = Maths.wmul(totalBurned_, bucketDeposit); - uint256 interestFactor = Maths.wdiv( + uint256 interestFactor = interestEarned_ == 0 ? 0 : Maths.wdiv( Maths.WAD - Maths.wdiv(prevBucketExchangeRate, curBucketExchangeRate), interestEarned_ ); diff --git a/tests/forge/unit/Rewards/RewardsManager.t.sol b/tests/forge/unit/Rewards/RewardsManager.t.sol index 191212bc0..f5521735e 100644 --- a/tests/forge/unit/Rewards/RewardsManager.t.sol +++ b/tests/forge/unit/Rewards/RewardsManager.t.sol @@ -177,6 +177,123 @@ contract RewardsManagerTest is RewardsHelperContract { }); } + function testUnstakeTokenAfterBurnNoInterest() external { + skip(10); + ERC20Pool pool = ERC20Pool(address(_pool)); + + // deposit into a high and low bucket + deal(address(_quoteOne), _minterOne, 400 * 1e18); + changePrank(_minterOne); + _quoteOne.approve(address(_pool), type(uint256).max); + _pool.addQuoteToken(200 * 1e18, 2_000, type(uint256).max); + _pool.addQuoteToken(200 * 1e18, 4_000, type(uint256).max); + skip(1 hours); + + // draw debt between the buckets + uint256 borrowAmount = 100 * 1e18; + uint256 limitIndex = 3_000; + assertGt(_pool.depositSize(), borrowAmount); + ( + uint256 collateralToPledge + ) = _createTestBorrower(address(_pool), _borrower, borrowAmount, limitIndex); + pool.drawDebt(_borrower, borrowAmount, limitIndex, collateralToPledge); + skip(3 days); + (,,, uint256 htpIndex,,) = _poolUtils.poolPricesInfo(address(_pool)); + assertLt(htpIndex, 4_000); + + // mint LP NFT and memorialize position for only the bucket which did not earn interest + (uint256 lpBalance, ) = _pool.lenderInfo(4000, _minterOne); + assertGt(lpBalance, 0); + uint256[] memory indexes = new uint256[](1); + indexes[0] = 4_000; + uint256[] memory lpBalances = new uint256[](1); + lpBalances[0] = lpBalance; + changePrank(_minterOne); + _pool.increaseLPAllowance(address(_positionManager), indexes, lpBalances); + IPositionManagerOwnerActions.MintParams memory mintParams = IPositionManagerOwnerActions.MintParams( + _minterOne, address(_pool), keccak256("ERC20_NON_SUBSET_HASH")); + uint256 tokenId = _positionManager.mint(mintParams); + IPositionManagerOwnerActions.MemorializePositionsParams memory memorializeParams = IPositionManagerOwnerActions.MemorializePositionsParams( + tokenId, indexes + ); + _positionManager.memorializePositions(memorializeParams); + _registerLender(address(_positionManager), indexes); + skip(4 days); + + // stake rewards + _stakeToken(address(_pool), _minterOne, tokenId); + skip(7 days); + + // repay debt to accumulate some reserves + changePrank(_borrower); + pool.repayDebt(_borrower, type(uint256).max, collateralToPledge, _borrower, MAX_FENWICK_INDEX); + skip(2 hours); + + // burn + changePrank(_bidder); + pool.kickReserveAuction(); + skip(11 hours); + _ajnaToken.approve(address(_pool), type(uint256).max); + (,, uint256 curClaimableReservesRemaining,,) = _poolUtils.poolReservesInfo(address(_pool)); + _pool.takeReserves(curClaimableReservesRemaining); + + // unstake with no interest earned + changePrank(_minterOne); + vm.expectEmit(true, true, true, true); + emit Unstake(_minterOne, address(_pool), tokenId); + _rewardsManager.unstake(tokenId); + assertEq(PositionManager(address(_positionManager)).ownerOf(tokenId), _minterOne); + } + + function testUnstakeNoBurn() external { + skip(10); + ERC20Pool pool = ERC20Pool(address(_pool)); + + // deposit into some buckets and mint an NFT + uint256[] memory indexes = new uint256[](3); + indexes[0] = 2000; + indexes[1] = 2500; + indexes[2] = 3000; + uint256 tokenId = _mintAndMemorializePositionNFT({ + indexes: indexes, + minter: _minterOne, + mintAmount: 1_000 * 1e18, + pool: address(_pool) + }); + + // draw debt + uint256 borrowAmount = 1_500 * 1e18; + uint256 limitIndex = 2_500; + assertEq(_pool.depositIndex(borrowAmount), limitIndex); + assertGt(_pool.depositSize(), borrowAmount); + ( + uint256 collateralToPledge + ) = _createTestBorrower(address(_pool), _borrower, borrowAmount, limitIndex); + pool.drawDebt(_borrower, borrowAmount, limitIndex, collateralToPledge); + skip(3 days); + + // stake rewards + _stakeToken(address(_pool), _minterOne, tokenId); + skip(7 days); + + // repay debt to accumulate some reserves + changePrank(_borrower); + pool.repayDebt(_borrower, type(uint256).max, collateralToPledge, _borrower, MAX_FENWICK_INDEX); + skip(2 hours); + + // start auction, but no burn + changePrank(_bidder); + pool.kickReserveAuction(); + skip(11 hours); + + // unstake + changePrank(_minterOne); + vm.expectEmit(true, true, true, true); + emit Unstake(_minterOne, address(_pool), tokenId); + _rewardsManager.unstake(tokenId); + assertEq(PositionManager(address(_positionManager)).ownerOf(tokenId), _minterOne); + } + function testUpdateExchangeRatesAndClaimRewards() external { skip(10);