Summary
The RToken.transferAccruedDust() function is supposed to transfer dust amounts of the underlying token to certain recipient. It uses calculateDustAmount() to get the amount of dust to transfer, but the calculation is incorrect.
Vulnerability Details
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;
}
RToken.sol#317
Looking at this function we can see that the contract balance of crvUSD is unnecessarily divided by the liquidity index, which makes it lower than expected.
After that, we can see that totalSupply() is multiplied by the liquidity index, but if we take a closer look inside totalSupply() we can see that this value is already multiplied by the liquidtyIndex.
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}
RToken.sol#203
Impact
Inside calculateDustAmount() totalRealBalance is higher than expected and contractBalance is lower than expected, which makes the function return wrong values.
Due to the fact that contractBalance is lower than expected, the function will return 0 even if there is dust to transfer. This will make the transferAccruedDust() function revert.
Recommendation
Update the calculateDustAmount() function the following way:
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());
// 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;
+ return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply;
}