QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: low
Valid

Admin fee collection for swapping of low decimal tokens round down to zero

Summary

Rounding down of fees are caused in swapping of low decimal tokens in the onAfterSwap callback function of UpliftOnlyExample contract .

Vulnerability Details

To explain the bug, let's take an example token - GUSD (or Gemini Dollar which has decimal of 2 https://etherscan.io/token/0x056fd409e1d7a124bd7017459dfea2f387b6d5cd#readContract#F3 -> It is supported by Balancer Pool , so I believe it is an in scope asset - https://balancer.fi/pools/ethereum/v2/0x81e998523f02adf4679ff57fff8ca2b9d23a574700020000000000000000060a ) .

In UpliftOnlyExample contract , when onAfterSwap function is called after a swap where the tokenout == GUSD , hookfee is to be calculated in the tokenOut amount as can be seen here -
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-hooks/contracts/hooks-quantamm/UpliftOnlyExample.sol#L292-L350

/// @inheritdoc BaseHooks
function onAfterSwap(
AfterSwapParams calldata params
) public override onlyVault returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) {
hookAdjustedAmountCalculatedRaw = params.amountCalculatedRaw;
if (hookSwapFeePercentage > 0) {
uint256 hookFee = params.amountCalculatedRaw.mulUp(hookSwapFeePercentage); /*<@audit - here (1) */
if (hookFee > 0) {
IERC20 feeToken;
// Note that we can only alter the calculated amount in this function. This means that the fee will be
// charged in different tokens depending on whether the swap is exact in / out, potentially breaking
// the equivalence (i.e., one direction might "cost" less than the other).
if (params.kind == SwapKind.EXACT_IN) {
// For EXACT_IN swaps, the `amountCalculated` is the amount of `tokenOut`. The fee must be taken
// from `amountCalculated`, so we decrease the amount of tokens the Vault will send to the caller.
//
// The preceding swap operation has already credited the original `amountCalculated`. Since we're
// returning `amountCalculated - hookFee` here, it will only register debt for that reduced amount
// on settlement. This call to `sendTo` pulls `hookFee` tokens of `tokenOut` from the Vault to this
// contract, and registers the additional debt, so that the total debits match the credits and
// settlement succeeds.
feeToken = params.tokenOut;
hookAdjustedAmountCalculatedRaw -= hookFee; /*<@audit - here (2) */
} else {
// For EXACT_OUT swaps, the `amountCalculated` is the amount of `tokenIn`. The fee must be taken
// from `amountCalculated`, so we increase the amount of tokens the Vault will ask from the user.
//
// The preceding swap operation has already registered debt for the original `amountCalculated`.
// Since we're returning `amountCalculated + hookFee` here, it will supply credit for that increased
// amount on settlement. This call to `sendTo` pulls `hookFee` tokens of `tokenIn` from the Vault to
// this contract, and registers the additional debt, so that the total debits match the credits and
// settlement succeeds.
feeToken = params.tokenIn;
hookAdjustedAmountCalculatedRaw += hookFee;
}
uint256 quantAMMFeeTake = IUpdateWeightRunner(_updateWeightRunner).getQuantAMMUpliftFeeTake();
uint256 ownerFee = hookFee;
if (quantAMMFeeTake > 0) {
uint256 adminFee = hookFee / (1e18 / quantAMMFeeTake); /*<@audit - here (3) */
ownerFee = hookFee - adminFee; /*<@audit - here (4) */
address quantAMMAdmin = IUpdateWeightRunner(_updateWeightRunner).getQuantAMMAdmin();
_vault.sendTo(feeToken, quantAMMAdmin, adminFee); /*<@audit - here (5) */
emit SwapHookFeeCharged(quantAMMAdmin, feeToken, adminFee);
}
if (ownerFee > 0) {
_vault.sendTo(feeToken, address(this), ownerFee);
emit SwapHookFeeCharged(address(this), feeToken, ownerFee);
}
}
}
return (true, hookAdjustedAmountCalculatedRaw);
}

Here let's take an example scenario of params.amountCalculatedRaw to be 50 dollars of GUSD i.e., 50e2 .
Let's hookSwapFeePercentage be 1% == 0.01e18 , quantAMMFeeTake also be 1% == 0.01e18 .

Here , hookFee will round up to 50 .
Now looking at the calculation of adminFee we see that it becomes => adminFee = (50)/(1e18/1e16)
= 50/100 = 0

Here as we can , for 50 USD worth of GUSD as output token , the admin fee turns out to be 0 . And so the whole amount hooFee would be transferred to UpliftOnlyExample which itself is not withdrawable as I have explained in another report of myself .

Impact

Quant AMM Admin doesn't receive the fees for swap, as it is round down to 0 & this 0 amount is tried to be transferred to quantAMMAdmin causing problems .

Tools Used

Manual review

Recommendations

Use a different mechanism to calculate Quant Amm Admin fees for low decimal tokens .

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_tokens_with_few_decimals_can_bypass_fees

Likelyihood: Very Low, tokens with 2 or less decimals and few fees. Impact: Low, bypass fees but for very few amounts, gas usage will be equivalent. (No reason to break a big swap in multiple)

Appeal created

angrymustacheman Submitter
7 months ago
n0kto Lead Judge
7 months ago
n0kto Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_tokens_with_few_decimals_can_bypass_fees

Likelyihood: Very Low, tokens with 2 or less decimals and few fees. Impact: Low, bypass fees but for very few amounts, gas usage will be equivalent. (No reason to break a big swap in multiple)

Support

FAQs

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