From 7f30ffd18dea3399d4be03350d40954cb18c272c Mon Sep 17 00:00:00 2001 From: ztsalexey Date: Mon, 9 Feb 2026 20:51:47 -0700 Subject: [PATCH 1/6] refactor: remove bot whitelist from Claw2ClawHook contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the allowedBots mapping, onlyWhitelisted modifier, addBot/removeBot admin functions, and the whitelist check in beforeSwap. The hook is now permissionless — any address can post orders and trigger P2P matching. --- contracts/src/Claw2ClawHook.sol | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/contracts/src/Claw2ClawHook.sol b/contracts/src/Claw2ClawHook.sol index 6a6dfb2..e52886c 100644 --- a/contracts/src/Claw2ClawHook.sol +++ b/contracts/src/Claw2ClawHook.sol @@ -12,7 +12,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @title Claw2ClawHook -/// @notice Uniswap v4 hook enabling P2P order matching between whitelisted bots. +/// @notice Uniswap v4 hook enabling P2P order matching between bots. /// @dev Uses CustomCurve pattern: take input from PM, settle output to PM. contract Claw2ClawHook is IHooks, ReentrancyGuard { using BalanceDeltaLibrary for BalanceDelta; @@ -34,15 +34,12 @@ contract Claw2ClawHook is IHooks, ReentrancyGuard { event OrderCancelled(uint256 indexed orderId, address indexed maker); event OrderExpired(uint256 indexed orderId, address indexed maker); event P2PTrade(uint256 indexed orderId, address indexed maker, address indexed taker, address tokenIn, address tokenOut, uint128 amountIn, uint128 amountOut); - event BotAdded(address indexed bot); - event BotRemoved(address indexed bot); event AdminChanged(address indexed oldAdmin, address indexed newAdmin); event PendingAdminSet(address indexed pendingAdmin); event RefundFailed(uint256 indexed orderId, address indexed maker); // Errors error NotAdmin(); - error NotWhitelisted(); error HookNotImplemented(); error OrderNotFound(); error OrderNotActive(); @@ -65,7 +62,6 @@ contract Claw2ClawHook is IHooks, ReentrancyGuard { address public admin; address public pendingAdmin; // M-2 fix: two-step admin transfer IPoolManager public immutable poolManager; - mapping(address => bool) public allowedBots; uint256 public nextOrderId; mapping(uint256 => Order) public orders; mapping(bytes32 => uint256[]) public poolOrders; @@ -86,14 +82,7 @@ contract Claw2ClawHook is IHooks, ReentrancyGuard { if (msg.sender != address(poolManager)) revert OnlyPoolManager(); _; } - modifier onlyWhitelisted() { - if (!allowedBots[msg.sender]) revert NotWhitelisted(); - _; - } - // Admin - function addBot(address bot) external onlyAdmin { allowedBots[bot] = true; emit BotAdded(bot); } - function removeBot(address bot) external onlyAdmin { allowedBots[bot] = false; emit BotRemoved(bot); } // M-2 fix: two-step admin transfer function setAdmin(address newAdmin) external onlyAdmin { @@ -111,7 +100,7 @@ contract Claw2ClawHook is IHooks, ReentrancyGuard { // Order Book function postOrder(PoolKey calldata key, bool sellToken0, uint128 amountIn, uint128 minAmountOut, uint256 duration) - external onlyWhitelisted nonReentrant returns (uint256 orderId) + external nonReentrant returns (uint256 orderId) { if (amountIn == 0 || minAmountOut == 0) revert InvalidAmounts(); if (duration == 0) revert InvalidDuration(); @@ -161,10 +150,6 @@ contract Claw2ClawHook is IHooks, ReentrancyGuard { function beforeSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) external onlyPoolManager nonReentrant returns (bytes4, BeforeSwapDelta, uint24) { - // I-1 fix: don't block non-whitelisted senders -- fall through to AMM instead - if (!allowedBots[sender]) { - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } // I-2 fix: don't revert on exact-output, just fall through to AMM if (params.amountSpecified >= 0) { return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); From f119439f4daff3d9157c3a5b261f1954cc7e3c20 Mon Sep 17 00:00:00 2001 From: ztsalexey Date: Mon, 9 Feb 2026 20:51:51 -0700 Subject: [PATCH 2/6] test: remove whitelist-related tests and setUp whitelisting Drop test_addBot, test_removeBot, test_postOrder_revert_notWhitelisted, test_beforeSwap_nonWhitelisted_fallsThrough, and the setUp addBot calls. All 27 remaining tests pass. --- contracts/test/Claw2ClawHook.t.sol | 70 ------------------------------ 1 file changed, 70 deletions(-) diff --git a/contracts/test/Claw2ClawHook.t.sol b/contracts/test/Claw2ClawHook.t.sol index 33a8dc1..95d5b44 100644 --- a/contracts/test/Claw2ClawHook.t.sol +++ b/contracts/test/Claw2ClawHook.t.sol @@ -49,12 +49,6 @@ contract Claw2ClawHookTest is Test { hooks: IHooks(address(hook)) }); - // Whitelist bots - vm.startPrank(admin); - hook.addBot(botA); - hook.addBot(botB); - vm.stopPrank(); - // Mint tokens to bots token0.mint(botA, 1000 ether); token1.mint(botA, 1000 ether); @@ -79,47 +73,6 @@ contract Claw2ClawHookTest is Test { // ── Admin tests ───────────────────────────────────────────────── - function test_addBot() public { - address newBot = address(0x123); - assertFalse(hook.allowedBots(newBot)); - - vm.prank(admin); - hook.addBot(newBot); - - assertTrue(hook.allowedBots(newBot)); - } - - function test_addBot_emitsEvent() public { - address newBot = address(0x123); - - vm.expectEmit(true, false, false, false); - emit Claw2ClawHook.BotAdded(newBot); - - vm.prank(admin); - hook.addBot(newBot); - } - - function test_removeBot() public { - vm.prank(admin); - hook.removeBot(botA); - - assertFalse(hook.allowedBots(botA)); - } - - function test_removeBot_emitsEvent() public { - vm.expectEmit(true, false, false, false); - emit Claw2ClawHook.BotRemoved(botA); - - vm.prank(admin); - hook.removeBot(botA); - } - - function test_addBot_revert_notAdmin() public { - vm.prank(notBot); - vm.expectRevert(Claw2ClawHook.NotAdmin.selector); - hook.addBot(address(0x123)); - } - // M-2: two-step admin transfer function test_setAdmin_twoStep() public { vm.prank(admin); @@ -199,12 +152,6 @@ contract Claw2ClawHookTest is Test { hook.postOrder(poolKey, true, 100 ether, 90 ether, 3600); } - function test_postOrder_revert_notWhitelisted() public { - vm.prank(notBot); - vm.expectRevert(Claw2ClawHook.NotWhitelisted.selector); - hook.postOrder(poolKey, true, 100 ether, 90 ether, 3600); - } - function test_postOrder_revert_zeroAmount() public { vm.prank(botA); vm.expectRevert(Claw2ClawHook.InvalidAmounts.selector); @@ -504,23 +451,6 @@ contract Claw2ClawHookTest is Test { assertEq(mockPM.getTakeCallCount(), 0); } - // I-1: non-whitelisted senders fall through to AMM - function test_beforeSwap_nonWhitelisted_fallsThrough() public { - IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ - zeroForOne: true, - amountSpecified: -100 ether, - sqrtPriceLimitX96: 0 - }); - - vm.prank(address(mockPM)); - (bytes4 selector, BeforeSwapDelta delta, uint24 fee) = - hook.beforeSwap(notBot, poolKey, params, ""); - - assertEq(selector, IHooks.beforeSwap.selector); - assertEq(BeforeSwapDelta.unwrap(delta), 0, "Non-whitelisted should get ZERO_DELTA"); - assertEq(fee, 0); - } - // I-2: exact-output swaps fall through to AMM function test_beforeSwap_exactOutput_fallsThrough() public { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ From 52619716bd73d23ac57fb3600969afa25d93eb0d Mon Sep 17 00:00:00 2001 From: ztsalexey Date: Mon, 9 Feb 2026 20:51:55 -0700 Subject: [PATCH 3/6] refactor: remove ensureWhitelisted from backend P2P service Remove the ensureWhitelisted function, its calls in postP2POrder and executeP2PSwap, and the allowedBots/addBot ABI entries. The contract no longer requires whitelisting so the backend no longer needs to auto-whitelist bots before P2P actions. --- backend/src/services/p2p.ts | 76 +------------------------------------ 1 file changed, 1 insertion(+), 75 deletions(-) diff --git a/backend/src/services/p2p.ts b/backend/src/services/p2p.ts index b5a0785..ff0c5b6 100644 --- a/backend/src/services/p2p.ts +++ b/backend/src/services/p2p.ts @@ -229,20 +229,6 @@ const HOOK_ABI = [ { name: 'poolId', type: 'bytes32' }, ], }, - { - name: 'allowedBots', - type: 'function', - stateMutability: 'view', - inputs: [{ name: 'bot', type: 'address' }], - outputs: [{ name: '', type: 'bool' }], - }, - { - name: 'addBot', - type: 'function', - stateMutability: 'nonpayable', - inputs: [{ name: 'bot', type: 'address' }], - outputs: [], - }, { name: 'nextOrderId', type: 'function', @@ -337,9 +323,6 @@ export async function postP2POrder(params: PostOrderParams): Promise { - const adminKey = process.env.HOOK_ADMIN_PRIVATE_KEY - if (!adminKey) { - console.warn('[P2P] HOOK_ADMIN_PRIVATE_KEY not set — cannot auto-whitelist') - return - } - - const publicClient = createBlockchainClient(CHAIN_IDS.BASE) - - // Check if already whitelisted - const isAllowed = await publicClient.readContract({ - address: HOOK_ADDRESS, - abi: HOOK_ABI, - functionName: 'allowedBots', - args: [botAddress as Hex], - }) - - if (isAllowed) return - - // Whitelist via admin key (direct EOA tx, not sponsored) - console.log(`[P2P] Whitelisting bot ${botAddress} on hook...`) - - const admin = privateKeyToAccount(adminKey as Hex) - - const walletClient = createWalletClient({ - account: admin, - chain: base, - transport: http(getRpcUrl(CHAIN_IDS.BASE)), - }) - - const addBotData = encodeFunctionData({ - abi: HOOK_ABI, - functionName: 'addBot', - args: [botAddress as Hex], - }) - - const txHash = await walletClient.sendTransaction({ - to: HOOK_ADDRESS, - data: addBotData, - value: 0n, - }) - - console.log(`[P2P] Bot whitelisted, tx: ${txHash}`) -} - // ── Token Registry Management ── From 729d0d85fc42eb627172b8144d9204adc986a590 Mon Sep 17 00:00:00 2001 From: ztsalexey Date: Mon, 9 Feb 2026 20:51:59 -0700 Subject: [PATCH 4/6] docs: update documentation and scripts for permissionless hook Remove whitelist references from READMEs, .env.example, and deployment scripts. The hook is now permissionless so addBot calls and whitelist setup steps are no longer needed. --- .env.example | 4 ++-- README.md | 2 +- contracts/README.md | 31 +++++++++----------------- contracts/script/DeployClaw2Claw.s.sol | 10 +-------- contracts/script/DeployMainnet.s.sol | 6 +---- contracts/script/TestP2P.s.sol | 6 ++--- contracts/script/TestP2PMainnet.s.sol | 9 +++----- 7 files changed, 21 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index 898f3ae..5a811f6 100644 --- a/.env.example +++ b/.env.example @@ -85,8 +85,8 @@ NEXT_PUBLIC_ENS_MAINNET=false # P2P Trading (Claw2ClawHook on Base) # =================================== -# Admin private key that can whitelist bots on the hook contract -# Required for auto-whitelisting new bots during their first P2P action +# Admin private key for pool initialization and contract management +# Required for initializing new trading pools HOOK_ADMIN_PRIVATE_KEY= # Contract addresses (defaults shown — override if redeployed) diff --git a/README.md b/README.md index 6970728..5da77b0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ claw2claw/ ## ⛓️ Smart Contracts -The `contracts/` directory contains **Claw2ClawHook** — a Uniswap v4 hook enabling P2P order matching between whitelisted AI bots. +The `contracts/` directory contains **Claw2ClawHook** — a Uniswap v4 hook enabling permissionless P2P order matching between AI bots. ### Base Mainnet (Production) diff --git a/contracts/README.md b/contracts/README.md index 5289a65..d3b3a1c 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,19 +1,18 @@ # Claw2ClawHook — P2P Order Matching on Uniswap v4 -A Uniswap v4 hook that enables peer-to-peer (P2P) order matching between whitelisted bots, bypassing pool liquidity when matching orders are available. +A Uniswap v4 hook that enables permissionless peer-to-peer (P2P) order matching between bots, bypassing pool liquidity when matching orders are available. **Trade CLAW 🐾 for ZUG ⚡ — Peer-to-peer, on-chain, on Uniswap v4!** ## Overview -Claw2ClawHook acts as an on-chain order book integrated directly into a Uniswap v4 pool. Whitelisted bots can post orders, and when another bot attempts to swap through the pool, the hook checks for matching orders and executes P2P trades directly between the maker and taker. +Claw2ClawHook acts as an on-chain order book integrated directly into a Uniswap v4 pool. Any bot can post orders, and when another bot attempts to swap through the pool, the hook checks for matching orders and executes P2P trades directly between the maker and taker. ### Key Features - **On-chain Order Book**: Orders stored on-chain with expiry times - **P2P Matching**: Direct token transfers between maker and taker when orders match - **Fallback to Pool**: If no matching order exists, swaps fall through to normal pool liquidity -- **Bot Whitelist**: Only authorized bots can post orders and swap - **BeforeSwapDelta**: Uses custom accounting to bypass pool liquidity for P2P trades ## Architecture @@ -36,7 +35,7 @@ Claw2ClawHook acts as an on-chain order book integrated directly into a Uniswap │ 3. beforeSwap Hook │ - ├─> Check whitelist (Bot B authorized?) + ├─> Check for matching orders ├─> Search for matching orders │ ├─> Check: opposite direction? │ ├─> Check: sufficient amount? @@ -172,7 +171,7 @@ poolManager.swap( The `beforeSwap` hook: -1. **Checks whitelist** - Reverts if Bot B is not authorized +1. **Validates swap params** - Only handles exact-input swaps 2. **Searches orders** - Iterates through active orders for this pool 3. **Validates match**: - Direction: Bot A sells token0, Bot B sells token1 (opposite) ✓ @@ -251,11 +250,11 @@ This means our tests verify the **complete P2P settlement flow**: | Category | Tests | What's Verified | |----------|-------|-----------------| -| **Admin** | 7 | addBot, removeBot, two-step setAdmin/acceptAdmin, events, access control | -| **Order Posting** | 6 | Success, escrow transfer, events, zero-amount/duration reverts, max duration | +| **Admin** | 2 | two-step setAdmin/acceptAdmin | +| **Order Posting** | 5 | Success, escrow transfer, events, zero-amount/duration reverts, max duration | | **Order Cancellation** | 5 | Success, refund, events, unauthorized, double-cancel, cross-pool theft prevention | | **P2P Matching** | 6 | Full settlement (both directions), token balances, multi-order, skip-filled | -| **No Match** | 4 | Same direction, insufficient amount, expired orders, non-whitelisted passthrough | +| **No Match** | 2 | Same direction, insufficient amount | | **View Functions** | 1 | getPoolOrders | | **afterSwap** | 1 | No-op verification | | **Access Control** | 2 | Non-PM caller revert, exact-output fallthrough | @@ -283,15 +282,7 @@ This means our tests verify the **complete P2P settlement flow**: ## Usage Example -### 1. Whitelist Bots - -```solidity -// Admin whitelists Bot A and Bot B -hook.addBot(botA); -hook.addBot(botB); -``` - -### 2. Bot A Posts Order +### 1. Bot A Posts Order ```solidity // Approve hook to spend CLAW tokens @@ -307,7 +298,7 @@ uint256 orderId = hook.postOrder( ); ``` -### 3. Bot B Swaps (P2P Match) +### 2. Bot B Swaps (P2P Match) ```solidity // Approve ZUG tokens for swap @@ -328,7 +319,7 @@ poolSwapTest.swap( // Result: Bot A and Bot B traded CLAW<>ZUG directly, pool liquidity not touched ``` -### 4. Cancel Order (Optional) +### 3. Cancel Order (Optional) ```solidity // Bot A cancels unfilled order @@ -338,7 +329,7 @@ hook.cancelOrder(orderId, poolKey); ## Security Considerations -- **Whitelist Only**: Only authorized bots can interact +- **Permissionless**: Any address can post orders and trade - **Expiry Protection**: Orders automatically expire - **Maker Authorization**: Only maker can cancel their order - **Amount Validation**: Ensures maker's minAmountOut is satisfied diff --git a/contracts/script/DeployClaw2Claw.s.sol b/contracts/script/DeployClaw2Claw.s.sol index 0cec062..fcfaab3 100644 --- a/contracts/script/DeployClaw2Claw.s.sol +++ b/contracts/script/DeployClaw2Claw.s.sol @@ -106,15 +106,7 @@ contract DeployClaw2Claw is Script { console.log("Claw2ClawHook:", hookAddr); require(uint160(hookAddr) & FLAG_MASK == REQUIRED_FLAGS, "Flag mismatch"); - Claw2ClawHook hook = Claw2ClawHook(hookAddr); - - // --- 3. Whitelist deployer + helpers as bots --- - hook.addBot(deployer); - hook.addBot(POOL_SWAP_TEST); - hook.addBot(POOL_MODIFY_LIQUIDITY_TEST); - console.log("Bots whitelisted"); - - // --- 4. Initialize pool --- + // --- 3. Initialize pool --- address token0; address token1; if (address(claw) < address(zug)) { diff --git a/contracts/script/DeployMainnet.s.sol b/contracts/script/DeployMainnet.s.sol index 967c577..3d4c7f2 100644 --- a/contracts/script/DeployMainnet.s.sol +++ b/contracts/script/DeployMainnet.s.sol @@ -71,10 +71,6 @@ contract DeployMainnet is Script { console.log("Claw2ClawHook deployed:", hookAddr); require(uint160(hookAddr) & FLAG_MASK == REQUIRED_FLAGS, "Flag mismatch"); - // 4. Whitelist deployer as initial bot - Claw2ClawHook hook = Claw2ClawHook(hookAddr); - hook.addBot(deployer); - console.log("Deployer whitelisted as bot"); vm.stopBroadcast(); @@ -88,6 +84,6 @@ contract DeployMainnet is Script { console.log(" 1. Verify on BaseScan:"); console.log(" forge verify-contract Claw2ClawHook --chain base"); console.log(" 2. Initialize a pool with real tokens"); - console.log(" 3. Whitelist bot wallets via addBot()"); + console.log(" 3. Fund bot wallets and start trading"); } } diff --git a/contracts/script/TestP2P.s.sol b/contracts/script/TestP2P.s.sol index 564d41c..bcd83dd 100644 --- a/contracts/script/TestP2P.s.sol +++ b/contracts/script/TestP2P.s.sol @@ -62,10 +62,8 @@ contract TestP2P is Script { Claw2ClawHook hook = Claw2ClawHook(HOOK); - // --- Step 1: Admin whitelists Bot A and Bot B --- + // --- Step 1: Fund Bot A and Bot B --- vm.startBroadcast(deployerKey); - hook.addBot(botA); - hook.addBot(botB); // Send tokens to bots MockToken(token0).transfer(botA, 10_000 ether); MockToken(token1).transfer(botA, 10_000 ether); @@ -77,7 +75,7 @@ contract TestP2P is Script { require(s1 && s2, "ETH transfer failed"); vm.stopBroadcast(); - console.log("--- Step 1: Bots whitelisted, funded ---"); + console.log("--- Step 1: Bots funded ---"); console.log("Bot A token0 balance:", IERC20(token0).balanceOf(botA)); console.log("Bot A token1 balance:", IERC20(token1).balanceOf(botA)); console.log("Bot B token0 balance:", IERC20(token0).balanceOf(botB)); diff --git a/contracts/script/TestP2PMainnet.s.sol b/contracts/script/TestP2PMainnet.s.sol index 01b9b6f..c37e2a6 100644 --- a/contracts/script/TestP2PMainnet.s.sol +++ b/contracts/script/TestP2PMainnet.s.sol @@ -108,7 +108,7 @@ contract TestP2PMainnet is Script { }); // ============================================= - // PHASE 1: Admin — deploy router & whitelist + // PHASE 1: Admin — deploy router // ============================================= console.log(""); console.log("--- Phase 1: Admin setup ---"); @@ -131,11 +131,6 @@ contract TestP2PMainnet is Script { console.log("Pool already initialized, skipping"); } - Claw2ClawHook hook = Claw2ClawHook(HOOK); - hook.addBot(botA); - hook.addBot(botB); - hook.addBot(address(router)); - console.log("Bots + router whitelisted"); vm.stopBroadcast(); @@ -147,6 +142,8 @@ contract TestP2PMainnet is Script { vm.startBroadcast(botAKey); + Claw2ClawHook hook = Claw2ClawHook(HOOK); + uint256 usdcBal = IERC20(USDC).balanceOf(botA); console.log("Bot A USDC balance:", usdcBal); From 9009923f46b42899094c42851189b56ed2071ba1 Mon Sep 17 00:00:00 2001 From: ztsalexey Date: Mon, 9 Feb 2026 20:57:19 -0700 Subject: [PATCH 5/6] refactor: extract SimpleSwapRouter to its own source file Move SimpleSwapRouter from inline in TestP2PMainnet.s.sol to src/SimpleSwapRouter.sol so it can be imported and reused. --- contracts/script/TestP2PMainnet.s.sol | 60 +------------------------ contracts/src/SimpleSwapRouter.sol | 64 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 58 deletions(-) create mode 100644 contracts/src/SimpleSwapRouter.sol diff --git a/contracts/script/TestP2PMainnet.s.sol b/contracts/script/TestP2PMainnet.s.sol index c37e2a6..d7dc5f5 100644 --- a/contracts/script/TestP2PMainnet.s.sol +++ b/contracts/script/TestP2PMainnet.s.sol @@ -6,13 +6,12 @@ pragma solidity ^0.8.26; import "forge-std/Script.sol"; import {Claw2ClawHook} from "../src/Claw2ClawHook.sol"; +import {SimpleSwapRouter} from "../src/SimpleSwapRouter.sol"; import {IPoolManager} from "@v4-core/interfaces/IPoolManager.sol"; import {PoolKey} from "@v4-core/types/PoolKey.sol"; -import {PoolId} from "@v4-core/types/PoolId.sol"; -import {Currency, CurrencyLibrary} from "@v4-core/types/Currency.sol"; +import {Currency} from "@v4-core/types/Currency.sol"; import {IHooks} from "@v4-core/interfaces/IHooks.sol"; import {TickMath} from "@v4-core/libraries/TickMath.sol"; -import {TransientStateLibrary} from "@v4-core/libraries/TransientStateLibrary.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; interface IWETH { @@ -21,61 +20,6 @@ interface IWETH { function balanceOf(address) external view returns (uint256); } -/// @dev Minimal swap router that handles unlock callback for v4 swaps -contract SimpleSwapRouter { - using CurrencyLibrary for Currency; - using TransientStateLibrary for IPoolManager; - - IPoolManager public immutable PM; - - struct SwapContext { - PoolKey key; - IPoolManager.SwapParams params; - address caller; - } - - constructor(IPoolManager _pm) { - PM = _pm; - } - - function swap(PoolKey calldata key, IPoolManager.SwapParams calldata params) external { - PM.unlock(abi.encode(SwapContext({key: key, params: params, caller: msg.sender}))); - } - - function unlockCallback(bytes calldata data) external returns (bytes memory) { - require(msg.sender == address(PM), "only PM"); - - SwapContext memory ctx = abi.decode(data, (SwapContext)); - PM.swap(ctx.key, ctx.params, bytes("")); - - // After swap (which may have been fully handled by the hook), check - // what this router actually owes/is owed by looking at the PM's ledger. - int256 d0 = PM.currencyDelta(address(this), ctx.key.currency0); - int256 d1 = PM.currencyDelta(address(this), ctx.key.currency1); - - if (d0 < 0) { - // We owe token0 to PM: sync → transferFrom → settle - uint256 owed = uint256(-d0); - PM.sync(ctx.key.currency0); - IERC20(Currency.unwrap(ctx.key.currency0)).transferFrom(ctx.caller, address(PM), owed); - PM.settle(); - } else if (d0 > 0) { - PM.take(ctx.key.currency0, ctx.caller, uint256(d0)); - } - - if (d1 < 0) { - uint256 owed = uint256(-d1); - PM.sync(ctx.key.currency1); - IERC20(Currency.unwrap(ctx.key.currency1)).transferFrom(ctx.caller, address(PM), owed); - PM.settle(); - } else if (d1 > 0) { - PM.take(ctx.key.currency1, ctx.caller, uint256(d1)); - } - - return bytes(""); - } -} - contract TestP2PMainnet is Script { // --- Base mainnet addresses --- address constant HOOK = 0x9114Ff08A837d0F8F9db23234Bf99794131FC188; diff --git a/contracts/src/SimpleSwapRouter.sol b/contracts/src/SimpleSwapRouter.sol new file mode 100644 index 0000000..9abcbef --- /dev/null +++ b/contracts/src/SimpleSwapRouter.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IPoolManager} from "@v4-core/interfaces/IPoolManager.sol"; +import {PoolKey} from "@v4-core/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "@v4-core/types/Currency.sol"; +import {TransientStateLibrary} from "@v4-core/libraries/TransientStateLibrary.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +/// @title SimpleSwapRouter +/// @notice Minimal swap router that handles unlock callback for Uniswap v4 swaps +contract SimpleSwapRouter { + using CurrencyLibrary for Currency; + using TransientStateLibrary for IPoolManager; + + IPoolManager public immutable PM; + + struct SwapContext { + PoolKey key; + IPoolManager.SwapParams params; + address caller; + } + + constructor(IPoolManager _pm) { + PM = _pm; + } + + function swap(PoolKey calldata key, IPoolManager.SwapParams calldata params) external { + PM.unlock(abi.encode(SwapContext({key: key, params: params, caller: msg.sender}))); + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + require(msg.sender == address(PM), "only PM"); + + SwapContext memory ctx = abi.decode(data, (SwapContext)); + PM.swap(ctx.key, ctx.params, bytes("")); + + // After swap (which may have been fully handled by the hook), check + // what this router actually owes/is owed by looking at the PM's ledger. + int256 d0 = PM.currencyDelta(address(this), ctx.key.currency0); + int256 d1 = PM.currencyDelta(address(this), ctx.key.currency1); + + if (d0 < 0) { + // We owe token0 to PM: sync → transferFrom → settle + uint256 owed = uint256(-d0); + PM.sync(ctx.key.currency0); + IERC20(Currency.unwrap(ctx.key.currency0)).transferFrom(ctx.caller, address(PM), owed); + PM.settle(); + } else if (d0 > 0) { + PM.take(ctx.key.currency0, ctx.caller, uint256(d0)); + } + + if (d1 < 0) { + uint256 owed = uint256(-d1); + PM.sync(ctx.key.currency1); + IERC20(Currency.unwrap(ctx.key.currency1)).transferFrom(ctx.caller, address(PM), owed); + PM.settle(); + } else if (d1 > 0) { + PM.take(ctx.key.currency1, ctx.caller, uint256(d1)); + } + + return bytes(""); + } +} From 2dbcd48d8a1280df456b70ff2d7e431db21daa49 Mon Sep 17 00:00:00 2001 From: ztsalexey Date: Mon, 9 Feb 2026 20:57:52 -0700 Subject: [PATCH 6/6] fix: return 500 on wallet creation failure instead of creating walletless bot Previously a failed createBotWallet call was silently caught and the bot was registered without a wallet. Now the endpoint returns a 500 error so the caller knows the operation failed. --- backend/src/routes/bots.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/bots.ts b/backend/src/routes/bots.ts index 1aea12f..cd04790 100644 --- a/backend/src/routes/bots.ts +++ b/backend/src/routes/bots.ts @@ -185,8 +185,6 @@ export async function botsRoutes(fastify: FastifyInstance) { let walletAddress: string | null = null let encryptedWalletKey: string | null = null - let walletError: string | null = null - if (createWallet && isAAConfigured()) { try { const wallet = await createBotWallet() @@ -194,7 +192,10 @@ export async function botsRoutes(fastify: FastifyInstance) { encryptedWalletKey = wallet.encryptedPrivateKey } catch (error) { console.error('Wallet creation failed:', error) - walletError = error instanceof Error ? error.message : String(error) + return reply.status(500).send({ + error: 'Wallet creation failed', + details: error instanceof Error ? error.message : String(error), + }) } } @@ -263,7 +264,6 @@ export async function botsRoutes(fastify: FastifyInstance) { ...(walletAddress && { walletInfo: `Your bot wallet is ready. Deposit assets to: ${walletAddress}` }), - ...(walletError && { walletError }), ...(createEns && isEnsConfigured() && walletAddress && { ensInfo: 'ENS subdomain is being minted on-chain. Poll GET /api/bots/me to check when ensName is ready.', }),