6.2k★by mejango
jb-suckers – OpenClaw Skill
jb-suckers is an OpenClaw Skills integration for coding workflows. |
Skill Snapshot
| name | jb-suckers |
| description | | OpenClaw Skills integration. |
| owner | mejango |
| repository | mejango/juicypath: jb-suckers |
| language | Markdown |
| license | MIT |
| topics | |
| security | L1 |
| install | openclaw add @mejango/juicy:jb-suckers |
| last updated | Feb 7, 2026 |
Maintainer

name: jb-suckers description: | 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.
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(), orclaim()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
| Contract | Purpose |
|---|---|
JBSucker | Abstract base with core bridging logic |
JBOptimismSucker | OP Stack bridges (Optimism, Base) |
JBArbitrumSucker | Arbitrum Inbox/Outbox messaging |
JBCCIPSucker | Chainlink CCIP for L2↔L2 |
JBSuckerRegistry | Deploys and tracks sucker pairs |
Querying Sucker Pairs
// 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):
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):
// 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):
// 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)
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
| Status | Meaning | Next Action |
|---|---|---|
pending | Prepared but not sent | Call toRemote() |
claimable | Root arrived, awaiting claim | Call claim() with proof |
claimed | Complete | None |
Emergency Exit
If a bridge becomes non-functional:
// 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:
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
Arbitrum:
- Uses Retryable Tickets
- Dynamic gas pricing
- Requires calculating
maxSubmissionCost
CCIP (L2↔L2):
- Highest fees but most flexible
- Works between any CCIP-supported chains
- Good for Optimism↔Arbitrum, Base↔Arbitrum
Sucker Deprecation
ENABLED → DEPRECATION_PENDING → SENDING_DISABLED → DEPRECATED
DEPRECATION_PENDING: Warning state, still functionalSENDING_DISABLED: Cannot prepare new bridges, can still claimDEPRECATED: Only emergency exits allowed
Verification
- Check sucker state before bridging:
sucker.state() - Verify token is mapped:
sucker.remoteTokenFor(localToken) - Check outbox balance:
sucker.outboxOf(token).balance - Verify claim proof via Juicerkle before submitting
Example
Complete bridge flow from React:
async function bridgeTokens({
sourceChainId,
destChainId,
suckerAddress,
amount,
beneficiary
}: BridgeParams) {
// 1. Approve project token
await writeContract({
address: projectToken,
abi: erc20Abi,
functionName: 'approve',
args: [suckerAddress, amount]
});
// 2. Prepare
await writeContract({
address: suckerAddress,
abi: suckerAbi,
functionName: 'prepare',
args: [amount, beneficiary, 0n, NATIVE_TOKEN]
});
// 3. Execute (can be batched with others)
const fee = await estimateBridgeFee(suckerAddress, NATIVE_TOKEN);
await writeContract({
address: suckerAddress,
abi: suckerAbi,
functionName: 'toRemote',
args: [NATIVE_TOKEN],
value: fee
});
// 4. Wait for root to arrive (check Bendystraw)
// 5. Claim on destination (separate transaction)
}
Notes
- Merkle tree depth is 32 - proofs are always
bytes32[32] - Nonces are monotonically increasing - prevents replay attacks
- Each token has independent outbox/inbox trees
addToBalanceModecan beMANUALorON_CLAIM- Double-spend prevention via executed leaf bitmap
- Emergency hatch uses separate execution namespace
Related Skills
/jb-omnichain-ui- Building omnichain UIs with Relayr and Bendystraw/jb-v5-currency-types- Currency handling for cross-chain projects/jb-bendystraw- Querying cross-chain data
No README available.
Permissions & Security
Security level L1: Low-risk skills with minimal permissions. Review inputs and outputs before running in production.
Requirements
- OpenClaw CLI installed and configured.
- Language: Markdown
- License: MIT
- Topics:
FAQ
How do I install jb-suckers?
Run openclaw add @mejango/juicy:jb-suckers in your terminal. This installs jb-suckers into your OpenClaw Skills catalog.
Does this skill run locally or in the cloud?
OpenClaw Skills execute locally by default. Review the SKILL.md and permissions before running any skill.
Where can I verify the source code?
The source repository is available at https://github.com/openclaw/skills/tree/main/skills/mejango/juicy. Review commits and README documentation before installing.
