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();
@> 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();
@> bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
@> lendingPool.updateState();
@> lendingPool.finalizeLiquidation(userAddress);
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(){
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);
await ethers.provider.send("evm_increaseTime", [3 * 86400 + 1]);
await ethers.provider.send("evm_mine");
await stabilityPool.connect(owner).addManager(user2.address, 1n)
await crvusd.mint(user2.address, ethers.parseEther("10000"))
await crvusd.connect(user2).transfer(stabilityPool.target, ethers.parseEther("10000"))
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
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);
}