Core Contracts

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

Users can burn RTokens without paying any accrued interest by reusing their original deposit index

Summary

The RToken contract's burn function allows the liquidity index to remain unchanged when it should increase. This creates an opportunity for interest-free loans since users can burn tokens without paying accumulated interest whihc directly undermines the protocol's interest accrual mechanism.

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
// Currently the implementation allows index to remain unchanged
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
}

The function accepts an index parameter but doesn't enforce that it must be higher than the current liquidity index. This means:

  1. A user can deposit assets and receive RTokens

  2. Later burn those RTokens using the same index value from deposit

  3. Effectively avoid paying any accrued interest

The proper behavior should follow this pattern:

// Expected validation
if (index <= getLiquidityIndex()) revert InvalidIndex();
_liquidityIndex = index;

Vulnerability Details

Interest accrual serves as the foundation of the entire RAAC system, like gravity in physics, and the RToken contract has an interesting oversight that allows users to sidestep this fundamental law. Let me walk you through how this happens in the burn function's handling of the liquidity index. Think of the liquidity index as a measure of accumulated interest over time, it should only go up, never down or stay flat during burns. However, it currently accepts any index value without validation, see the code:

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
// Early return for zero amounts - not part of vulnerability
if (amount == 0) {
return (0, totalSupply(), 0);
}
// Get user's current balance with EXISTING interest applied
uint256 userBalance = balanceOf(from);
// EXPOSURE POINT 1:
// Index is accepted without validation
// Attacker can reuse their original deposit index
_userState[from].index = index.toUint128();
// Basic balance check
if(amount > userBalance){
amount = userBalance;
}
// EXPOSURE POINT 2:
// amountScaled calculation uses potentially stale index
uint256 amountScaled = amount.rayMul(index);
// EXPOSURE POINT 3:
// Redundant index assignment - shows confusion in implementation
_userState[from].index = index.toUint128();
// Burns tokens without ensuring interest was paid
_burn(from, amount.toUint128());
// Transfers underlying asset regardless of index manipulation
if (receiverOfUnderlying != address(this)) {
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}

A malicious attacker can deposit 1000 RAAC when the liquidityIndex is 1.0, wait for interest to accrue until the index reaches 1.2, then burn her tokens using her original 1.0 index. The protocol fails to capture 20% worth of interest that should have accrued.

Because of the vulnerabiity you see how a malicious attacker can deposit assets, wait for interest to accrue, then burn their RTokens using the same index from when they deposited. The protocol thinks "no interest has accrued" even though time has passed.

Historical Parallel

This vulnerability resembles the Compound v1 interest rate manipulation issues, but with a major difference, instead of manipulating rates directly, it bypasses interest accrual entirely through index reuse.

Impact

This allows users to circumvent the protocol's interest mechanism. For lending protocols, accurate interest accrual is fundamental without it, the protocol cannot properly compensate lenders or maintain economic stability. Without proper interest accrual, the protocol cannot maintain stable lending markets or fairly compensate lenders for their capital. The time value of money must be respected for the system to function properly.

Tools Used

manual

Recommendations

Consider adding index validation in the burn function

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
// FIX 1: Validate index progression
// Ensures new index is always higher than current liquidity index
// Prevents interest-free loans through index manipulation
require(index > getLiquidityIndex(), "Invalid index");
// FIX 2: Update global liquidity index first
// This affects all subsequent balance calculations
_liquidityIndex = index;
// Get user balance AFTER index update
// This ensures proper interest accrual
uint256 userBalance = balanceOf(from);
if (amount > userBalance) {
amount = userBalance;
}
// Update user state with validated index
_userState[from].index = index.toUint128();
// Burn tokens and transfer assets
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}
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!