Core Contracts

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

Incorrect Dust Calculation in RToken

01. Relevant GitHub Links

02. Summary

In the RToken contract, there is a function called calculateDustAmount used to compute the dust balance in the contract. This dust is then transferred using transferAccruedDust to claim protocol fees. However, the calculation in calculateDustAmount is incorrect, causing the function to return zero dust even when a non-zero dust balance actually exists. As a result, the transferAccruedDust function fails and prevents the protocol from collecting fees.

03. Vulnerability Details

Inside calculateDustAmount, the contract balance is scaled by performing an additional division, while the total supply is also scaled again. Specifically:

  • contractBalance applies a division with rayDiv, reducing the value.

  • currentTotalSupply returns a value that is already de-scaled, but then the code applies an additional rayMul to it.

  • This leads to an incorrect comparison between contractBalance and totalRealBalance, causing the dust amount calculation to be zero despite existing dust.

/**
* @notice Calculate the dust amount in the contract
* @return The amount of dust in the contract
*/
function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
@> uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
// Calculate the total real obligations to the token holders
uint256 currentTotalSupply = totalSupply();
// Calculate the total real balance equivalent to the total supply
@> uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}

04. Impact

Because the dust calculation always wrong, transferAccruedDust reverts when it attempts to transfer dust. Or only receive the smaller value. This prevents the contract owner or protocol from retrieving the accrued fee portion, resulting in lost fee revenue.

05. Proof of Concept

When running the test test_poc_calculateDustAmount with Foundry (using forge test --mt test_poc_calculateDustAmount -vv), the log output shows that calculateDustAmount returns zero, even though there is a real dust balance of 3300802636529422469. Because the function returns zero, transferAccruedDust fails and no dust is transferred.

calculated dust amount: 0
rTokenContractTotalBalance: 55488302636529422469
rTokenContractTotalSupply: 52187500000000000000
real dust amount: 3300802636529422469
transferAccruedDust failed
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {crvUSDToken} from "src/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "src/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "src/core/tokens/RAACNFT.sol";
import {IRToken, RToken} from "src/core/tokens/RToken.sol";
import {DebtToken} from "src/core/tokens/DebtToken.sol";
import {LendingPool} from "src/core/pools/LendingPool/LendingPool.sol";
import {ReserveLibrary} from "src/libraries/pools/ReserveLibrary.sol";
contract BaseTest is Test {
crvUSDToken public crvUSDTokenInstance;
RAACHousePrices public raacHousePricesInstance;
RAACNFT public raacNFTInstance;
RToken public rTokenInstance;
DebtToken public debtTokenInstance;
LendingPool public lendingPoolInstance;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hyuunn = makeAddr("hyuunn");
function setUp() public {
// crvUSDToken deploy
crvUSDTokenInstance = new crvUSDToken(address(this));
// raacHousePrices deploy
raacHousePricesInstance = new RAACHousePrices(address(this));
raacHousePricesInstance.setOracle(address(this));
// raacNFT deploy
raacNFTInstance = new RAACNFT(
address(crvUSDTokenInstance),
address(raacHousePricesInstance),
address(this)
);
_mintRaacNFT();
rTokenInstance = new RToken(
"RToken",
"RTK",
address(this),
address(crvUSDTokenInstance)
);
debtTokenInstance = new DebtToken("DebtToken", "DEBT", address(this));
lendingPoolInstance = new LendingPool(
address(crvUSDTokenInstance),
address(rTokenInstance),
address(debtTokenInstance),
address(raacNFTInstance),
address(raacHousePricesInstance),
0.1e27
);
rTokenInstance.setReservePool(address(lendingPoolInstance));
debtTokenInstance.setReservePool(address(lendingPoolInstance));
}
function _mintRaacNFT() internal {
// housePrices setting
raacHousePricesInstance.setHousePrice(0, 100e18);
raacHousePricesInstance.setHousePrice(1, 50e18);
raacHousePricesInstance.setHousePrice(2, 150e18);
// crvUSDToken mint
deal(address(crvUSDTokenInstance), alice, 1000e18);
deal(address(crvUSDTokenInstance), bob, 1000e18);
deal(address(crvUSDTokenInstance), hyuunn, 1000e18);
// raacNFT mint
vm.startPrank(alice);
crvUSDTokenInstance.approve(address(raacNFTInstance), 100e18 + 1);
raacNFTInstance.mint(0, 100e18 + 1);
vm.stopPrank();
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(raacNFTInstance), 50e18 + 1);
raacNFTInstance.mint(1, 50e18 + 1);
vm.stopPrank();
}
function test_poc_calculateDustAmount() public {
// 0. set protocolFeeRate as 50%
lendingPoolInstance.setProtocolFeeRate(0.5e27);
// 1. bob deposit, depositNFT, borrow
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(lendingPoolInstance), 500e18);
lendingPoolInstance.deposit(50e18);
raacNFTInstance.approve(address(lendingPoolInstance), 1);
lendingPoolInstance.depositNFT(1);
lendingPoolInstance.borrow(10e18);
// 2. Over time, bob repays the debt.
vm.warp(block.timestamp + 365 days * 10);
crvUSDTokenInstance.approve(address(lendingPoolInstance), type(uint256).max);
lendingPoolInstance.repay(type(uint256).max);
vm.stopPrank();
// 3. The accrued fee is collected by admin by calling the transferAccruedDust function.
// Calculate the fee incurred through the calculateDustAmount function.
console.log("calculated dust amount: ", rTokenInstance.calculateDustAmount());
uint256 rTokenContractTotalBalance = crvUSDTokenInstance.balanceOf(address(rTokenInstance));
uint256 rTokenContractTotalSupply = rTokenInstance.totalSupply();
console.log("rTokenContractTotalBalance: ", rTokenContractTotalBalance);
console.log("rTokenContractTotalSupply: ", rTokenContractTotalSupply);
console.log("real dust amount: ", rTokenContractTotalBalance - rTokenContractTotalSupply);
vm.expectRevert(IRToken.NoDust.selector);
lendingPoolInstance.transferAccruedDust(address(this), type(uint256).max);
console.log("transferAccruedDust failed");
}
}

06. Tools Used

Manual Code Review and Foundry

07. Recommended Mitigation

/**
* @notice Calculate the dust amount in the contract
* @return The amount of dust in the contract
*/
function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
- uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// Calculate the total real obligations to the token holders
uint256 currentTotalSupply = totalSupply();
// Calculate the total real balance equivalent to the total supply
- uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 totalRealBalance = currentTotalSupply;
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}
Updates

Lead Judging Commences

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

RToken::calculateDustAmount incorrectly applies liquidity index, severely under-reporting dust amounts and permanently trapping crvUSD in contract

Support

FAQs

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