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 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!