QuantAMM

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

In `UpliftOnlyExample::onAfterSwap` function, `ownerFee` tokens are sent to `UpliftOnlyExample` contract in stead of owner and are stuck there.

Summary

In UpliftOnlyExample::onAfterSwap function, ownerFee tokens are sent to UpliftOnlyExample contract in stead of owner and are stuck there.

Vulnerability Details

  • In onAfterSwap function, whenever user swap, they will be charged a swap fee divided into two parts adminFee and ownerFee.

  • The adminFee tokens will be sent to quantAMMAdmin

  • And the ownerFee tokens in stead of transfer to owner(), they are transfers to UpliftOnlyExample contract address. UpliftOnlyExample.sol#L343.

Unfortunately, UpliftOnlyExample contract have no function to transfer ERC20 tokens out or implement some logic to use these tokens after receiving them. So there is no way to take these tokens out.

PoC

  • Place this test in UpliftExample.t.sol

  • Then in /2024-12-quantamm/pkg/pool-hooks run forge test --mt test_feeTokenStuckInUpliftContract -vv. Look into the terminal, the last call will revert with ERC20InsufficientAllowance error.

  • The owner can not take the ownerFee out.

function test_feeTokenStuckInUpliftContract() public {
uint256 swapAmount = 1e3 * 1e18;
// Fee between 0 and 100%
uint64 hookFeePercentage = uint64(5e16);
vm.expectEmit();
emit UpliftOnlyExample.HookSwapFeePercentageChanged(
poolHooksContract,
hookFeePercentage
);
vm.prank(owner);
UpliftOnlyExample(payable(poolHooksContract)).setHookSwapFeePercentage(
hookFeePercentage
);
uint256 hookFee = swapAmount.mulUp(hookFeePercentage);
vm.prank(bob);
vm.expectCall(
address(poolHooksContract),
abi.encodeCall(
IHooks.onAfterSwap,
AfterSwapParams({
kind: SwapKind.EXACT_IN,
tokenIn: dai,
tokenOut: usdc,
amountInScaled18: swapAmount,
amountOutScaled18: swapAmount,
tokenInBalanceScaled18: poolInitAmount + swapAmount,
tokenOutBalanceScaled18: poolInitAmount - swapAmount,
amountCalculatedScaled18: swapAmount,
amountCalculatedRaw: swapAmount,
router: address(router),
pool: pool,
userData: bytes('')
})
)
);
if (hookFee > 0) {
vm.expectEmit();
emit UpliftOnlyExample.SwapHookFeeCharged(
poolHooksContract,
IERC20(usdc),
hookFee
);
}
router.swapSingleTokenExactIn(
address(pool),
dai,
usdc,
swapAmount,
0,
MAX_UINT256,
false,
bytes('')
);
vm.startPrank(owner);
usdc.transferFrom(
address(upliftOnlyRouter),
owner,
usdc.balanceOf(address(upliftOnlyRouter))
);
vm.stopPrank();
}

Impact

Owner lost all ownerFee tokens.

Tools Used

  • Manual review

  • Foundry

Recommendations

Send ownerFee to owner, not the UpliftOnlyExample contract.

- _vault.sendTo(feeToken, address(this), ownerFee);
+ _vault.sendTo(feeToken, owner(), ownerFee);
Updates

Lead Judging Commences

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

finding_ownerFee_cannot_be_withdrawn

Likelihood: High, every swap. Impact: High, funds are stuck.

Support

FAQs

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

Give us feedback!