Core Contracts

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

Liquidation Fails Due to Interest-Adjusted Debt Amount Exceeding Initial Approval

Summary

In LendingPool::finalizeLiquidation, the function attempts to transfer more crvUSD tokens than what was initially approved by the Stability Pool. This occurs because amountScaled includes accumulated interest, while the approval in liquidateBorrower uses the initial debt amount. The liquidation fails due to insufficient allowance, as the interest-adjusted debt amount exceeds the original approval.

POC

Create a foundry setup using the commands in this document:
https://book.getfoundry.sh/config/hardhat?highlight=hardhat#adding-foundry-to-a-hardhat-project

Create a raacFoundrySetup.t.sol file under the test directory and add this code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePricesMock} from "contracts/mocks/core/primitives/RAACHousePricesMock.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {MockFunctionsRouter} from "contracts/mocks/core/oracles/MockFunctionsRouter.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {MockVeToken} from "contracts/mocks/core/tokens/MockVeToken.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {MockVaultV3} from "contracts/mocks/CurveVaultMock.sol";
contract SetupContract is Test {
address public user1;
address public user2;
address public user3;
uint256 public currentBlockTimestamp = 1000 days;
address public treasury;
address public repairFund;
LendingPool public lendingPool;
crvUSDToken public _crvUSDToken;
RToken public rToken;
MockVeToken public veRToken;
DebtToken public debtToken;
RAACHousePricesMock public raacHousePrices;
RAACNFT public raacNFT;
RAACHousePriceOracle public raacHousePriceOracle;
FeeCollector public feeCollector;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
DEToken public deToken;
RAACToken public raacToken;
MockVaultV3 public curveVault;
uint256 public constant INITIAL_PRIME_RATE = 1e26;
function setUp() external {
vm.warp(currentBlockTimestamp);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
stabilityPool = new StabilityPool(address(this));
veRToken = new MockVeToken();
_crvUSDToken = new crvUSDToken(address(this));
rToken = new RToken("rtoken", "rtk", address(this), address(_crvUSDToken)); // reserve pool
raacToken = new RAACToken(address(this), 0, 0);
deToken = new DEToken("deToken", "detk", address(this), address(rToken)); // setStabilityPool;
debtToken = new DebtToken("debtToken", "dtk", address(this)); //reservePool
raacHousePrices = new RAACHousePricesMock();
raacNFT = new RAACNFT(address(_crvUSDToken), address(raacHousePrices), address(this));
raacHousePriceOracle = new RAACHousePriceOracle(
address(new MockFunctionsRouter()), bytes32(bytes("fun-ethereum-mainnet-1")), address(this)
);
curveVault = new MockVaultV3(address(_crvUSDToken), "vault", "vv");
feeCollector = new FeeCollector(address(rToken), address(veRToken), treasury, repairFund, address(this));
lendingPool = new LendingPool(
address(_crvUSDToken),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
//lendingPool.setCurveVault(address(curveVault));
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(_crvUSDToken),
address(lendingPool)
);
raacToken.setMinter(address(raacMinter));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
lendingPool.setStabilityPool(address(stabilityPool));
_crvUSDToken.mint(user1, 10000e18);
_crvUSDToken.mint(user2, 100e18);
_crvUSDToken.mint(user3, 1000e18);
_crvUSDToken.mint(address(stabilityPool), 100000e18);
}
function testLiquidations() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
vm.stopPrank();
raacHousePrices.setTokenPrice(1, 100e18);
raacHousePrices.setTokenPrice(2, 100e18);
vm.startPrank(user3);
_crvUSDToken.approve(address(raacNFT), 200e18);
raacNFT.mint(1, 100e18);
raacNFT.mint(2, 100e18);
raacNFT.approve(address(lendingPool), 1);
raacNFT.approve(address(lendingPool), 2);
lendingPool.depositNFT(1);
lendingPool.borrow(120e18);
vm.stopPrank();
lendingPool.initiateLiquidation(user3);
vm.warp(block.timestamp + 4 days);
stabilityPool.liquidateBorrower(user3);
}

Here is a part of the results:

│ ├─ [78018] LendingPool::finalizeLiquidation(user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec])
│ │ ├─ [50408] RAACNFT::transferFrom(LendingPool: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], StabilityPool: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 1)
│ │ │ ├─ emit Transfer(from: LendingPool: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], to: StabilityPool: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 1)
│ │ │ └─ ← [Stop]
│ │ ├─ [13331] DebtToken::burn(user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], 120047680703074231840 [1.2e20], 1000397339192285265334448368 [1e27])
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ ├─ emit Transfer(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], to: 0x0000000000000000000000000000000000000000, value: 120000000000000000000 [1.2e20])
│ │ │ ├─ emit Transfer(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], to: 0x0000000000000000000000000000000000000000, value: 120047680703074231840 [1.2e20])
│ │ │ ├─ emit Burn(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 120000000000000000000 [1.2e20], index: 1000397339192285265334448368 [1e27])
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ └─ ← [Return] 120047680703074231840 [1.2e20], 0, 120000000000000000000 [1.2e20], 47699648486278949 [4.769e16]
│ │ ├─ [969] crvUSDToken::transferFrom(StabilityPool: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], RToken: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 120047680703074231840 [1.2e20])
│ │ │ └─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
│ │ └─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
│ └─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
└─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])

Impact

The liquidation mechanism becomes non-functional because the Stability Pool's token approval doesn't account for accumulated interest on the debt. This prevents liquidations from being executed successfully, leaving bad debt in the system and blocking the proper liquidation of NFT collateral.

Recommendations

The approval in liquidateBorrower should account for potential interest accumulation. Alternatively, approve the maximum possible amount (type(uint256).max) if the Stability Pool contract is trusted to handle only legitimate liquidations.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::liquidateBorrower double-scales debt by multiplying already-scaled userDebt with usage index again, causing liquidations to fail

Support

FAQs

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

Give us feedback!