Core Contracts

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

RToken Precision Loss Creates Invisible Deposit Threshold

Summary

RToken contract's mint function can fail silently when scaling calculations result in zero amounts. This breaks accounting invariants and could lead to lost deposits. The precision loss occurs at the ray division boundary.

When amountToMint is smaller than index, the rayDiv operation can result in zero due to division rounding. The contract correctly reverts in this case, but this creates a minimum deposit threshold that isn't clearly documented.

This precision loss means small deposits below the index value cannot be processed. For example, if the index is 1e27 (RAY), then deposits smaller than that will fail. This could affect users trying to make small deposits, especially as the index grows over time with accrued interest.

Vulnerability Details

The core of RAAC's lending system relies on precise token scaling for accurate representation of user positions. Observe how the RToken.sol contract handles deposit scaling through ray math, this is crucial for maintaining the relationship between user deposits and their corresponding shares in the lending pool.

The issue emerges in the mint() function's scaling calculation

function mint(...) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
// CHECKPOINT 1: Initial Amount Validation
if (amountToMint == 0) {
return (false, 0, 0, 0); // ✓ Correct handling of zero amounts
}
// CRITICAL POINT: Precision Loss Risk
uint256 amountScaled = amountToMint.rayDiv(index);
// When amountToMint < index (e.g., 1e18 < 1e27), this rounds to zero
if (amountScaled == 0) revert InvalidAmount();
// CHECKPOINT 2: User State Management
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0; // Track new users
// CHECKPOINT 3: Interest Accrual Calculation
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
// Calculate interest accrual based on index difference
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
// CHECKPOINT 4: State Updates
_userState[onBehalfOf].index = index.toUint128(); // Update user's index
_mint(onBehalfOf, amountToMint.toUint128()); // Mint new tokens
// Return values track mint operation results
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
}
/*
The vulnerability flow:
1. User deposits small amount → amountToMint
2. Ray division with large index → amountScaled = 0
3. Revert on zero scaled amount → Transaction fails silently
4. User cannot make small deposits as index grows
This creates an invisible minimum deposit threshold that scales with the protocol's accumulated interest.
*/

When a user deposits an amount smaller than the current liquidity index, the rayDiv operation silently rounds to zero. This means that while the contract correctly reverts on zero amounts, it creates an invisible minimum deposit threshold that scales with the index.

Think of it like trying to measure millimeters with a ruler that only shows centimeters, you lose the ability to track small movements. In the context of RAAC's lending pool, this precision loss could affect users trying to make small deposits, especially as the index grows over time with accumulated interest.

The impact becomes clear when we consider RAAC's role in real estate tokenization. The protocol aims to bring real estate on-chain with seamless accessibility, but this precision limitation creates friction for smaller position adjustments.

Impact

When we observe how the RToken.sol contract handles deposit scaling through ray math, is crucial for maintaining the relationship between user deposits and their corresponding shares in the lending pool.

Recommendations

We need to either:

  1. Make the minimum deposit threshold explicit and tied to the current index

  2. Implement scaled precision handling for small amounts

This maintains RAAC's core mission of capital efficiency while providing clear boundaries for valid operations.

function mint(...) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
// ENTRY POINT: User attempts to deposit tokens
if (amountToMint == 0) {
return (false, 0, 0, 0);
}
// VULNERABLE POINT
uint256 amountScaled = amountToMint.rayDiv(index);
// Example: amountToMint = 1e18 (1 token), index = 1e27 (RAY)
// Result: amountScaled = 1e18/1e27 = 0
if (amountScaled == 0) revert InvalidAmount();
// IMPACT: These state updates never execute for small deposits
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
// Interest calculation skipped due to early revert
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
// State never updates for small deposits
_userState[onBehalfOf].index = index.toUint128();
_mint(onBehalfOf, amountToMint.toUint128());
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
}

Needs to address the core scaling issue while maintaining the existing state management flow. A more precise solution would be

function mint(...) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
if (amountToMint == 0) return (false, 0, 0, 0);
// New precision handling
uint256 amountScaled;
if (amountToMint < index) {
// Use higher precision intermediate calculation
amountScaled = (amountToMint * 1e9).rayDiv(index);
require(amountScaled > 0, "Amount too small even with precision boost");
} else {
amountScaled = amountToMint.rayDiv(index);
}
// Continue with existing state management...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 7 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.

Give us feedback!