HardhatDeFi
15,000 USDC
View results
Submission Details
Severity: low
Valid

Incorrect handling of aave v3 rounding in claim and redeem can temporarily DOS diva fee recipient/ Last user from redeeming/removing liquidity.

Summary

This is quite different from the known issue.

>Yield rounding issues could temporarily prevent yield claims if aToken balance becomes smaller than wToken supply.

This will DOS the redeem/remove liquidity function.

When admin calls to claim yields in the Aave contract the contract then withdraws collateral and rounds up the amount of atokens burned to truncate 0.5 to 1 this ensures that we are correctly handling withdraws by rounding up to favour the protocol and rounding down if necessary. This issue although correctly handled before the call, the balance remaining for address(this) after this claim may become 1 wei or some wei less and this will temporarily DOS the last user who tries to redeem/remove liquidity, which can either be the diva fee recipient of the Last user.

>Invariants

>At any point in time, the following invariants have to hold true:

  • aToken balance of AaveDIVAWrapper >= wToken supply

  • short token supply = long token supply = wToken supply

The first invariant will be broken.

Vulnerability Details

Temporary DOS of the last user to redeem and remove liquidity

function _claimYield(address _collateralToken, address _recipient) internal returns (uint256) {
// Confirm that the collateral token is registered
if (_collateralTokenToWToken[_collateralToken] == address(0)) {
revert CollateralTokenNotRegistered();
}
if (_recipient == address(0)) revert ZeroAddress();
// Redeem aToken for collateral token at Aave Protocol and send collateral token to recipient.
@audit>> uint256 _amountReturned = IAave(_aaveV3Pool).withdraw(
_collateralToken, // Address of the underlying asset (e.g., USDT), not the aToken.
@audit>> _getAccruedYieldPrivate(_collateralToken), // Amount to withdraw.
_recipient // Address that will receive the underlying asset.
);
emit YieldClaimed(owner(), _recipient, _collateralToken, _amountReturned);
return _amountReturned;
}

We get the amount of interest earned

function _getAccruedYieldPrivate(address _collateralToken) private view returns (uint256) {
uint256 aTokenBalance = IERC20Metadata(IAave(_aaveV3Pool).getReserveData(_collateralToken).aTokenAddress)
.balanceOf(address(this));
uint256 wTokenSupply = IERC20Metadata(_collateralTokenToWToken[_collateralToken]).totalSupply();
@audit>> // Handle case where the aToken balance might be smaller than the wToken supply (e.g., due to rounding).
@audit>> // In that case, the owner should just wait until yield accrues.
@audit>> return aTokenBalance > wTokenSupply ? aTokenBalance - wTokenSupply : 0;
}

Also, this occurs during withdrawals

function _redeemWTokenPrivate(
address _wToken,
uint256 _wTokenAmount,
address _recipient,
address _burnFrom
) private returns (uint256) {
if (_recipient == address(0)) revert ZeroAddress();
// Burn the specified amount of wTokens. Only this contract has the authority to do so.
// Reverts if `_wTokenAmount` exceeds the user's wToken balance.
IWToken(_wToken).burn(_burnFrom, _wTokenAmount);
address _collateralToken = _wTokenToCollateralToken[_wToken];
// Withdraw the collateral asset from Aave, which burns the equivalent amount of aTokens owned by this contract.
// E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC.
// Collateral token is transferred to `_recipient`.
// Reverts if the collateral token is not a registered wToken (first parameter will be address(0)).
@audit>> uint256 _amountReturned = IAave(_aaveV3Pool).withdraw(
_collateralToken, // Address of the underlying asset (e.g., USDT), not the aToken.
@audit>> _wTokenAmount, // Amount to withdraw.
_recipient // Address that will receive the underlying asset.
);
emit WTokenRedeemed(_wToken, _wTokenAmount, _collateralToken, _amountReturned, _recipient);
return _amountReturned;
}

Aave rounds up the amount burned in the user balance to handle rounding issues

https://github.com/aave/aave-v3-core/blob/782f51917056a53a2c228701058a6c3fb233684a/contracts/protocol/libraries/logic/SupplyLogic.sol#L139-L144

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;
if (params.amount == type(uint256).max) {
amountToWithdraw = userBalance;
}
ValidationLogic.validateWithdraw(reserveCache, amountToWithdraw, userBalance);
reserve.updateInterestRates(reserveCache, params.asset, 0, amountToWithdraw);
bool isCollateral = userConfig.isUsingAsCollateral(reserve.id);
if (isCollateral && amountToWithdraw == userBalance) {
userConfig.setUsingAsCollateral(reserve.id, false);
emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender);
}
@audit >>> IAToken(reserveCache.aTokenAddress).burn(
msg.sender,
params.to,
amountToWithdraw,
reserveCache.nextLiquidityIndex
);

The burned amount can be rounded up in special cases

function _burnScaled(address user, address target, uint256 amount, uint256 index) internal {
@audit>> uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID\_BURN\_AMOUNT);
uint256 scaledBalance = super.balanceOf(user);
@audit>> uint256 balanceIncrease = scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[user].additionalData);
_userState[user].additionalData = index.toUint128();
@audit>> _burn(user, amountScaled.toUint128());
if (balanceIncrease > amount) {
uint256 amountToMint = balanceIncrease - amount;
emit Transfer(address(0), user, amountToMint);
emit Mint(user, user, amountToMint, balanceIncrease, index);
} else {
uint256 amountToBurn = amount - balanceIncrease;
emit Transfer(user, address(0), amountToBurn);
emit Burn(user, target, amountToBurn, balanceIncrease, index);
}

Math operation

/**
* @notice Divides two ray, rounding half up to the nearest ray
* @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328
* @param a Ray
* @param b Ray
* @return c = a raydiv b
*/
function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {
// to avoid overflow, a <= (type(uint256).max - halfB) / RAY
assembly {
if or(iszero(b), iszero(iszero(gt(a, div(sub(not(0), div(b, 2)), RAY))))) {
revert(0, 0)
}
c := div(add(mul(a, RAY), div(b, 2)), b)
}
}

Does It Always Round Up?

Not always. It follows "round half up" behavior:

  • If the remainder is exactly 0.5 or greater, it rounds up.

  • Otherwise, it truncates (rounds down).

Impact

Not correctly handling the small rounding issue during withdrawals can cause a temporary DOS to the last user, who will have to wait for a while for the Aave index to increase the balance to be able to withdraw.

Tools Used

Manual Review

Recommendations

  1. Admin can create 2 claim yield functions, The first should be callable when the wtoken total supply is greater than 0, this claim yield function should ensure leaving behind some tokens behind (1 or 1000) in the contract to prevent the temporary DOS.

  2. The second claim function can be called only when the total supply of the wtoken is 0, and all token yield should only be sweepable then. This will help to maintain and keep this invariant unbroken.

  3. Alternatively, we can add a check after claim and redeem calls to ensure that the invariant always holds.

Updates

Lead Judging Commences

bube Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

bigsam Submitter
9 months ago
bube Lead Judge
9 months ago
bube Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Rounding issue

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.