// SPDX-License-Identifier: MIT pragma solidity 0.8.26; // Antarmuka minimal Uniswap v4 (ditulis sendiri supaya mudah diaudit) struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } struct SwapParams { bool zeroForOne; int256 amountSpecified; uint160 sqrtPriceLimitX96; } interface IPoolManager { function unlock(bytes calldata data) external returns (bytes memory); function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) external returns (int256); function sync(address currency) external; function settle() external payable returns (uint256); function take(address currency, address to, uint256 amount) external; } interface IERC20 { function transfer(address, uint256) external returns (bool); function balanceOf(address) external view returns (uint256); } // Probe: menjalankan swap di dalam unlock lalu SENGAJA revert membawa hasilnya. // Tidak ada token yang berpindah — dipakai lewat eth_call dengan state override. contract Probe { IPoolManager constant PM = IPoolManager(0x8366a39CC670B4001A1121B8F6A443A643e40951); error Result(int128 amount0, int128 amount1); error RoundTrip(uint256 received, uint256 returned); error Stage(uint8 stage, bytes reason); error Settled(uint256 taken, uint256 received, uint256 paid, uint256 returned); // Bolak-balik DENGAN pelunasan sungguhan: token benar-benar ditransfer PoolManager → Probe → PoolManager. // Menangkap pajak transfer dan token yang menolak dijual (honeypot di level token). // Probe butuh saldo token masuk (diberi lewat state override). Tahap gagal: 0 beli, 1 token masuk, 2 token keluar, 3 jual. function tokenTrip(PoolKey calldata key, bool zeroForOne, uint256 amountIn) external returns (uint8 stage, uint256 taken, uint256 received, uint256 paid, uint256 returned, bytes memory reason) { try PM.unlock(abi.encode(key, zeroForOne, -int256(amountIn), true, true)) { revert("unreachable"); } catch (bytes memory err) { bytes4 sel = bytes4(err); if (sel == Settled.selector) { assembly { taken := mload(add(err, 36)) received := mload(add(err, 68)) paid := mload(add(err, 100)) returned := mload(add(err, 132)) } return (4, taken, received, paid, returned, ""); } if (sel == Stage.selector) { bytes memory inner; assembly { let p := add(err, 4) mstore(p, sub(mload(err), 4)) inner := p } (uint8 st, bytes memory why) = abi.decode(inner, (uint8, bytes)); return (st, 0, 0, 0, 0, why); } return (0, 0, 0, 0, 0, err); } } function quote(PoolKey calldata key, bool zeroForOne, int256 amountSpecified) external returns (bool ok, int128 amount0, int128 amount1, bytes memory reason) { try PM.unlock(abi.encode(key, zeroForOne, amountSpecified)) { revert("unreachable"); } catch (bytes memory err) { if (err.length == 68 && bytes4(err) == Result.selector) { assembly { amount0 := mload(add(err, 36)) amount1 := mload(add(err, 68)) } return (true, amount0, amount1, ""); } return (false, 0, 0, err); } } // Bolak-balik: jual amountIn sisi z, lalu jual kembali seluruh hasilnya ke arah sebaliknya. function roundTrip(PoolKey calldata key, bool zeroForOne, uint256 amountIn) external returns (uint8 stage, uint256 received, uint256 returned, bytes memory reason) { try PM.unlock(abi.encode(key, zeroForOne, -int256(amountIn), true)) { revert("unreachable"); } catch (bytes memory err) { bytes4 sel = bytes4(err); if (sel == RoundTrip.selector) { assembly { received := mload(add(err, 36)) returned := mload(add(err, 68)) } return (2, received, returned, ""); } if (sel == Stage.selector) { bytes memory inner; assembly { let p := add(err, 4) mstore(p, sub(mload(err), 4)) inner := p } (uint8 st, bytes memory why) = abi.decode(inner, (uint8, bytes)); return (st, 0, 0, why); } return (0, 0, 0, err); } } function _swap(PoolKey memory key, bool z, int256 amt) internal returns (int256) { uint160 limit = z ? 4295128740 : 1461446703485210103287273052203988822378723970341; return PM.swap(key, SwapParams(z, amt, limit), ""); } function unlockCallback(bytes calldata data) external returns (bytes memory) { require(msg.sender == address(PM)); if (data.length == 32 * 7) { (PoolKey memory key, bool z, int256 amt) = abi.decode(data, (PoolKey, bool, int256)); int256 d = _swap(key, z, amt); revert Result(int128(d >> 128), int128(d)); } if (data.length == 32 * 9) return _tokenTrip(data); (PoolKey memory k, bool zf, int256 a, ) = abi.decode(data, (PoolKey, bool, int256, bool)); int256 d1; try this.swapOnce(k, zf, a) returns (int256 r) { d1 = r; } catch (bytes memory e) { revert Stage(0, e); } int128 got = zf ? int128(d1) : int128(d1 >> 128); if (got <= 0) revert Stage(0, "no output"); int256 d2; try this.swapOnce(k, !zf, -int256(int128(got))) returns (int256 r) { d2 = r; } catch (bytes memory e) { revert Stage(1, e); } int128 back = zf ? int128(d2 >> 128) : int128(d2); revert RoundTrip(uint256(int256(got)), back > 0 ? uint256(int256(back)) : 0); } function _tokenTrip(bytes calldata data) internal returns (bytes memory) { (PoolKey memory k, bool z, int256 a, , ) = abi.decode(data, (PoolKey, bool, int256, bool, bool)); (address cin, address cout) = z ? (k.currency0, k.currency1) : (k.currency1, k.currency0); int256 d1; try this.swapOnce(k, z, a) returns (int256 r) { d1 = r; } catch (bytes memory e) { revert Stage(0, e); } uint256 owe = uint256(-int256(z ? int128(d1 >> 128) : int128(d1))); uint256 out = uint256(int256(z ? int128(d1) : int128(d1 >> 128))); _pay(cin, owe); // token keluar dari PoolManager ke Probe (sama seperti pembeli menerima token) uint256 b0 = _bal(cout); try this.takeOnce(cout, out) {} catch (bytes memory e) { revert Stage(1, e); } uint256 received = _bal(cout) - b0; // token dikirim balik ke PoolManager (sama seperti penjual membayar token) uint256 paid; try this.payOnce(cout, received) returns (uint256 p) { paid = p; } catch (bytes memory e) { revert Stage(2, e); } int256 d2; try this.swapOnce(k, !z, -int256(paid)) returns (int256 r) { d2 = r; } catch (bytes memory e) { revert Stage(3, e); } int128 back = z ? int128(d2 >> 128) : int128(d2); revert Settled(out, received, paid, back > 0 ? uint256(int256(back)) : 0); } function _bal(address c) internal view returns (uint256) { return c == address(0) ? address(this).balance : IERC20(c).balanceOf(address(this)); } function _pay(address c, uint256 amt) internal returns (uint256) { if (c == address(0)) return PM.settle{value: amt}(); PM.sync(c); IERC20(c).transfer(address(PM), amt); return PM.settle(); } function takeOnce(address c, uint256 amt) external { require(msg.sender == address(this)); PM.take(c, address(this), amt); } function payOnce(address c, uint256 amt) external returns (uint256) { require(msg.sender == address(this)); return _pay(c, amt); } receive() external payable {} // dipanggil lewat this.* supaya error bisa ditangkap per tahap; unlock tetap milik Probe function swapOnce(PoolKey calldata key, bool z, int256 amt) external returns (int256) { require(msg.sender == address(this)); return _swap(key, z, amt); } } // NullHook: dipasang di alamat hook lewat state override untuk simulasi "hook dimatikan". // Menjawab semua panggilan hook dengan selektor yang benar dan delta nol. contract NullHook { function beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata) external pure returns (bytes4, int256, uint24) { return (this.beforeSwap.selector, 0, 0); } function afterSwap(address, PoolKey calldata, SwapParams calldata, int256, bytes calldata) external pure returns (bytes4, int128) { return (this.afterSwap.selector, 0); } }