Skip to content

Commit

Permalink
Merge pull request #97 from Ion-Protocol/jun/PAG-H-01
Browse files Browse the repository at this point in the history
H-01 Fix [PAG]
  • Loading branch information
junkim012 authored May 16, 2024
2 parents 2074642 + 597dbf9 commit 0c3fbb4
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 1 deletion.
5 changes: 4 additions & 1 deletion src/IonPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,10 @@ contract IonPool is PausableUpgradeable, RewardToken {
Ilk storage ilk = $.ilks[ilkIndex];

uint256 _totalNormalizedDebt = ilk.totalNormalizedDebt;
if (_totalNormalizedDebt == 0 || block.timestamp == ilk.lastRateUpdate) {
// Because all interest that would have accrued during a pause is
// cancelled upon `unpause`, we return zero interest while markets are
// paused.
if (_totalNormalizedDebt == 0 || block.timestamp == ilk.lastRateUpdate || paused()) {
// Unsafe cast OK
// block.timestamp - ilk.lastRateUpdate will almost always be 0
// here. The exception is on first borrow.
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IIonPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,5 @@ interface IIonPool {
function getTotalUnderlyingClaims() external view returns (uint256);
function getUnderlyingClaimOf(address user) external view returns (uint256);
function extsload(bytes32 slot) external view returns (bytes32);
function balanceOfUnaccrued(address user) external view returns (uint256);
}
8 changes: 8 additions & 0 deletions src/token/RewardToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ abstract contract RewardToken is
return $._normalizedBalances[user].rayMulDown($.supplyFactor + totalSupplyFactorIncrease);
}

/**
* @dev Current claim of the underlying token without accounting for interest to be accrued.
*/
function balanceOfUnaccrued(address user) public view returns (uint256) {
RewardTokenStorage storage $ = _getRewardTokenStorage();
return $._normalizedBalances[user].rayMulDown($.supplyFactor);
}

/**
* @dev Accounting is done in normalized balances
* @param user to get normalized balance of
Expand Down
61 changes: 61 additions & 0 deletions test/unit/concrete/IonPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,67 @@ contract IonPool_InterestTest is IonPoolSharedSetup, IIonPoolEvents {
previousRates[i] = rate;
}
}

function test_AccrueInterestWhenPaused() public {
uint256 collateralDepositAmount = 10e18;
uint256 normalizedBorrowAmount = 5e18;

for (uint8 i = 0; i < lens.ilkCount(iIonPool); i++) {
vm.prank(borrower1);
ionPool.depositCollateral(i, borrower1, borrower1, collateralDepositAmount, new bytes32[](0));

uint256 rate = ionPool.rate(i);
uint256 liquidityBefore = lens.liquidity(iIonPool);

assertEq(ionPool.collateral(i, borrower1), collateralDepositAmount);
assertEq(underlying.balanceOf(borrower1), normalizedBorrowAmount.rayMulDown(rate) * i);

vm.prank(borrower1);
ionPool.borrow(i, borrower1, borrower1, normalizedBorrowAmount, new bytes32[](0));

uint256 liquidityRemoved = normalizedBorrowAmount.rayMulDown(rate);

assertEq(ionPool.normalizedDebt(i, borrower1), normalizedBorrowAmount);
assertEq(lens.totalNormalizedDebt(iIonPool, i), normalizedBorrowAmount);
assertEq(lens.liquidity(iIonPool), liquidityBefore - liquidityRemoved);
assertEq(underlying.balanceOf(borrower1), normalizedBorrowAmount.rayMulDown(rate) * (i + 1));
}

vm.warp(block.timestamp + 1 hours);

ionPool.pause();

uint256 rate0AfterPause = ionPool.rate(0);
uint256 rate1AfterPause = ionPool.rate(1);
uint256 rate2AfterPause = ionPool.rate(2);

uint256 supplyFactorAfterPause = ionPool.supplyFactor();
uint256 lenderBalanceAfterPause = ionPool.balanceOf(lender2);

vm.warp(block.timestamp + 365 days);

(
uint256 totalSupplyFactorIncrease,
uint256 treasuryMintAmount,
uint104[] memory rateIncreases,
uint256 totalDebtIncrease,
uint48[] memory timestampIncreases
) = ionPool.calculateRewardAndDebtDistribution();

assertEq(totalSupplyFactorIncrease, 0, "no supply factor increase");
assertEq(treasuryMintAmount, 0, "no treasury mint amount");
for (uint8 i = 0; i < lens.ilkCount(iIonPool); i++) {
assertEq(rateIncreases[i], 0, "no rate increase");
assertEq(timestampIncreases[i], 365 days, "no timestamp increase");
}
assertEq(totalDebtIncrease, 0, "no total debt increase");

assertEq(ionPool.balanceOf(lender2), lenderBalanceAfterPause, "lender balance doesn't change");
assertEq(ionPool.supplyFactor(), supplyFactorAfterPause, "supply factor doesn't change");
assertEq(ionPool.rate(0), rate0AfterPause, "rate 0 doesn't change");
assertEq(ionPool.rate(1), rate1AfterPause, "rate 1 doesn't change");
assertEq(ionPool.rate(2), rate2AfterPause, "rate 2 doesn't change");
}
}

contract IonPool_AdminTest is IonPoolSharedSetup {
Expand Down
132 changes: 132 additions & 0 deletions test/unit/concrete/vault/Vault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,138 @@ contract VaultERC4626ExternalViews is VaultSharedSetup {
super.setUp();
}

function test_TotalAssetsWithSinglePausedIonPool() public {
weEthIonPool.updateSupplyCap(type(uint256).max);
weEthIonPool.updateIlkDebtCeiling(0, type(uint256).max);

supply(address(this), weEthIonPool, 1000e18);
borrow(address(this), weEthIonPool, weEthGemJoin, 100e18, 70e18);

uint256[] memory allocationCaps = new uint256[](3);
allocationCaps[0] = 20e18;
allocationCaps[1] = 0;
allocationCaps[2] = 0;

vm.prank(OWNER);
vault.updateAllocationCaps(markets, allocationCaps);

uint256 depositAmt = 10e18;
setERC20Balance(address(BASE_ASSET), address(this), depositAmt);
vault.deposit(depositAmt, address(this));

assertEq(weEthIonPool.balanceOf(address(vault)), depositAmt, "weEthIonPool balance");

// Pause the weEthIonPool, stop accruing interest
weEthIonPool.pause();
assertTrue(weEthIonPool.paused(), "weEthIonPool is paused");

vm.warp(block.timestamp + 365 days);

assertEq(weEthIonPool.balanceOf(address(vault)), depositAmt, "weEthIonPool accrues interest");
assertEq(
weEthIonPool.balanceOfUnaccrued(address(vault)),
weEthIonPool.balanceOf(address(vault)),
"weEthIonPool unaccrued balance"
);

uint256 totalAssets = vault.totalAssets();
assertEq(totalAssets, depositAmt, "total assets with paused IonPool does not include interest");

// When unpaused, should now accrue interest
weEthIonPool.unpause();
vm.warp(block.timestamp + 365 days);

assertGt(weEthIonPool.balanceOf(address(vault)), depositAmt, "weEthIonPool accrues interest");
assertGt(
weEthIonPool.balanceOf(address(vault)),
weEthIonPool.balanceOfUnaccrued(address(vault)),
"weEthIonPool unaccrued balance"
);

assertGt(vault.totalAssets(), depositAmt, "total assets with paused IonPool does not include interest");
}

function test_TotalAssetsWithMultiplePausedIonPools() public {
// Make sure every pool has debt to accrue interest from
uint256 initialSupplyAmt = 1000e18;
weEthIonPool.updateSupplyCap(type(uint256).max);
rsEthIonPool.updateSupplyCap(type(uint256).max);
rswEthIonPool.updateSupplyCap(type(uint256).max);

weEthIonPool.updateIlkDebtCeiling(0, type(uint256).max);
rsEthIonPool.updateIlkDebtCeiling(0, type(uint256).max);
rswEthIonPool.updateIlkDebtCeiling(0, type(uint256).max);

supply(address(this), weEthIonPool, initialSupplyAmt);
borrow(address(this), weEthIonPool, weEthGemJoin, 100e18, 70e18);

supply(address(this), rsEthIonPool, initialSupplyAmt);
borrow(address(this), rsEthIonPool, rsEthGemJoin, 100e18, 70e18);

supply(address(this), rswEthIonPool, initialSupplyAmt);
borrow(address(this), rswEthIonPool, rswEthGemJoin, 100e18, 70e18);

uint256[] memory allocationCaps = new uint256[](3);
uint256 weEthIonPoolAmt = 10e18;
uint256 rsEthIonPoolAmt = 20e18;
uint256 rswEthIonPoolAmt = 30e18;
allocationCaps[0] = weEthIonPoolAmt;
allocationCaps[1] = rsEthIonPoolAmt;
allocationCaps[2] = rswEthIonPoolAmt;

vm.prank(OWNER);
vault.updateAllocationCaps(markets, allocationCaps);

uint256 depositAmt = 60e18;
setERC20Balance(address(BASE_ASSET), address(this), depositAmt);
vault.deposit(depositAmt, address(this));

assertEq(weEthIonPool.balanceOf(address(vault)), weEthIonPoolAmt, "weEthIonPool balance");
assertEq(rsEthIonPool.balanceOf(address(vault)), rsEthIonPoolAmt, "rsEthIonPool balance");
assertEq(rswEthIonPool.balanceOf(address(vault)), rswEthIonPoolAmt, "rswEthIonPool balance");

weEthIonPool.pause();
// NOTE rsEthIonPool is not paused
rswEthIonPool.pause();

assertTrue(weEthIonPool.paused(), "weEthIonPool is paused");
assertFalse(rsEthIonPool.paused(), "rsEthIonPool is not paused");
assertTrue(rswEthIonPool.paused(), "rswEthIonPool is paused");

vm.warp(block.timestamp + 365 days);

// The 'unaccrued' values should not change
assertEq(weEthIonPool.balanceOfUnaccrued(address(vault)), weEthIonPoolAmt, "weEthIonPool balance");
assertEq(rsEthIonPool.balanceOfUnaccrued(address(vault)), rsEthIonPoolAmt, "rsEthIonPool balance");
assertEq(rswEthIonPool.balanceOfUnaccrued(address(vault)), rswEthIonPoolAmt, "rswEthIonPool balance");

// When paused, the unaccrued and accrued balanceOf should be the same
assertEq(
weEthIonPool.balanceOf(address(vault)),
weEthIonPool.balanceOfUnaccrued(address(vault)),
"weEthIonPool balance increases"
);
assertEq(
rswEthIonPool.balanceOf(address(vault)),
rswEthIonPool.balanceOfUnaccrued(address(vault)),
"rswEthIonPool balance increases"
);

// When not paused, the accrued balanceOf should be greater
assertGt(
rsEthIonPool.balanceOf(address(vault)),
rsEthIonPool.balanceOfUnaccrued(address(vault)),
"rsEthIonPool balance does not change"
);

uint256 expectedTotalAssets = weEthIonPool.balanceOfUnaccrued(address(vault))
+ rsEthIonPool.balanceOf(address(vault)) + rswEthIonPool.balanceOfUnaccrued(address(vault));

assertEq(
vault.totalAssets(), expectedTotalAssets, "total assets without accounting for interest in paused IonPools"
);
}

// --- Max ---
// Get max and submit max transactions

Expand Down

0 comments on commit 0c3fbb4

Please sign in to comment.