Summary
The RToken::calculateDustAmount() function incorrectly scales balances by the liquidity index, leading to wrong dust calculations that prevent legitimate dust from being claimed via transferAccruedDust().
Vulnerability Details
The dust calculation in RToken::calculateDustAmount() contains two critical errors:
1) The contract balance is incorrectly scaled down by the liquidity index:
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this))
.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
The contract's balance represents actual underlying tokens and should not be scaled by the liquidity index.
2) The total supply is scaled up twice:
uint256 currentTotalSupply = totalSupply();
uint256 totalRealBalance = currentTotalSupply
.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
The totalSupply() function already returns a value scaled by the liquidity index, so scaling it again results in an inflated total balance.
Proof of Concept:
Add the following test to the RToken.test.js file:
describe("Incorrect Dust Amount Calculation, when the liquidity index is not 1", function () {
beforeEach(async function () {
await mockLendingPool.setRToken(rToken.target);
});
it("should not work correctly when calculating the dust amount", async function () {
await mockAsset.mint(rToken.target, ethers.parseEther("100"));
await expect(mockLendingPool.mockMint(reservePool.address, user1.address, ethers.parseEther("100"), RAY))
.to.emit(rToken, "Mint")
.withArgs(reservePool.address, user1.address, ethers.parseEther("100"), RAY);
let assetBalance = await mockAsset.balanceOf(rToken.target);
let scaledSupply = await rToken.scaledTotalSupply();
let currentTotalSupply = await rToken.totalSupply();
let normalizedIncome = await mockLendingPool.getNormalizedIncome();
expect(assetBalance).to.equal(ethers.parseEther("100"));
expect(scaledSupply).to.equal(ethers.parseEther("100"));
expect(currentTotalSupply).to.equal(ethers.parseEther("100"));
expect(normalizedIncome).to.equal(RAY);
await mockLendingPool.mockGetNormalizedIncome(ethers.parseUnits("2", 27));
await mockAsset.mint(rToken.target, ethers.parseEther("100"));
assetBalance = await mockAsset.balanceOf(rToken.target);
currentTotalSupply = await rToken.totalSupply();
normalizedIncome = await mockLendingPool.getNormalizedIncome();
scaledSupply = await rToken.scaledTotalSupply();
expect(assetBalance).to.equal(ethers.parseEther("200"));
expect(scaledSupply).to.equal(ethers.parseEther("100"));
expect(currentTotalSupply).to.equal(ethers.parseEther("200"));
expect(normalizedIncome).to.equal(ethers.parseUnits("2", 27));
await mockAsset.mint(rToken.target, ethers.parseEther("1"));
expect(await rToken.calculateDustAmount()).to.not.equal(ethers.parseEther("1"));
});
})
Impact
Dust calculations will be incorrect, preventing the owner from claiming legitimate dust via transferAccruedDust
Funds can become permanently stuck in the contract
The accounting system becomes unreliable for tracking dust amounts
Recommendations
Option 1: Remove incorrect scaling
function calculateDustAmount() public view returns (uint256) {
- uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this))
- .rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
uint256 currentTotalSupply = totalSupply();
- uint256 totalRealBalance = currentTotalSupply
- .rayMul(ILendingPool(_reservePool).getNormalizedIncome());
- return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
+ return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply;
}
Option 2: Use scaled balances consistently
function calculateDustAmount() public view returns (uint256) {
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
- uint256 currentTotalSupply = totalSupply();
+ uint256 scaledTotalSupply = scaledTotalSupply();
- uint256 totalRealBalance = currentTotalSupply
- .rayMul(ILendingPool(_reservePool).getNormalizedIncome());
- return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
+ uint256 index = ILendingPool(_reservePool).getNormalizedIncome();
+ return contractBalance <= scaledTotalSupply.rayMul(index) ? 0 : contractBalance - scaledTotalSupply.rayMul(index);
}