Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Unable to finalize liquidation becuase debt interest is updated incorrectly

Summary

The function StabilityPool::liquidateBorrower() allows owner or manager to finalize a liquidation. However, the debt interest incorrectly updated, which cause the function to fail because of insufficient allowance

Vulnerability Details

The function StabilityPool::liquidateBorrower() fetches position debt from LendingPool contract by calling lendingPool.getUserDebt(userAddress). However, the function logic is incorrect that the user debt is fetched before lending pool updates states, such that the call lendingPool.updateState() is called after token approval. This can cause the actual debt at the function call lendingPool.finalizeLiquidation(userAddress) is higher than the approved token amount. Hence, the function liquidateBorrower() will be fail

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
@> uint256 userDebt = lendingPool.getUserDebt(userAddress); // <<< this debt amount is not updated
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
@> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt); // << approve amount is userDebt amount
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
@> lendingPool.updateState(); // <<< update lending pool states, this can make position accrues more debt
// Call finalizeLiquidation on LendingPool
@> lendingPool.finalizeLiquidation(userAddress); // << this call will fail because of insufficient allowance
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

PoC

Add the test to file test/unit/core/pools/StabilityPool/StabilityPool.test.js

describe("Deposits", function () {
...
it.only('debt interest is updated incorrectly', async function(){
// @audit POC REPORTED debt interest is updated incorrectly
// user1 borrows
await userBorrow(user1, ethers.parseEther("100"), ethers.parseEther("100") , 1)
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await lendingPool.connect(user2).initiateLiquidation(user1.address);
// grace period passed
await ethers.provider.send("evm_increaseTime", [3 * 86400 + 1]);
await ethers.provider.send("evm_mine");
await stabilityPool.connect(owner).addManager(user2.address, 1n)
// manager prepares excessive funds
await crvusd.mint(user2.address, ethers.parseEther("10000"))
await crvusd.connect(user2).transfer(stabilityPool.target, ethers.parseEther("10000"))
// fail to liquidate
await stabilityPool.connect(user2).liquidateBorrower(user1.address);
})

Run the test and it shows

StabilityPool
Core Functionality
Deposits
1) debt interest is updated incorrectly
0 passing (2s)
1 failing
1) StabilityPool
Core Functionality
Deposits
debt interest is updated incorrectly:
Error: VM Exception while processing transaction: reverted with custom error 'ERC20InsufficientAllowance("0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", 100015356901543683049, 100030716250323046805)'
at crvUSDToken.burnFrom (contracts/mocks/core/tokens/crvUSDToken.sol:37)
at crvUSDToken.transferFrom (@openzeppelin/contracts/token/ERC20/ERC20.sol:156)
at LendingPool.functionCallWithValue (@openzeppelin/contracts/utils/Address.sol:87)
at LendingPool.verifyCallResultFromTarget (@openzeppelin/contracts/utils/Address.sol:120)
at LendingPool.functionCallWithValue (@openzeppelin/contracts/utils/Address.sol:88)
at LendingPool.functionCall (@openzeppelin/contracts/utils/Address.sol:71)
at LendingPool._callOptionalReturn (@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol:96)
at LendingPool.safeTransferFrom (@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol:45)
at LendingPool.finalizeLiquidation (contracts/core/pools/LendingPool/LendingPool.sol:576)
at StabilityPool.liquidateBorrower (contracts/core/pools/StabilityPool/StabilityPool.sol:506)

Impact

  • Unable to liquidate positions when Lending pool states are not updated before calling function liquidateBorrower()

Tools Used

Manual

Recommendations

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
+ lendingPool.updateState();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
- lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool: liquidateBorrower should call lendingPool.updateState earlier, to ensure the updated usageIndex is used in calculating the scaledUserDebt

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!