Summary
In the case that hookSwapFeePercentage of UpliftOnlyExample contract is configured bigger than 0. This is likely to happen because the contract is intended to charge some fees for swap service. In the test case of the contract, the minimum fee is 0.001%.
The swap fee is sent to this UpliftOnlyExample contract from the Vault contract. Swap fees are frozen forever in UpliftOnlyExample contract because there is no way to withdraw the fee token.
Vulnerability Details
The root cause is that there is no function to withdraw the fee. So in the case, the hook fee is > 0 the Vault will send the token to the UpliftOnlyExample contract. So the fee token will be frozen forever.
https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-hooks/contracts/hooks-quantamm/UpliftOnlyExample.sol#L293-L298
https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-hooks/contracts/hooks-quantamm/UpliftOnlyExample.sol#L342-L350
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);
if (hookFee > 0) {
if (ownerFee > 0) {
_vault.sendTo(feeToken, address(this), ownerFee);
emit SwapHookFeeCharged(address(this), feeToken, ownerFee);
}
}
}
return (true, hookAdjustedAmountCalculatedRaw);
}
Impact
Impact: High
Reason: Because the swap fee that is money are stuck (frozen) forever in the contract. The amount of money lost is accumulated over time and eventually become quite big.
Likelyhood: High
It's highly to happen. The hookSwapFeePercentage is likely to be configured as > 0. In the test case, there is configuration of min and max swap fee. See testFeeSwapExactIn__Fuzz for example.
https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-hooks/test/foundry/UpliftExample.t.sol#L56-L57
uint64 private constant _MIN_SWAP_FEE_PERCENTAGE = 0.001e16;
uint64 private constant _MAX_SWAP_FEE_PERCENTAGE = 10e16;
=> Total Impact: High
Reference: https://docs.codehawks.com/hawks-auditors/how-to-evaluate-a-finding-severity
Tools Used
Foundry test case
Proof of Concept
Step 1: Setup the precondition that hookSwapFeePercentage is > 0 for example, 5e16.
Step 2: Get the USDC balance of the contract upliftOnlyRouter that is 0. An user (Bob) swaps DAI for USDC.
Check the USDC balance of the upliftOnlyRouter now is > 0
POC code below:
Copy the test testFeeSwapExactIn into https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-hooks/test/foundry/UpliftExample.t.sol
function testFeeSwapExactIn() public {
uint256 swapAmount = 1000*10**18;
console.log("Setup the scenario the swapFee is 5e16");
uint64 hookFeePercentage = 5e16;
vm.expectEmit();
emit UpliftOnlyExample.HookSwapFeePercentageChanged(address(upliftOnlyRouter), hookFeePercentage);
vm.prank(owner);
upliftOnlyRouter.setHookSwapFeePercentage(hookFeePercentage);
uint256 hookFee = swapAmount.mulUp(hookFeePercentage);
console.log("Before swap: USDC balance of uplift Router ", IERC20(usdc).balanceOf(address(upliftOnlyRouter)));
vm.prank(bob);
vm.expectCall(
address(upliftOnlyRouter),
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(address(upliftOnlyRouter), IERC20(usdc), hookFee);
}
console.log("User Bob swap dai for usdc");
router.swapSingleTokenExactIn(address(pool), dai, usdc, swapAmount, 0, MAX_UINT256, false, bytes(""));
console.log("After swap: USDC balance of uplift Router ", IERC20(usdc).balanceOf(address(upliftOnlyRouter)));
}
Run test:
forge test --match-test testFeeSwapExactIn -vv
Test log:
[PASS] testFeeSwapExactIn() (gas: 216871)
Logs:
Setup the scenario the swapFee is 5e16
Before swap: USDC balance of uplift Router 0
User Bob swap dai for usdc
After swap: USDC balance of uplift Router 50000000000000000000
Recommendations
Implement some function to withdraw the fees. Or use some admin EOA to receive the fee.
@note In the hook examples, FeeTakingHookExample implements withdrawFees functionality. But the upliftExampleOnly now does not inherit or have any similar function. In the test case of 2024-12-quantamm\pkg\pool-hooks\test\foundry\UpliftExample.t.sol, I see some test related to FeeTakingHookExample, but it is not applicable for upliftExampleOnly.