Summary
When User Calls to withdraw they specify a value bigger than their Balance so they can withdraw their max balance, But this is not well handled in the _ensureLiquidity call and if the right amount is not specified the call to withdraw will revert.
Vulnerability Details
Rtoken burn
* @notice Burns RToken from a user and transfers underlying asset
* @param from The address from which tokens are burned
* @param receiverOfUnderlying The address receiving the underlying asset
* @param amount The amount to burn (in underlying asset units)
* @param index The liquidity index at the time of burning
* @return A tuple containing:
* - uint256: The amount of scaled tokens burned
* - uint256: The new total supply after burning
* - uint256: The amount of underlying asset transferred
*/
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
@audit>> supplied balance>>> uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
@audit>> if(amount > userBalance){
@audit>> amount = userBalance;
@audit>> }
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}
The contract supports stating a large integer to withdraw the max balance of their account
but this will not work well as the last user to withdraw will be DOSed also if users use the uint256.max to withdraw , their call will revert.
see lending pool
* @notice Allows a user to withdraw reserve assets by burning RTokens
* @param amount The amount of reserve assets to withdraw
*/
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (withdrawalsPaused) revert WithdrawalsArePaused();
ReserveLibrary.updateReserveState(reserve, rateData);
@audit>> what if we want to use a large amount to withdraw all >>
@audit>> amount sent might not be the actual amount >> _ensureLiquidity(amount);
(uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve,
rateData,
amount,
msg.sender
);
_rebalanceLiquidity();
emit Withdraw(msg.sender, amountWithdrawn);
}
Call will revert if used by the last User or any user that uses the maxuint256
* @notice Internal function to ensure sufficient liquidity is available for withdrawals or borrowing
* @param amount The amount required
*/
function _ensureLiquidity(uint256 amount) internal {
if (address(curveVault) == address(0)) {
return;
}
uint256 availableLiquidity = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (availableLiquidity < amount) {
uint256 requiredAmount = amount - availableLiquidity;
@audit>> reverts>> _withdrawFromVault(requiredAmount);
}
}
The same implementation exists in AAVE =>
function executeWithdraw(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ExecuteWithdrawParams memory params
) external returns (uint256) {
DataTypes.ReserveData storage reserve = reservesData[params.asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
uint256 userBalance = IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender).rayMul(
reserveCache.nextLiquidityIndex
);
uint256 amountToWithdraw = params.amount;
@audit>> if (params.amount == type(uint256).max) {
amountToWithdraw = userBalance;
}
Impact
DOS to users that what to use uint128 or unit 256 max to withdraw their max balance. Also specifically the last user to withdraw from the vault will be DOSed.
Tools Used
Manual Review
Recommendations
NEST a check to ensure that amount if > user balance is equal to the user balance and our request is feasible to avoid unnecessary revertions.
* @notice Allows a user to withdraw reserve assets by burning RTokens
* @param amount The amount of reserve assets to withdraw
*/
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (withdrawalsPaused) revert WithdrawalsArePaused();
ReserveLibrary.updateReserveState(reserve, rateData);
++ uint256 userBalance = balanceOf(from);
++ if(amount > userBalance){
++ amountin = userBalance;
++ }
++
-- _ensureLiquidity(amount);
++ _ensureLiquidity(amountin);
(uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve,
rateData,
amount,
msg.sender
);
_rebalanceLiquidity();
emit Withdraw(msg.sender, amountWithdrawn);
}