Core Contracts

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

Users can withdraw reserve tokens without burning RTokens

Summary

In the RToken#burn(), amountScaled is not explicitly computed as amount.rayDiv(index), and there is no check to ensure it is non-zero. If amount < liquidityIndex, the computed amountScaled can be zero.

This can lead to incorrect token burning behavior and allows users to receive reserve tokens without actually burning any RTokens.

Vulnerability Details

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
...
@> uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
@> _burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
@> IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}
function _update(address from, address to, uint256 amount) internal override {
// Scale amount by normalized income for all operations (mint, burn, transfer)
@> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

Incorrect Computation of amountScaled

The function multiplies by index instead of dividing:

uint256 amountScaled = amount.rayMul(index); // ❌ Incorrect

This overestimates the burn amount when index > 1e27 (WadRay precision).

User Receives Reserve Tokens Without Burning RTokens.

If amount < liquidityIndex, then: amountScaled = 0. The user receives amount of reserve tokens and no RTokens are burned.

Impact

Users can receive reserve tokens without reducing their RToken balance.

Thus users can exploit this to drain liquidity.

Tools Used

manual

Recommendations

Modify burn() to:

  1. Properly compute amountScaled using .rayDiv(index).

  2. Ensure amountScaled is not zero.

  3. Revert if the burn amount is too small.

function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
- uint256 amountScaled = amount.rayMul(index);
+ uint256 amountScaled = amount.rayDiv(index);
+ if (amountScaled == 0) revert InvalidAmount();
_userState[from].index = index.toUint128();
_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 about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

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

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

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

Support

FAQs

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