Core Contracts

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

Precision-Based Liquidation Bypass Through Health Factor Manipulation in RAAC Protocol

Relevant GitHub Links

Summary

The RAAC protocol's calculateHealthFactor function contains a critical precision vulnerability in its minimum debt check. When a user's debt approaches but doesn't quite reach zero, the function fails to properly handle the precision scaling between RAY (1e27) and regular (1e18) units, allowing positions to avoid liquidation even when they should be liquidated.

Severity: HIGH

Impact: HIGH

  • Direct financial impact through prevention of necessary liquidations

  • Affects core protocol safety mechanism

  • Potential for significant value extraction

  • Protocol-wide risk due to accumulation of bad debt

Likelihood: HIGH

  • Can be exploited through normal protocol operations

  • No special permissions required

  • Consistently reproducible

  • Low technical barrier to exploit

Impact

This vulnerability allows users to manipulate their positions to become "unliquidatable" by maintaining a minimal debt amount that triggers precision inconsistencies between RAY and regular units. When the debt is small enough (but greater than 1 wei), the health factor calculation produces inflated results, preventing legitimate liquidations even when the collateral value has significantly decreased.

Real-world Impact Analysis

  1. Financial Loss: A malicious user could lock significant collateral value while maintaining minimal debt, effectively preventing liquidation even during market crashes.

  2. Protocol Insolvency Risk: Multiple exploited positions could lead to protocol-wide insolvency if collateral values drop significantly.

  3. Market Impact: Exploiters could profit from market movements while being protected from liquidation.

Historical Context

Similar precision-based vulnerabilities have led to significant exploits:

  • Compound Finance (2020): $500k exploit due to precision handling in liquidation calculations

  • Venus Protocol (2021): $11M loss partially due to precision issues in health factor calculations

Proof of Concept (PoC)

The following test demonstrates how a user can exploit the precision vulnerability to avoid liquidation:

function testHealthFactorPrecisionExploit() public {
// Setup initial state
uint256 tokenId = 1;
uint256 initialPrice = 100e18; // 100 ETH worth
raacNFT.mint(address(this), tokenId);
mockOracle.setPrice(tokenId, initialPrice);
// Borrow maximum allowed (80% LTV)
uint256 borrowAmount = 80e18; // 80 ETH
lendingPool.borrow(borrowAmount);
// Repay almost everything except 2 wei
uint256 repayAmount = borrowAmount - 2;
lendingPool.repay(repayAmount);
// Verify tiny debt remains
uint256 remainingDebt = lendingPool.getUserDebt(address(this));
assertEq(remainingDebt, 2);
// Drop collateral value by 90% - should be liquidatable
mockOracle.setPrice(tokenId, initialPrice / 10); // Now worth 10 ETH
// Calculate health factor
uint256 healthFactor = lendingPool.calculateHealthFactor(address(this));
uint256 liquidationThreshold = lendingPool.healthFactorLiquidationThreshold();
// Position should be liquidatable but isn't
assertTrue(healthFactor > liquidationThreshold,
"Position remains healthy despite 90% collateral drop");
// Attempt to initiate liquidation fails
vm.expectRevert("HealthFactorTooHigh");
lendingPool.initiateLiquidation(address(this));
}

The vulnerability occurs because:

  1. The minimum debt check (if (userDebt < 1)) is too simplistic

  2. The interaction between RAY math (rayMul) and regular math creates precision gaps

  3. The health factor calculation doesn't properly normalize these precision differences

Recommendations

  1. Implement proper minimum debt threshold with RAY precision:

function calculateHealthFactor(address userAddress) public view returns (uint256) {
uint256 collateralValue = getUserCollateralValue(userAddress);
uint256 userDebt = getUserDebt(userAddress);
// Use RAY-based minimum threshold
uint256 constant MINIMUM_DEBT_RAY = 1e6 * 1e27; // 1 USDC in RAY
if (userDebt < MINIMUM_DEBT_RAY) {
return userDebt == 0 ? type(uint256).max : 0;
}
uint256 collateralThreshold = collateralValue.percentMul(liquidationThreshold);
return (collateralThreshold * 1e18) / userDebt;
}
  1. Add proper validation in the borrow and repay functions:

function repay(uint256 amount) external {
// ... existing code ...
uint256 remainingDebt = getUserDebt(msg.sender);
if (remainingDebt > 0 && remainingDebt < MINIMUM_DEBT_RAY) {
// Force full repayment for dust amounts
amount = remainingDebt;
}
// ... rest of repay logic ...
}

Implementation Priority

  1. Immediate (24h):

    • Add minimum debt threshold

    • Implement forced full repayment for dust amounts

    • Add event logging for threshold violations

  2. Short-term (1 week):

    • Add comprehensive precision tests

    • Implement monitoring for dust positions

    • Update documentation and risk parameters

Tools Used

  • Manual code review

  • Foundry for PoC testing

  • Echidna for property-based testing

  • Tenderly for transaction simulation

Time Spent

Total: 10 hours

  • Initial Analysis: 3h

  • Math Validation: 3h

  • PoC Development: 2h

  • Documentation: 2h

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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