Core Contracts

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

Rounding Error in rToken::calculateDustAmount allows draining of rToken assets over time

Summary

A vulnerability has been identified in the rToken::calculateDustAmount . The issue arises due to rounding errors in the rayDiv and rayMul operations, which result in a non-zero dust amount even when it should theoretically be zero. This discrepancy could lead to unintended behavior, such as incorrect dust calculations or potential exploitation by malicious actors.

Vulnerability Details

rToken::calculateDustAmount contains the following code:

/**
* @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;
}

Rounding Errors in rayDiv and rayMul:

The rayDiv and rayMul operations involve division and multiplication with large numbers (scaled by 1e27). These operations truncate fractional parts, leading to precision loss.

For example:

rayDiv(a, b) truncates the result of (a * RAY) / b.

rayMul(a, b) truncates the result of (a * b) / RAY.

Dust Calculation:

The dust amount is calculated as:

solidity
Copy
dustAmount = contractBalance - totalRealBalance;
Due to truncation in rayDiv and rayMul, contractBalance and totalRealBalance may not match exactly, resulting in a small positive dust amount even when it should be zero.

Example
Given assetBalance = 100000000009115210018n

normalizedIncome = 1000000000036460840071145071n

totalSupply = 100000000000000000000n

contractBalance Calculation:
contractBalance = assetBalance.rayDiv(normalizedIncome)
= (100000000009115210018 * 1e27) / 1000000000036460840071145071
≈ 100000000005469126011n (truncated)

totalRealBalance Calculation:
totalRealBalance = totalSupply.rayMul(normalizedIncome)
= (100000000000000000000 * 1000000000036460840071145071) / 1e27
≈ 100000000003646084007n (truncated)

Dust Amount:
dustAmount = contractBalance - totalRealBalance
= 100000000005469126011 - 100000000003646084007
= 1823042004n
This non-zero dust amount is a result of truncation in rayDiv and rayMul

As a result, when rToken::transferAccruedDust is called to transfer the accrued dust to a specified, there will be a small positive amount of dust left and when rToken::calculateDustAmount is called again after rToken::transferAccruedDust, there will be leftover dust calculated when the amount of dust should be 0. As a result, the next time dust is transfered out of the rToken, a small amount of the asset token will be transferred. Over time, this function called repeatedly will slowly drain asset tokens in the rToken contract that are needed to fulfill user withdrawals.

Proof Of Code (POC)

This test was run in LendingPool.test.js in the "Full Sequence" describe block

it("dust amount precision error", async function () {
// create obligations
await crvusd
.connect(user1)
.approve(lendingPool.target, ethers.parseEther("100"));
await lendingPool.connect(user1).deposit(ethers.parseEther("100"));
//c i donate 50 crvusd to the lending pool which should be considered as dust
const dustTransfer = ethers.parseEther("50");
await token.connect(user2).transfer(rToken.target, dustTransfer);
// Calculate dust amount
const dustAmount = await rToken.calculateDustAmount();
console.log("Dust amount:", dustAmount);
// Set up recipient and transfer dust
const dustRecipient = owner.address;
// TODO: Ensure dust case - it is 0n a lot. (NoDust())
if (dustAmount !== 0n) {
await lendingPool
.connect(owner)
.transferAccruedDust(dustRecipient, dustAmount);
}
//c after dust is transferred, the dust amount should be 0 but due to precision and rounding errors, the dust amount is positive
const newDustAmount = await rToken.calculateDustAmount();
console.log("newdustamount", newDustAmount);
//c since the calcuatedustamount calculation is flawed, if user2 transfers more dust to the contract, the new dust amount will be greater than the previous dust amount for the same amount of dust transferred
await token.connect(user2).transfer(rToken.target, dustTransfer);
const newDustAmount1 = await rToken.calculateDustAmount();
console.log("dustamount1", newDustAmount1);
assert(newDustAmount1 > newDustAmount);
await lendingPool
.connect(owner)
.transferAccruedDust(dustRecipient, newDustAmount1);
const newDustAmount2 = await rToken.calculateDustAmount();
console.log("newdustamount2", newDustAmount2);
await lendingPool
.connect(owner)
.transferAccruedDust(dustRecipient, newDustAmount2);
});

See results from test:

LendingPool
Full sequence
Dust amount: 49999999990894380485n
newdustamount 1823042003n
dustamount1 49999999999999999999n
newdustamount2 1823042004n
✔ dust amount precision error (272ms)

Note: To get the above result, the first time I ran the test, it produced a 'No Dust' error and then after running the exact same test again, this the result that occurs and when using chisel to calculate this , it checks out that there are rounding errors so if you come across the NoDust custom error, run the test again with no changes and you will get the above result

Impact

Incorrect Dust Calculations:

The rounding errors cause the dust amount to be non-zero even when it should theoretically be zero. This could lead to incorrect dust calculations and unintended behavior in the contract.

Loss of Funds:

If the dust amount is transferred out of the contract, the rounding errors could result in a small but continuous loss of funds.

Tools Used

Manual Review, Hardhat

Recommendations

Add a small tolerance to handle rounding errors. For example:

uint256 tolerance = 1e10; // Adjust based on expected precision
if (dustAmount <= tolerance) {
dustAmount = 0;
}

Avoid Unnecessary Operations: If possible, simplify the dust calculation to minimize the number of operations that can introduce rounding errors. For example, use scaledBalanceOf instead of balanceOf and rayDiv.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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

Give us feedback!