Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

H-01 Fix [PAG] #97

Merged
merged 2 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1425,6 +1425,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
5 changes: 3 additions & 2 deletions test/unit/fuzz/vault/Vault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ contract VaultWithYieldAndFee_Fuzz is VaultSharedSetup {
// expected resulting state

uint256 expectedFeeAssets = interestAccrued.mulDiv(feePerc, RAY);
uint256 expectedFeeShares =
expectedFeeAssets.mulDiv(vault.totalSupply(), newTotalAssets - expectedFeeAssets, Math.Rounding.Floor);
uint256 expectedFeeShares = expectedFeeAssets.mulDiv(
vault.totalSupply() + 1, newTotalAssets - expectedFeeAssets + 1, Math.Rounding.Floor
);

uint256 expectedUserAssets = prevUserAssets + interestAccrued.mulDiv(RAY - feePerc, RAY);

Expand Down
Loading