Core Contracts

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

liquidateBorrower does not update lending Pool state before fetch NormalizedDebt

Summary

StabilityPool::liquidateBorrower fetch NormalizedDebt from lending pool to calcualte the **scaledUserDebt , ** however lendingPool.updateState() is not called before fetch this vaule , which lead to fected NormalizedDebt out-of-date , result in transaction revert due to `ERC20InsufficientAllowance` error

Vulnerability Details

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());//@audit not update usage.Index.
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);

As we know , getNormalizedDebt() == reserve.usageIndex ,

// Update usage index (debt index) using compounded interest
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta, <@
reserve.usageIndex
);

reserve.usageIndex is updated according to timeDelta which changes every seconds.

Test:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "contracts/core/collectors/FeeCollector.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/veRAACToken.sol";
import "forge-std/Test.sol";
import "contracts/mocks/core/oracles/TestRAACHousePriceOracle.sol";
import "contracts/mocks/core/tokens/crvUSDToken.sol";
import "contracts/mocks/core/tokens/MockUSDC.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/DebtToken.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "forge-std/Console2.sol";
import "contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/core/pools/StabilityPool/NFTLiquidator.sol";
import "contracts/core/pools/StabilityPool/MarketCreator.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract Pool2 is Test{
RAACToken public raccToken;
veRAACToken public veraacToken;
crvUSDToken public crv;
RToken public rToken;
DebtToken public debtToken;
RAACNFT public raccNFT;
TestRAACHousePriceOracle public oracle;
RAACHousePrices public housePrice;
MockUSDC public usdc;
LendingPool public pool;
NFTLiquidator nftLq;
StabilityPool sbPool;
TransparentUpgradeableProxy proxy;
MarketCreator public market;
uint256 NFTTokenId = 1;
address alice = address(0x1001);
address bob = address(0x1002);
address candy = address(0x1003);
function setUp() public {
crv = new crvUSDToken(address(this));
rToken = new RToken("rt","rt",address(this),address(crv));
debtToken = new DebtToken("db","db",address(this));
address router;
usdc = new MockUSDC(1_000_000e6);
housePrice = new RAACHousePrices(address(this));
oracle = new TestRAACHousePriceOracle(router,bytes32('1'),address(housePrice));
raccNFT = new RAACNFT(address(usdc),address(housePrice),address(this));
pool = new LendingPool(address(crv),address(rToken),address(debtToken),address(raccNFT),address(housePrice),1e26);
rToken.setReservePool(address(pool));
housePrice.setOracle(address(this));
debtToken.setReservePool(address(pool));
nftLq = new NFTLiquidator(address(crv),address(raccNFT),address(this),50);
sbPool = new StabilityPool(address(this));
//add proxy.
proxy = new TransparentUpgradeableProxy(address(sbPool),address(this),"");
raccToken = new RAACToken(address(this),1_000,1_000);
veraacToken = new veRAACToken(address(raccToken));
market = new MarketCreator(address(this),address(raccToken),address(crv));
StabilityPool(address(proxy)).initialize(address(rToken),address(rToken),address(raccToken),address(this),address(crv),address(pool));
}
function testLiquidateBorrower() public {
pool.setStabilityPool(address(proxy));
crv.mint(address(proxy),10e18);
//alice deposit crv to pool.
crv.mint(alice,100e18);
vm.startPrank(alice);
crv.approve(address(pool), 100e18);
pool.deposit(100e18);
vm.stopPrank();
//bob mint nft.
housePrice.setHousePrice(NFTTokenId, 10e18);
usdc.mint(bob,10e18);
vm.startPrank(bob);
usdc.approve(address(raccNFT),10e18);
raccNFT.mint(NFTTokenId, 10e18);
raccNFT.approve(address(pool), NFTTokenId);
//bob deposit NFT.
pool.depositNFT(NFTTokenId);
//bob borrow.
pool.borrow(5e18);
vm.stopPrank();
//price change.
housePrice.setHousePrice(NFTTokenId, 1e18);
//init lq.
pool.initiateLiquidation(bob);
//skip grace period.
skip(4 days);
//stable.
StabilityPool(address(proxy)).liquidateBorrower(bob);
}

Out:

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 9.38ms (3.87ms CPU time)
Ran 1 test suite in 146.54ms (9.38ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in tests/Pool2.t.sol:Pool2
[FAIL: ERC20InsufficientAllowance(0xA4AD4f68d0b91CFD19687c881e50f3A00242828c, 5000000000000000000 [5e18], 5001626976976766812 [5.001e18])] testLiquidateBorrower() (gas: 1216694)
Encountered a total of 1 failing tests, 0 tests succeeded

Impact

dos

Tools Used

Foundry

Recommendations

add lendingPool.updateState() before fetch usage.Index

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.