Core Contracts

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

Use of stale values of `reserve.usageIndex` will cause `liquidateBorrower()` to fail due to not enough token approval calculation

Summary

Use of stale values of reserve.usageIndex will cause liquidateBorrower() to fail due to not enough token approval calculation. Consider calling lendingPool.updateState() before calling liquidateBorrower.

Vulnerability Details

  1. The root cause is that liquidateBorrower() in StabilityPool assumes that the state in LendingPool is up to date.

  2. In LendingPool, the state is not updated to reflect usageIndex or liquidityIndex after the borrowing has occurred.

  3. It relies on someone calling updateState() or calling another LendingPool function such as borrow or repay to update state.

  4. When liquidateBorrower() in the StabilityPool is called, it calculates scaledUserDebt as WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()) . The function is defined as such:

function getNormalizedDebt() external view returns (uint256) {
return reserve.usageIndex;
}

5.The initialValue of reserve.usageIndex 1e27. After a user borrows, this should increase reserve.usageIndex. But this is not the case. It is only after the state changing function where this change is reflected.

  1. If no changes take place, the liquidateBorrower() function will use scaledUserDebt as the approval value to send crvUSD to the LendingPool.

  2. However, scaledUserDebt will be less than the debt plus accrued interest, and the function will always fail.

  3. Since the cost of RAACNFT can be high as they are reflective of property prices, trading activity will be infrequent, and such as scenario of one borrower is likely.

Impact

Unable to liquidate underwater position because of code does not approveenough tokens to be transferred to LendingPool

Tools Used

Manual Review

PoC

Foundry PoC attached as such:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "../../contracts/libraries/math/PercentageMath.sol";
import "../../contracts/libraries/math/WadRayMath.sol";
import {BaseSetup} from "./BaseSetup.t.sol";
import "../../contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
contract NoIncentiveToRepayTest is BaseSetup {
using WadRayMath for uint256;
using PercentageMath for uint256;
using SafeCast for uint256;
using SafeERC20 for IERC20;
IERC20 mainnetUSDC = IERC20(address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48));
address mainnetUsdcCurveUSDVault = address(0x7cA00559B978CFde81297849be6151d3ccB408A9);
address curveUSDWhale = address(0x4e59541306910aD6dC1daC0AC9dFB29bD9F15c67);
address newUser = makeAddr("new user");
address newUser2 = makeAddr("new user2");
address newUser3 = makeAddr("new user3");
address newUser4 = makeAddr("new user4");
address bob = makeAddr("bob");
address charlie = makeAddr("charlie");
function setUp() public override {
string memory MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
uint256 mainnetFork = vm.createFork(MAINNET_RPC_URL, 19614507);
vm.selectFork(mainnetFork);
super.setUp();
vm.startPrank(owner);
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidationThreshold, 8000); // 80%
lendingPool.setParameter(ILendingPool.OwnerParameter.HealthFactorLiquidationThreshold, 1e18); // 1.0e18
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidationGracePeriod, 3 days);
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidityBufferRatio, 2000); // 20%
lendingPool.setParameter(ILendingPool.OwnerParameter.WithdrawalStatus, 0); // Allow withdrawals
lendingPool.setParameter(ILendingPool.OwnerParameter.CanPaybackDebt, 1); // Enable payback
// house is 1000 with 18 decimals, same as crvUSD
uint256 housePrice = 1_000_000e18;
housePrices.setHousePrice(1, housePrice);
housePrices.setHousePrice(2, housePrice);
housePrices.setHousePrice(3, housePrice);
housePrices.setHousePrice(4, housePrice);
housePrices.setHousePrice(5, housePrice);
housePrices.setHousePrice(6, housePrice);
housePrices.setHousePrice(7, housePrice);
housePrices.setHousePrice(8, housePrice);
vm.stopPrank();
vm.startPrank(curveUSDWhale);
crvUSD.transfer(newUser, 15_000_000e18);
crvUSD.transfer(newUser2, 6_000_000e18);
crvUSD.transfer(newUser3, 1_000_000e18);
crvUSD.transfer(newUser4, 5_000_000e18);
crvUSD.transfer(address(stabilityPool), 10_000_000e18);
crvUSD.transfer(bob, 3_000_000e18);
crvUSD.transfer(charlie, 3_000_000e18);
vm.stopPrank();
}
function test_notUpdatingBeforeLiquidation() public {
// Setup have some liquidity in LendingPool
vm.startPrank(newUser);
crvUSD.approve(address(lendingPool),type(uint256).max);
lendingPool.deposit(15_000_000e18);
vm.stopPrank();
// Step 1. bob will mints nft and borrows to go underwater
vm.startPrank(bob);
crvUSD.approve(address(raacNFT),type(uint256).max);
raacNFT.mint(8,1_000_000e18);
assertEq(raacNFT.ownerOf(8),bob);
raacNFT.approve(address(lendingPool), 8);
lendingPool.depositNFT(8);
uint256 initialBorrowAmount = 1_250_000e18;
lendingPool.borrow(initialBorrowAmount);
// bob is underwater
assertTrue(lendingPool.calculateHealthFactor(bob)< 1e18);
vm.stopPrank();
// Step 2: initiateLiquidation
lendingPool.initiateLiquidation(bob);
// Step 3: Skip grace period and owner/manager of StabilityPool calls liquidation
skip(3 days + 1);
vm.startPrank(owner);
vm.expectRevert(); // call fails because of too low approval if lendingPool state is not updated
stabilityPool.liquidateBorrower(bob);
}
}

BaseSetup is as such:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
// OpenZeppelin Imports
// import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
// Primitives Import
import {RAACHousePrices} from "../../contracts/core/primitives/RAACHousePrices.sol";
// Token Imports
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {IndexToken} from "../../contracts/core/tokens/IndexToken.sol";
import {LPToken} from "../../contracts/core/tokens/LPToken.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
// Collector Imports
import {Treasury} from "../../contracts/core/collectors/Treasury.sol";
import {FeeCollector} from "../../contracts/core/collectors/FeeCollector.sol";
// Governance Imports
import {Governance} from "../../contracts/core/governance/proposals/Governance.sol";
import {TimelockController} from "../../contracts/core/governance/proposals/TimelockController.sol";
// Gauge Imports
import {BaseGauge} from "../../contracts/core/governance/gauges/BaseGauge.sol";
import {GaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
// Boost Import
import {BoostController} from "../../contracts/core/governance/boost/BoostController.sol";
// Minter Imports
import {RAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {RAACReleaseOrchestrator} from "../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
// Oracle Imports
import {BaseChainlinkFunctionsOracle} from "../../contracts/core/oracles/BaseChainlinkFunctionsOracle.sol";
import {RAACHousePriceOracle} from "../../contracts/core/oracles/RAACHousePriceOracle.sol";
import {RAACPrimeRateOracle} from "../../contracts/core/oracles/RAACPrimeRateOracle.sol";
// Pool Imports
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {MarketCreator} from "../../contracts/core/pools/StabilityPool/MarketCreator.sol";
import {NFTLiquidator} from "../../contracts/core/pools/StabilityPool/NFTLiquidator.sol";
contract StakingToken is ERC20 {
constructor() ERC20("ST","ST"){
}
function mint(address account, uint256 value) public {
_mint(account, value);
}
function burn(address account, uint256 value) public {
_burn(account, value);
}
}
contract BaseSetup is Test {
IERC20 baseUSDC = IERC20(address(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913));
address baseUSDCWhale = address(0x20FE51A9229EEf2cF8Ad9E89d91CAb9312cF3b7A);
// Collectors
Treasury treasury;
Treasury repairFund;
FeeCollector feeCollector;
// Tokens
IERC20 crvUSD = IERC20(address(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E)); // main
IERC20 basecrvUSD = IERC20(address(0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93)); // base
RAACToken raacToken;
veRAACToken veRaacToken;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
address mainnetChainlinkRouter = address(0x65Dcc24F8ff9e51F10DCc7Ed1e4e2A61e6E14bd6);
//m Orchestatro
RAACReleaseOrchestrator raacReleaseOrchestrator;
RAACHousePrices housePrices;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACMinter minter;
// Governance
Governance public governance;
TimelockController public timelockController;
BoostController public boostController;
GaugeController public gaugeController;
RAACGauge public raacGauge;
RWAGauge public rwaGauge;
address owner;
address user1;
address user2;
address user3;
StakingToken stakingToken;
function setUp() public virtual {
stakingToken = new StakingToken();
owner = makeAddr("owner");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
vm.startPrank(owner);
// collectors
//
// Setup Tokens
raacToken = new RAACToken(owner, 100, 50);
veRaacToken = new veRAACToken(address(raacToken));
// veRAACToken
// RAACNFT
// DeToken
// Setup contracts
raacReleaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
housePrices = new RAACHousePrices(owner);
housePrices.setOracle(owner);
raacNFT = new RAACNFT(address(crvUSD), address(housePrices), owner);
rToken = new RToken("RToken", "RT", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
lendingPool = new LendingPool(
address(crvUSD), address(rToken), address(debtToken), address(raacNFT), address(housePrices), 0.1e27
);
console.log("lendingPool:", address(lendingPool));
treasury = new Treasury(owner);
repairFund = new Treasury(owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
stabilityPool = new StabilityPool(owner);
minter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner); // should be owner
feeCollector =
new FeeCollector(address(raacToken), address(veRaacToken), address(treasury), address(repairFund), owner);
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veRaacToken), true);
raacToken.manageWhitelist(owner, true);
raacToken.setMinter(address(owner));
raacToken.mint(user2, 1000e18);
raacToken.mint(user3, 1000e18);
feeCollector.grantRole(feeCollector.FEE_MANAGER_ROLE(), owner);
feeCollector.grantRole(feeCollector.EMERGENCY_ROLE(), owner);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
//
raacToken.setMinter(address(minter));
raacToken.transferOwnership(address(minter));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(minter),
address(crvUSD),
address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
// Governance Components
// Timelock controller
address[] memory proposers = new address[](1);
proposers[0] = owner;
address[] memory executors = new address[](1);
executors[0] = owner;
timelockController = new TimelockController(2 days, proposers, executors, owner);
// Governance
governance = new Governance(address(veRaacToken), address(timelockController));
timelockController.grantRole(timelockController.PROPOSER_ROLE(), address(governance));
timelockController.grantRole(timelockController.EXECUTOR_ROLE(), address(governance));
timelockController.grantRole(timelockController.CANCELLER_ROLE(), address(governance));
//Gauge
gaugeController = new GaugeController(address(veRaacToken));
boostController = new BoostController(address(veRaacToken));
rwaGauge = new RWAGauge(address(raacToken), address(stakingToken), address(gaugeController));
raacGauge = new RAACGauge(address(raacToken), address(stakingToken), address(gaugeController));
gaugeController.addGauge(address(rwaGauge),IGaugeController.GaugeType.RWA , 0);
gaugeController.addGauge(address(raacGauge),IGaugeController.GaugeType.RAAC , 0);
rwaGauge.grantRole(rwaGauge.CONTROLLER_ROLE(),owner);
raacGauge.grantRole(rwaGauge.CONTROLLER_ROLE(),owner);
vm.stopPrank();
}
}

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 6 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.