Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions backend/src/routes/bots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,17 @@ 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()
walletAddress = wallet.walletAddress
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),
})
}
}

Expand Down Expand Up @@ -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.',
}),
Expand Down
76 changes: 1 addition & 75 deletions backend/src/services/p2p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -337,9 +323,6 @@ export async function postP2POrder(params: PostOrderParams): Promise<PostOrderRe
const sellAmount = BigInt(params.sellAmount)
const minBuyAmount = BigInt(params.minBuyAmount)

// Ensure bot is whitelisted on the hook
await ensureWhitelisted(params.botAddress)

// Create sponsored AA client (Pimlico pays gas)
const { client: smartClient, signedAuthorization } = await createSponsoredClient(
params.encryptedPrivateKey, CHAIN_IDS.BASE
Expand Down Expand Up @@ -555,12 +538,6 @@ export async function executeP2PSwap(params: MatchOrderParams): Promise<MatchOrd
const poolKey = computePoolKey(payInfo.address, receiveInfo.address)
const payAmount = BigInt(params.payAmount)

// Ensure bot is whitelisted
await ensureWhitelisted(params.botAddress)

// Also ensure the router is whitelisted (it's the msg.sender in beforeSwap)
await ensureWhitelisted(SWAP_ROUTER_ADDRESS)

const { client: smartClient, signedAuthorization } = await createSponsoredClient(
params.encryptedPrivateKey, CHAIN_IDS.BASE
)
Expand Down Expand Up @@ -851,7 +828,7 @@ export interface InitPoolResult {

/**
* Initialize a new Uniswap v4 pool with our hook attached.
* Uses the admin key (same as for whitelisting).
* Uses the admin key for pool initialization.
* Anyone can call poolManager.initialize — it's permissionless on-chain,
* but we gate it behind the admin key for consistency.
*/
Expand Down Expand Up @@ -910,57 +887,6 @@ export async function initializePool(
}


// ── Bot Whitelisting ──

/**
* Check if a bot is whitelisted on the hook, auto-whitelist if not.
* Uses the admin key from env to call addBot().
*/
export async function ensureWhitelisted(botAddress: string): Promise<void> {
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 ──

Expand Down
31 changes: 11 additions & 20 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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) ✓
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 1 addition & 9 deletions contracts/script/DeployClaw2Claw.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 1 addition & 5 deletions contracts/script/DeployMainnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -88,6 +84,6 @@ contract DeployMainnet is Script {
console.log(" 1. Verify on BaseScan:");
console.log(" forge verify-contract <HOOK_ADDR> 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");
}
}
6 changes: 2 additions & 4 deletions contracts/script/TestP2P.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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));
Expand Down
Loading