Core Contracts

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

Interest-Free Loans Through Index Manipulation in RToken

Summary

RToken's balance update can be bypassed when minting or burning tokens without properly updating the liquidity index. This allows users to manipulate their token balances without the corresponding interest accrual, breaking the economic model of the lending protocol, because any balance changes must be accompanied by a liquidity index update, because the RToken.sol contract allows minting and burning operations to proceed with a stale liquidity index.

In the normal flow, the LendingPool should update the index before any balance changes, but there's no strict enforcement of this sequence.

Vulnerability Details

RAAC bridges real estate and DeFi by tokenizing property into on-chain assets, enabling lending, borrowing, and trading through dual-gauge system. The RToken contract manages interest-bearing positions, similar to how traditional banks track growing savings deposits. However, the interest accrual mechanism allows users to bypass interest payments entirely, threatening the protocol's economic foundation. This means users can:

  • Mint tokens using old (lower) indices → Get more tokens than they should

  • Burn tokens using old indices → Repay less than they actually owe

This is what happened, the RToken contract tracks user balances that should automatically adjust with interest rates, similar to how a savings account grows over time. The liquidity index acts as our interest multiplier when it increases from 1.0 to 1.1, everyone's balances should grow by 10%.

function mint(
address caller, // Who triggered the mint
address onBehalfOf, // Who receives the tokens
uint256 amountToMint,// Raw amount in underlying asset (e.g., 100 crvUSD)
uint256 index // CRITICAL: This index value is trusted but not verified!
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
// Basic input validation, but misses critical index check
if (amountToMint == 0) {
return (false, 0, 0, 0);
}
// ISSUE POINT 1:
// amountScaled = actual_amount / index
// If index is stale (too low), amountScaled becomes larger than it should be
uint256 amountScaled = amountToMint.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
// Get user's current scaled balance
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
// ISSUE POINT 2:
// Attempts to handle interest accrual, but can be bypassed
// by providing manipulated index values
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
// Updates user's index without validating against global index
_userState[onBehalfOf].index = index.toUint128();
// ISSUE POINT 3:
// Mints tokens based on potentially manipulated calculations
_mint(onBehalfOf, amountToMint.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
}
  1. Think of index as an interest multiplier (like 1.1 for 10% interest)

  2. amountScaled represents the base amount without interest

  3. The vulnerability occurs because an attacker can provide an old, lower index to get more tokens than they should

  4. Missing validation: index should equal getLiquidityIndex() to prevent manipulation

Impact

This vulnerability allows borrowers to mint tokens at old interest rates and repay loans using outdated indices. In concrete terms, if the protocol has accumulated 10% interest, an attacker could borrow 1000 RTokens but only repay 909 tokens worth of value, a direct 91 token loss to lenders.

Tools Used

foundry

Recommendations

One line of validation prevents the entire interest-free loan exploit while preserving the contract's core functionality.

function mint(
address caller,
address onBehalfOf,
uint256 amountToMint,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
// Add index validation at the start
require(index == getLiquidityIndex(), "Index must be current");
if (amountToMint == 0) {
return (false, 0, 0, 0);
}
// Now amountScaled calculation is safe because index is verified
uint256 amountScaled = amountToMint.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
// Interest calculation becomes reliable
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].index);
}
// User state update with verified index
_userState[onBehalfOf].index = index.toUint128();
// Mint with correct amount including interest
_mint(onBehalfOf, amountToMint.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
}
Updates

Lead Judging Commences

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

Support

FAQs

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

Give us feedback!