Summary
The RToken::calculateDustAmount()
function incorrectly scales values when comparing the contract underlying asset balance with RToken
total supply, leading to dust amounts being underreported or completely missed.
Vulnerability Details
In calculateDustAmount()
, the underlying asset contract balance is incorrectly scaled down by the normalized income, while the total supply is incorrectly scaled up by the normalized income again after already being scaled in totalSupply()
:
function calculateDustAmount() public view returns (uint256) {
@> uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
@> uint256 currentTotalSupply = totalSupply();
@> uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}
The double scaling results in:
This causes the comparison to almost always return 0, even when dust exists.
Impact
The protocol fails to detect and handle dust amounts correctly, which:
Prevents the protocol from properly managing excess funds
Makes the transferAccruedDust
function essentially non-functional
Could lead to temporarily stuck funds in the contract that should be recoverable as dust
Tools Used
Manual review
Proof of Concept
Add the following test case to the test/unit/core/tokens/RToken.test.js
file inside the describe("Dust handling", function() {
block:
it("should correctly calculate dust amount", async function() {
const newIndex = RAY + ethers.parseUnits("0.1", 27);
const repaidAmount = ethers.parseEther("10");
await mockLendingPool.mockGetNormalizedIncome(newIndex);
await mockAsset.mint(rToken.target, repaidAmount);
const balanceUser1 = await rToken.balanceOf(user1.address);
const rTokenBalance = await mockAsset.balanceOf(rToken.target);
expect(rTokenBalance).to.equal(balanceUser1);
const dustAmount = ethers.parseEther("1");
await mockAsset.mint(rToken.target, dustAmount);
const calculatedDust = await rToken.calculateDustAmount();
expect(calculatedDust).to.not.equal(dustAmount);
});
Recommendations
Remove the incorrect scaling operations and compare the raw values directly:
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;
}