dvcrn/openclaw-skills-marketplace/jb-suckers

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Juicebox V5 sucker contracts for cross-chain token bridging. Use when: (1) implementing bridge functionality, (2) understanding prepare/toRemote/claim flow, (3) working with merkle proofs for cross-chain claims, (4) querying sucker pairs from registry, (5) handling emergency exits, (6) debugging "claimable" vs "pending" states, (7) encoding sucker transaction calldata. Covers JBSucker, JBOptimismSucker, JBArbitrumSucker, JBCCIPSucker, and JBSuckerRegistry.

Install

npx vskill@latest install dvcrn/openclaw-skills-marketplace/jb-suckers

SKILL.md

Juicebox V5 Suckers - Cross-Chain Token Bridging

Problem

Bridging project tokens between chains while maintaining their proportional treasury backing requires understanding a complex three-phase protocol with merkle proofs, chain-specific AMBs, and careful state management.

Context / Trigger Conditions

Apply this knowledge when:

  • Building cross-chain bridging UIs
  • Encoding prepare(), toRemote(), or claim() transactions
  • Querying pending/claimable bridge transactions
  • Fetching merkle proofs from Juicerkle
  • Understanding why a bridge is "stuck" in pending state
  • Implementing emergency exit flows
  • Working with JBSuckerRegistry to find bridge routes

Solution

What Are Suckers?

Suckers are specialized bridge contracts that link Juicebox projects across chains and move project tokens AND their proportional treasury backing between them.

Why Suckers are necessary: Project IDs cannot be coordinated across chains—each chain assigns the next available ID independently. If you deploy to Ethereum you might get project #42, and deploying to Optimism might give you project #17. Suckers connect these separate projects so they function as a single "omnichain project" with unified token bridging.

Unlike standard token bridges:

  • Tokens are burned on source chain via cash-out
  • Proportional ETH/USDC moves with the tokens
  • Recipient receives newly minted tokens on destination
  • Treasury value follows the tokens
The Three-Phase Bridge Flow
PHASE 1: PREPARE (Source Chain)
┌─────────────────────────────────────────────────────────────┐
│ User calls: sucker.prepare( │
│ projectTokenCount, // Amount to bridge │
│ beneficiary, // Recipient on remote chain │
│ minTokensReclaimed, // Slippage protection │
│ token // Terminal token (ETH/USDC address) │
│ ) │
│ │
│ What happens: │
│ 1. Project tokens transferred from user to sucker │
│ 2. Sucker calls terminal.cashOutTokensOf() │
│ 3. Receives proportional ETH/USDC from treasury │
│ 4. Creates leaf in outbox merkle tree │
│ 5. Emits InsertToOutboxTree event │
│ │
│ Status: PENDING │
└─────────────────────────────────────────────────────────────┘
│
▼
PHASE 2: EXECUTE (Cross-Chain Message)
┌─────────────────────────────────────────────────────────────┐
│ User/Relayer calls: sucker.toRemote(token) │
│ │
│ What happens: │
│ 1. Computes merkle root of all pending outbox leaves │
│ 2. Increments nonce │
│ 3. Sends JBMessageRoot via AMB: │
│ - OP Stack: IOPMessenger.sendMessage() │
│ - Arbitrum: IInbox.unsafeCreateRetryableTicket() │
│ - CCIP: ICCIPRouter.ccipSend() │
│ 4. Transfers ETH/tokens to peer sucker │
│ 5. Emits RootToRemote event │
│ │
│ Status: CLAIMABLE (on destination) │
└─────────────────────────────────────────────────────────────┘
│
▼
PHASE 3: CLAIM (Destination Chain)
┌─────────────────────────────────────────────────────────────┐
│ User calls: peerSucker.claim(claimData) │
│ │
│ claimData = { │
│ token: address, │
│ leaf: { index, beneficiary, projectTokenCount, │
│ terminalTokenAmount }, │
│ proof: bytes32[32] // Merkle proof from Juicerkle │
│ } │
│ │
│ What happens: │
│ 1. Validates merkle proof against inbox root │
│ 2. Checks leaf not already executed (prevents double-spend) │
│ 3. Marks leaf as executed in bitmap │
│ 4. Mints project tokens to beneficiary │
│ 5. Adds terminal tokens to project balance │
│ 6. Emits Claimed event │
│ │
│ Status: CLAIMED │
└─────────────────────────────────────────────────────────────┘
Key Contracts
ContractPurpose
JBSuckerAbstract base with core bridging logic
JBOptimismSuckerOP Stack bridges (Optimism, Base)
JBArbitrumSuckerArbitrum Inbox/Outbox messaging
JBCCIPSuckerChainlink CCIP for L2↔L2
JBSuckerRegistryDeploys and tracks sucker pairs
Querying Sucker Pairs
javascript
// Get all bridge destinations for a project
const pairs = await publicClient.readContract({
address: JB_SUCKER_REGISTRY,
abi: [{
name: 'suckerPairsOf',
type: 'function',
inputs: [{ name: 'projectId', type: 'uint256' }],
outputs: [{
name: 'pairs',
type: 'tuple[]',
components: [
{ name: 'local', type: 'address' },
{ name: 'remote', type: 'address' },
{ name: 'remoteChainId', type: 'uint256' }
]
}],
stateMutability: 'view'
}],
functionName: 'suckerPairsOf',
args: [projectId]
});
// pairs = [
// { local: '0x...', remote: '0x...', remoteChainId: 10n },
// { local: '0x...', remote: '0x...', remoteChainId: 8453n }
// ]
Encoding Transactions

Prepare (Step 1):

javascript
import { encodeFunctionData } from 'viem';
const prepareData = encodeFunctionData({
abi: [{
name: 'prepare',
type: 'function',
inputs: [
{ name: 'projectTokenCount', type: 'uint256' },
{ name: 'beneficiary', type: 'address' },
{ name: 'minTokensReclaimed', type: 'uint256' },
{ name: 'token', type: 'address' }
],
outputs: [],
stateMutability: 'nonpayable'
}],
functionName: 'prepare',
args: [
parseUnits('100', 18), // 100 project tokens
beneficiaryAddress,
parseUnits('0.9', 18), // 10% slippage allowed
NATIVE_TOKEN // 0xEEEE...EEEe for ETH
]
});
// Send transaction
await walletClient.sendTransaction({
to: suckerAddress,
data: prepareData
});

Execute (Step 2):

javascript
// Estimate fee via simulation (binary search)
async function estimateBridgeFee(sucker, token) {
let low = 0n;
let high = parseUnits('0.04', 18);
for (let i = 0; i < 10; i++) {
const mid = (low + high) / 2n;
try {
await publicClient.simulateContract({
address: sucker,
abi: SUCKER_ABI,
functionName: 'toRemote',
args: [token],
value: mid
});
high = mid; // Success - try lower
} catch {
low = mid; // Failed - try higher
}
}
return (high * 110n) / 100n; // Add 10% buffer
}
const fee = await estimateBridgeFee(suckerAddress, NATIVE_TOKEN);
const toRemoteData = encodeFunctionData({
abi: [{
name: 'toRemote',
type: 'function',
inputs: [{ name: 'token', type: 'address' }],
outputs: [],
stateMutability: 'payable'
}],
functionName: 'toRemote',
args: [NATIVE_TOKEN]
});
await walletClient.sendTransaction({
to: suckerAddress,
data: toRemoteData,
value: fee
});

Claim (Step 3):

javascript
// Fetch proof from Juicerkle
const JUICERKLE_API = 'https://juicerkle-production.up.railway.app';
// NOTE: Addresses must be lowercase for Juicerkle API
const proofResponse = await fetch(`${JUICERKLE_API}/claims`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chainId: destinationChainId,
sucker: peerSuckerAddress.toLowerCase(),
token: NATIVE_TOKEN.toLowerCase(),
beneficiary: userAddress.toLowerCase()
})
});
// Response uses PascalCase
// interface JuicerkleClaim {
// Token: string;
// Leaf: { Index, Beneficiary, ProjectTokenCount, TerminalTokenAmount };
// Proof: number[][]; // Array of 32-byte arrays
// }
const proofs = await proofResponse.json();
const claim = proofs[0];
// Convert Proof from number[][] to bytes32[]
const proofBytes = claim.Proof.map(arr => {
const hex = arr.map(b => b.toString(16).padStart(2, '0')).join('');
return `0x${hex}`;
});
const claimData = encodeFunctionData({
abi: [{
name: 'claim',
type: 'function',
inputs: [{
name: 'claimData',
type: 'tuple',
components: [
{ name: 'token', type: 'address' },
{ name: 'leaf', type: 'tuple', components: [
{ name: 'index', type: 'uint256' },
{ name: 'beneficiary', type: 'address' },
{ name: 'projectTokenCount', type: 'uint256' },
{ name: 'terminalTokenAmount', type: 'uint256' }
]},
{ name: 'proof', type: 'bytes32[32]' }
]
}],
outputs: [],
stateMutability: 'nonpayable'
}],
functionName: 'claim',
args: [{
token: claim.Token,
leaf: {
index: BigInt(claim.Leaf.Index),
beneficiary: claim.Leaf.Beneficiary,
projectTokenCount: BigInt(claim.Leaf.ProjectTokenCount),
terminalTokenAmount: BigInt(claim.Leaf.TerminalTokenAmount)
},
proof: proofBytes
}]
});
await walletClient.sendTransaction({
to: peerSuckerAddress,
data: claimData
});
Querying Bridge Status (Bendystraw)
graphql
query SuckerTransactions($suckerGroupId: String!, $status: suckerTransactionStatus) {
suckerTransactions(
where: { suckerGroupId: $suckerGroupId, status: $status }
orderBy: "createdAt"
orderDirection: "desc"
) {
items {
id
chainId
peerChainId
sucker
peer
beneficiary
projectTokenCount
terminalTokenAmount
token
status # "pending" | "claimable" | "claimed"
index
root
createdAt
}
}
}
State Transitions
StatusMeaningNext Action
pendingPrepared but not sentCall toRemote()
claimableRoot arrived, awaiting claimCall claim() with proof
claimedCompleteNone
Emergency Exit

If a bridge becomes non-functional:

javascript
// 1. Project owner enables emergency hatch
await ownerClient.writeContract({
address: suckerAddress,
abi: SUCKER_ABI,
functionName: 'enableEmergencyHatchFor',
args: [token]
});
// 2. Users can exit locally (no bridging)
// Retrieves funds from outbox without crossing chains
await userClient.writeContract({
address: suckerAddress,
abi: SUCKER_ABI,
functionName: 'exitThroughEmergencyHatch',
args: [claimData] // Same structure as claim()
});
Token Mapping

Projects must map which tokens can be bridged:

javascript
const mapping = {
localToken: USDC_MAINNET,
remoteToken: USDC_OPTIMISM,
minGas: 300000, // Minimum gas for cross-chain call
minBridgeAmount: 10e6 // Minimum 10 USDC to bridge
};
await ownerClient.writeContract({
address: suckerAddress,
abi: SUCKER_ABI,
functionName: 'mapToken',
args: [mapping]
});
Chain-Specific Notes

OP Stack (Optimism, Base):

  • Uses native OP Messenger
  • Lowest fees (~0.0005-0.002 ETH)
  • Fast finality

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
24
Repository forks
9
7-day trend score
16

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/dvcrn/openclaw-skills-marketplace/jb-suckers/badge)](https://verified-skill.com/skills/dvcrn/openclaw-skills-marketplace/jb-suckers)

Something wrong with this skill or its page? Tell us.