Summary
When users supply liquidity into the Rtoken, they are minted a rebase rTOKEN which increasing over time. When the amount in the contract after all debt repayment is greater than the contract balance admin can take this dust amount without affecting other users amount but presently the total supply returns the asset value directly but we multiply this by the index again inflating the Total real amount to withdraw and disallowing the admin function from operating as it should.
Vulnerability Details
Admin calls transfer Accrued Dust
* @notice Allows the Reserve Pool to transfer accrued token dust
* @dev Only callable by the Reserve Pool
* @param recipient The address to send the accrued dust to
* @param amount The requested amount to transfer (will be capped at actual dust amount)
*
* Limits transfer to actual dust amount
*/
function transferAccruedDust(address recipient, uint256 amount) external onlyReservePool {
if (recipient == address(0)) revert InvalidAddress();
@audit>> uint256 poolDustBalance = calculateDustAmount();
if(poolDustBalance == 0) revert NoDust();
@audit>> uint256 transferAmount = (amount < poolDustBalance) ? amount : poolDustBalance;
@audit>> IERC20(_assetAddress).safeTransfer(recipient, transferAmount);
emit DustTransferred(recipient, transferAmount);
}
The dust is calculated by ensuring the amount of assets available is above the maxsupply we would pay to liquidity providers.
* @notice Calculate the dust amount in the contract
* @return The amount of dust in the contract
*/
function calculateDustAmount() public view returns (uint256) {
@audit>> uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
@audit>> uint256 currentTotalSupply = totalSupply();
@audit>>BUG>> uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
@audit>> return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}
Total supply returns the actual Amount owed in the contract but this is multiplied again with the rayMul .
E.g
Total scaled balance = 1000 USD
index = 1.1e27
All balance = 1120 USD
Total supply will return = scaled * index = 1100 USD
But this is multiplied with index again
Total real balance will return = 1100 * 1.1 = 1210 USD
Admin is allowed to withdraw 0, 1120 - 1210 = -90 USD
* @notice Returns the non-scaled total supply
* @return The non-scaled total supply
*/
function scaledTotalSupply() external view returns (uint256) {
return super.totalSupply();
}
* @notice Returns the scaled total supply
* @return The total supply (scaled by the liquidity index)
*/
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
@audit>> return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}
Asset value is obtained by rayMul from scaled
While scaled is obtained from amount via rayDiv
function mint(
address caller,
address onBehalfOf,
uint256 amountToMint,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
if (amountToMint == 0) {
return (false, 0, 0, 0);
}
@audit>> uint256 amountScaled = amountToMint.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
Impact
Admin will not be able to withdraw the dust amount in the contract as long as there are Rtokens in the contract. only when total supply is 0 can admin withdraw breaking the code functionality.
Tools Used
Manual Review
Recommendations
Use and return the scaled total supply or use the valuation returned by the total supply directly.