Core Contracts

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

Debt Token Burn Scaling Creates "Free" Debt Repayment

Summary

DebtToken's burn function fails to properly scale balances when burning debt tokens. This leads to incorrect debt accounting that could impact the entire lending protocol's solvency calculations when a user burns debt tokens with a high index value. The contract's scaling calculations for burning tokens produce incorrect results because the burn amount isn't properly scaled relative to the current index.

The scaled balance reduction is expected to match the burn amount divided by the index. However, the actual implementation in DebtToken.sol shows the scaling calculation isn't handling the ray math precision correctly.

The whitepaper specifically mentions scaled accounting for debt positions, but this wasn't fully implemented in the burn logic.

Vulnerability Details

Imagine a mortgage system where borrowers could repay their loans using historical rates, completely ignoring years of accumulated interest, so this is exactly what's happening in RAAC's DebtToken contract.

It unfolds in the heart of RAAC's lending mechanism, where the DebtToken represents real estate-backed debt positions. When users borrow against their tokenized properties, their debt should grow with interest over time through an index multiplier, just like a traditional mortgage. However, the current implementation creates a dangerous shortcut.

Let's walk through a real scenario: Alice borrows 100,000 RAAC tokens against her tokenized property. After a year, the currentIndex has grown to 1.5, meaning her actual debt is now 150,000 RAAC. But here's where things break, when Alice calls burn() to repay 100,000 tokens, the contract accepts this as full repayment for that amount, completely ignoring the accumulated interest. She just saved 50,000 RAAC through a simple math error.

The root cause lives in this line: /DebtToken.sol#burn()

_burn(from, amount.toUint128()); // Should be amount.rayDiv(currentIndex)

This flaw ripples through the entire RAAC ecosystem. The protocol's real estate lending system depends on accurate debt accounting to maintain proper collateralization ratios and trigger liquidations when needed. With this bug, the system progressively loses track of real debt obligations, potentially leading to protocol insolvency as users exploit this "discount" mechanism.

function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
// 🔍 Basic input validation
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
// 💰 Get user's current debt balance
uint256 userBalance = balanceOf(from);
// 📈 Calculate interest accrual
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount; // 🚩 Redundant assignment
}
// ⏰ Update user's index
_userState[from].index = index.toUint128();
// ⚖️ Cap burn amount to user balance
if(amount > userBalance){
amount = userBalance;
}
// 🔢 Scale amount by current index
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
// 💥 Critical Bug: Burns unscaled amount instead of scaled amount
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}

See the key vulnerability is in the final _burn() call where it uses the unscaled amount instead of the amountScaled value, creating the debt repayment discrepancy we identified.

Impact

The core of RAAC's lending system uses an interest-bearing debt token where balances scale with a rate index over time. Think of it like compound interest on a mortgage, your actual debt grows even though the number of tokens stays the same, users could repay less debt than they actually owe. The contract's scaling mechanism, which should adjust debt based on accumulated interest (like any real-world mortgage), fails to account for the current interest rate index during burns.

The bug emerges in the burn function

function burn(address from, uint256 amount) external {
// Fails to scale amount by current index
_burn(from, amount.toUint128());
}

When a user repays their debt (burns tokens), the contract should divide the repayment amount by the current index to get the correct scaled balance reduction. Instead, it burns the raw amount, leading to incorrect debt accounting.

Real World Impact This isn't just a math error - it directly affects the protocol's ability to track real estate debt positions accurately. For example, if Alice borrows against her tokenized property with an initial index of 1.0, and the index later grows to 1.5, burning 100 tokens should only reduce her scaled balance by ~67. The current implementation would incorrectly reduce it by 100, essentially giving her a ~33% discount on her debt.

Recommendations

changing _burn(from, amount.toUint128()) to _burn(from, amountScaled.toUint128()), ensuring the debt reduction properly accounts for the accumulated interest through the index scaling.

function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
// 🔍 Input validation
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
// 💰 Current debt position
uint256 userBalance = balanceOf(from);
// 📈 Interest calculation
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
}
// ⏰ Update index
_userState[from].index = index.toUint128();
// ⚖️ Balance cap
if(amount > userBalance){
amount = userBalance;
}
// 🔢 Correct scaling calculation
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
// 🔧 Fix: Use amountScaled instead of amount
_burn(from, amountScaled.toUint128()); // <- This is the correct fix
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}
Updates

Lead Judging Commences

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

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

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

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

Support

FAQs

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

Give us feedback!