Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Limits are applied on both buy and sell operations instead of only on sell operations

Author Revealed upon completion

Root +Impact

Description

  • The README.md of the project states: The protocol implements a phased fee structure to prevent manipulation during the initial launch period, with configurable limits, cooldowns, and penalties for excessive selling.

  • In the TokenLaunchHook::_beforeSwap limits and penalties are applied to both buy and sell operations. Specifically the addressSwappedAmount mapping is updated on both types of swap and then used to decide to apply penalties. The same happens with updates of addressLastSwapBlock.
    In this code snippet swapAmount is updated without checking if the user is buying or selling the tokens:

uint256 swapAmount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);

and later used to decide to apply penalties:

if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}

The mapping addressLastSwapBlock is used in a similar fashion in _beforeSwap to both buy and sell operations

if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}

Risk

Likelihood

  • High: Everytime a swap occurs addressSwappedAmount and addressLastSwapBlock are updated

Impact

  • High: The behavior of the contract contrasts with what is specified in the README.md.

  • This will invalidate the whole purpose of the hook.

Proof of concept

The following test demonstrates that both buy and sell operations are tracked for limiting purposes, an operation is a buy operation ( in the context of the test contract) when params.zeroToOne = true.

function test_swapLimitsApplyToBothBuyAndSellOperations() public {
uint256 swapAmount = 10 ether;
vm.deal(user1, 100 ether);
// Initial swap to set initial liquidity
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.001 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
// Record initial remaining limit
uint256 initialRemainingLimit = antiBotHook.getUserRemainingLimit(address(swapRouter));
console.log("Initial Remaining Swap Limit:", antiBotHook.getUserRemainingLimit(address(swapRouter)));
console.log("Initial addressSwappedAmount:", antiBotHook.addressSwappedAmount(address(swapRouter)));
vm.startPrank(user1);
// Buy operation
params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
swapRouter.swap{value: swapAmount}(key, params, testSettings, ZERO_BYTES);
// Buy orders are being unnecessarily tracked
uint256 postBuyRemainingLimit = antiBotHook.getUserRemainingLimit(address(swapRouter));
console.log("Post-Buy Remaining Swap Limit:", antiBotHook.getUserRemainingLimit(address(swapRouter)));
console.log("Post-Buy addressSwappedAmount:", antiBotHook.addressSwappedAmount(address(swapRouter)));
assertLt(
postBuyRemainingLimit,
initialRemainingLimit,
"Remaining swap limit should decrease after buy operation"
);
// Sell operation
params = SwapParams({
zeroForOne: false,
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
});
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// Sell operations are also tracked, correctly
uint256 endingRemainingLimit = antiBotHook.getUserRemainingLimit(address(swapRouter));
console.log("Post-Sell Remaining Swap Limit:", antiBotHook.getUserRemainingLimit(address(swapRouter)));
console.log("Post-Sell addressSwappedAmount:", antiBotHook.addressSwappedAmount(address(swapRouter)));
assertLt(
endingRemainingLimit,
postBuyRemainingLimit,
"Remaining swap limit should decrease after sell operation"
);
}

Recommended mitigation

In TokenLaunchHook::_beforeSwap there should be a check to identify the swap as a "Buy" or "Sell" operation and act accordingly.
Add the following code:

if (currentPhase == 3) {
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
+ if (params.zeroForOne) {
+ // Buy operation, no limits or penalties applied
+ return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
+ }

Support

FAQs

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

Give us feedback!