Root + Impact
The _beforeSwap function in TokenLaunchHook applies penalties based on swap amounts and cooldowns without distinguishing between buy and sell operations
This contradicts the stated purpose of the hook, which is to prevent excessive selling, as buy operations are also penalized.
Description
-
The hook is intended to apply a phased fee strcture with configurable limits, cooldowns, and penalties for excessive selling as per the README
-
However, the _beforeSwap function in TokenLaunchHook applies penalties based on swap amounts and cooldowns without distinguishing between buy and sell operations.
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
bool applyPenalty = false;
if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
@> applyPenalty = true;
}
}
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
@> applyPenalty = true;
}
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
uint24 feeOverride = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}
Risk
Likelihood: High
-
This will affect all buy swaps during the launch phases, which are critical periods for token distribution and price discovery
-
The issue occurs consistently for every buy transaction processed by the hook
Impact: High
Proof of Concept
function test_PenalizedOnBuy() public {
vm.deal(user1, 1 ether);
uint256 amountIn = 0.001 ether;
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(amountIn),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
uint256 swap1BeforeBalance = token.balanceOf(user1);
vm.startPrank(user1);
token.approve(address(swapRouter), type(uint256).max);
swapRouter.swap{value: amountIn}(key, params, testSettings, abi.encode(user1));
uint256 swap1AfterBalance = token.balanceOf(user1);
vm.roll(block.number + 1);
swapRouter.swap{value: amountIn}(key, params, testSettings, abi.encode(user1));
vm.stopPrank();
uint256 swap2AfterBalance = token.balanceOf(user1);
uint256 delta1 = swap1AfterBalance - swap1BeforeBalance;
uint256 delta2 = swap2AfterBalance - swap1AfterBalance;
assertGt(delta1, delta2, "Second swap should yield fewer tokens due to cooldown penalty");
}
Recommended Mitigation
+ import {Currency} from "v4-core/types/Currency.sol";
+ address public immutable launchToken;
constructor(
IPoolManager _poolManager,
uint256 _phase1Duration,
uint256 _phase2Duration,
uint256 _phase1LimitBps,
uint256 _phase2LimitBps,
uint256 _phase1Cooldown,
uint256 _phase2Cooldown,
uint256 _phase1PenaltyBps,
- uint256 _phase2PenaltyBps
+ uint256 _phase2PenaltyBps,
+ address _launchToken
) BaseHook(_poolManager) {
if (_phase1Duration == 0 || _phase2Duration == 0) revert InvalidConstructorParams();
if (
- _phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000
+ _phase1LimitBps > 10000 || _phase2LimitBps > 10000 || _phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000 || _launchToken == address(0)
) revert InvalidConstructorParams();
// ... existing code ...
+ launchToken = _launchToken;
}
+ function _isSell(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
+ bool IsCurrency0 = Currency.unwrap(key.currency0) == launchToken;
+ if (IsCurrency0) {
+ return zeroForOne;
+ } else {
+ return !zeroForOne;
+ }
+ }
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ... existing code ...
bool applyPenalty = false;
+ bool isSell = _isSell(key, params.zeroForOne);
- if (addressLastSwapBlock[sender] > 0) {
+ if(isSell && addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
- if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
+ if (isSell && !applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
// ... rest of function ...
}