Vanguard

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

Anti-Bot Protection Does Not Distinguish Buy vs Sell Directions

Author Revealed upon completion

Description

  • The README states the hook provides protection against "excessive selling" during token launches, implying sell-side protection to prevent dumps.

  • The implementation applies penalties to all swaps regardless of direction (zeroForOne). Both buys and sells are counted toward limits and trigger cooldowns.

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ...
// @> Ignores direction
uint256 swapAmount =
params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);
// @> zeroForOne never checked
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
// ...
}

Risk

Likelihood:

  • Occurs on every buy transaction, not just sells

  • Any user buying the token during launch phases faces the same restrictions as sellers

Impact:

  • Legitimate buyers are penalized and limited, reducing buying pressure

  • Discourages token accumulation during fair launch period

  • May reduce price discovery effectiveness as buyers are throttled

  • Contradicts stated goal of preventing "excessive selling"

Proof of Concept

Buyer executes multiple swaps - cooldowns and limits apply to buys the same as sells.

function test_BuysArePenalizedToo() public {
vm.deal(user1, 10 ether);
// User wants to BUY tokens (zeroForOne = true means ETH -> Token)
SwapParams memory buyParams = SwapParams({
zeroForOne: true, // This is a BUY
amountSpecified: -int256(0.1 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
vm.startPrank(user1);
// Multiple buys trigger cooldown and limits - same as sells
swapRouter.swap{value: 0.1 ether}(key, buyParams, testSettings, ZERO_BYTES);
swapRouter.swap{value: 0.1 ether}(key, buyParams, testSettings, ZERO_BYTES);
vm.stopPrank();
// Cooldown is now active for this buyer
uint256 cooldownEnd = antiBotHook.getUserCooldownEnd(address(swapRouter));
assertGt(cooldownEnd, block.number, "Buyers face cooldown too");
}

Recommended Mitigation

Check params.zeroForOne to determine swap direction and only apply limits to sells.

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ... phase logic ...
+ // Determine if this is a sell (token -> ETH)
+ // Assuming token is currency1, sell is when zeroForOne = false
+ bool isSell = !params.zeroForOne;
+
+ // Only apply anti-bot limits to sells
+ if (!isSell) {
+ // This is a buy - no restrictions
+ return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
+ }
uint256 swapAmount =
params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);

Support

FAQs

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

Give us feedback!