On 15 October 2026, an attacker took about 2.31 ETH out of KolanHook, a Uniswap v4 hook on Ethereum mainnet, and kept roughly 1.28 ETH (around $3K) as profit after repaying a flash loan. The dollar amount is small. The pattern is not. A hook that spends its own reserves on a swap, at whatever the pool’s spot price happens to be, triggered by a function anyone can call, is an exploit waiting for a caller.
This post-mortem walks through what Kolan does, why the buyback path was exploitable, how the attack was sequenced, and what auditors and hook developers should take away from it.
KolanHook, token: KLN.accept(id) lets anyone close an expired window. Closing a window runs a buyback swap with the hook’s own ETH, and that swap used a price limit of MIN_SQRT_PRICE + 1, which in practice means no slippage protection at all.According to Kolan’s own documentation, the system is two contracts: KolanHook, which holds the pool liquidity and the “windows”, and KolanToken, a fixed-supply ERC-20 (1,000,000 KLN) minted once to the hook. There is no owner, no proxy and no pause, and the hook refuses outside liquidity. The docs also state plainly that the contracts are tested but not audited.
A “window” is attached to a purchase. A buyer pays a premium on top of the ETH they spend, and for a period of time they can undo the purchase. Once a window expires it can no longer be undone, only kept, and the docs describe closing it as something anyone can do after expiry. When a window is closed, the hook uses ETH it holds to buy back KLN and burn it.
That design gives the hook two jobs at once: it is the market’s liquidity and it is a trader that periodically buys from that market. Whenever a contract trades on its own behalf, the question an auditor must ask is simple: who decides when it trades, and at what price?
Two properties, each defensible on its own, combined into the bug.
Making accept() permissionless after expiry is a reasonable liveness choice. Nobody wants windows stuck open because the buyer went offline. But it also means the timing of the buyback is chosen by whoever calls it, including someone who has just moved the price.
Per Defimon’s analysis, the call chain is accept → _closeWindow → _buyBack(), and the buyback swaps the hook’s ETH into the pool with sqrtPriceLimitX96 = MIN_SQRT_PRICE + 1. In Uniswap v4 that limit is the furthest price the swap is allowed to move to. Setting it to the extreme bound tells the PoolManager “fill me at any price”.
The simplified sketch below is our own illustration of the pattern, not Kolan’s verified source:
// Simplified illustration of the vulnerable pattern (not verbatim Kolan code)
function accept(uint256 id) external {
Window storage w = windows[id];
require(block.timestamp >= w.expiry || msg.sender == w.owner, "WindowStillOpen");
_closeWindow(id);
}
function _closeWindow(uint256 id) internal {
uint256 ethForBuyback = windows[id].premium / 2;
delete windows[id];
_buyBack(ethForBuyback);
}
function _buyBack(uint256 ethIn) internal {
// ETH -> KLN, exact input, burn the output
BalanceDelta delta = poolManager.swap(
key,
IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: -int256(ethIn),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 // no real limit
}),
""
);
_settleAndBurn(delta);
}
Nothing in this path compares the current spot price with a reference price, a TWAP or a minimum output. The amount of KLN the hook receives is whatever the pool gives it at that moment.
The figures below come from Defimon’s alert and the transaction breakdown published by ChainBounty; we have rounded them.
accept(10) and accept(42) on two expired windows. Each one closes a window and triggers _buyBack(), which spends the hook’s ETH buying KLN at the inflated price and burns it. In total about 2.31 ETH leaves the hook.The gap between the hook’s 2.31 ETH outflow and the 1.28 ETH profit is worth spelling out. ChainBounty’s write-up rightly separates the two: one is what the hook lost, the other is what the attacker kept. The rest went to swap fees, the token’s 1% tax and price impact. Reporting “2.31 ETH stolen” or “1.28 ETH lost” alone would both be misleading.
This is the classic sandwich of a protocol-owned trade. Usually MEV bots sandwich users; here the victim was the protocol’s own treasury logic, and the attacker didn’t even need to wait for it to trade. They could make it trade on demand.
We covered how hooks work in Uniswap v4 Hooks: How They Work and Where They Break, and two recent incidents on this blog, the GoldPesa GPXHooks rebalance drain and the one-wei share vault exploit, point at the same theme: once a hook holds value and acts on its own, it inherits every risk of an on-chain trader.
Hooks make this easy to get wrong for three reasons:
getSlot0() and an unbounded swap are the path of least resistance. A safe price needs extra work.sqrtPriceLimitX96 from a reference price (a TWAP, an oracle, or the price recorded when the window opened) and revert if the spot price is too far away.A hardened version of the buyback could look like this (again illustrative):
function _buyBack(uint256 ethIn) internal {
uint160 ref = _twapSqrtPrice(30 minutes); // manipulation-resistant reference
uint160 limit = uint160(uint256(ref) * 9_900 / 10_000); // allow ~1% adverse move
(uint160 spot,,,) = poolManager.getSlot0(key.toId());
if (spot < limit) {
pendingBuyback += ethIn; // defer instead of trading into a manipulated pool
return;
}
BalanceDelta delta = poolManager.swap(
key,
IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: -int256(ethIn),
sqrtPriceLimitX96: limit
}),
""
);
require(uint128(delta.amount1()) >= _minOut(ethIn, ref), "Slippage");
_settleAndBurn(delta);
}
MIN_SQRT_PRICE + 1, MAX_SQRT_PRICE - 1 and amountOutMin = 0 are fine in tests and in user-facing routers where the user sets the bound. Inside protocol logic they are red flags.Our smart contract security checklist includes these slippage and price-source checks for any contract that swaps.
0x7b131926e6f06491ae1aead3e419b208c8aa70ed7898e8c5a33d9f52e40915a90x8df735014eab81905e76b87ef0c069bd8e0ae3820x0136feCf49E0217BFd2D9551E8e7b05f174Ba8eC0x2CADc4F2660Bb77a118b0e74fC9E48E935b3a1bFKolanHook lost a few thousand dollars, but its design is not unusual. Plenty of new hooks buy back, rebalance or compound with their own funds, and many of them let anyone trigger it. If your hook trades, the price it accepts is part of your security model, not a gas optimization you can skip.
Building a Uniswap v4 hook or a token with automated buybacks? Contact Panda Security for a review before launch, or see our audit pricing.
A recovery bot finalized stale Sui Bridge withdrawals and paid 86 ETH, 109K USDT and 0.9 WBTC to the Cetus exploiter. What went wrong and what to fix.
UNCX's token vesting contract lost roughly $87.7K when an attacker exploited a share-accounting flaw in release(), topUpVesting(), and cancel(). We break down the root cause, the exact attack sequence, and what builders should take away from it — without the jargon.
Autheo’s THEO token lost 92% after attackers used a Hyperlane TrustedRelayerIsm misconfiguration to mint 19 million unbacked tokens on Base and extract roughly $1.76 million. Here’s how the attack worked and why single-key security models remain dangerous for cross-chain assets.
Uniswap v4 hooks explained: the PoolManager singleton, flash accounting, and what the Cork and Bunni exploits teach us about hook security. A PandaSecurity deep dive.