@bananapus/suckers-v6 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,60 +1,51 @@
1
1
  # Juicebox Suckers
2
2
 
3
- Cross-chain bridging for Juicebox V6 projects. Suckers let users cash out project tokens on one chain, move the backing funds across a bridge, and mint the same number of project tokens on another chain -- all via merkle-tree-based claims and chain-specific bridges.
3
+ `@bananapus/suckers-v6` provides cross-chain bridging for Juicebox project tokens and the terminal assets that back them. A pair of suckers lets users burn on one chain, move value across a bridge, and mint the same project token representation on another chain.
4
4
 
5
- _If you're having trouble understanding this contract, take a look at the [core protocol contracts](https://github.com/Bananapus/nana-core-v6) and the [documentation](https://docs.juicebox.money/) first. If you have questions, reach out on [Discord](https://discord.com/invite/ErQYmth4dS)._
5
+ Docs: <https://docs.juicebox.money>
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
7
 
7
- ## What are Suckers?
8
+ The codebase includes multiple bridge variants, but the canonical deployment and discovery tooling in this repo is narrower than the full runtime surface. Treat the deployment scripts and helper libraries as the source of truth for what is operationally supported today.
8
9
 
9
- `JBSucker` contracts are deployed in pairs, with one on each network being bridged to or from. The [`JBSucker`](src/JBSucker.sol) contract implements core logic, and is extended by network-specific implementations adapted to each bridge:
10
+ ## Overview
10
11
 
11
- | Sucker | Networks | Description |
12
- |--------|----------|-------------|
13
- | [`JBCCIPSucker`](src/JBCCIPSucker.sol) | Any CCIP-connected chains | Uses [Chainlink CCIP](https://docs.chain.link/ccip) (`ccipSend`/`ccipReceive`). Handles native token wrapping/unwrapping for chains with different native assets. Supports Ethereum, Optimism, Base, Arbitrum, Polygon, Avalanche, BNB Chain, and their testnets. |
14
- | [`JBOptimismSucker`](src/JBOptimismSucker.sol) | Ethereum and Optimism | Uses the [OP Standard Bridge](https://docs.optimism.io/builders/app-developers/bridging/standard-bridge) and the [OP Messenger](https://docs.optimism.io/builders/app-developers/bridging/messaging). |
15
- | [`JBBaseSucker`](src/JBBaseSucker.sol) | Ethereum and Base | A thin wrapper around `JBOptimismSucker` with Base chain IDs. |
16
- | [`JBCeloSucker`](src/JBCeloSucker.sol) | Ethereum and Celo | Extends `JBOptimismSucker` for Celo, an OP Stack chain with a custom gas token (CELO, not ETH). Wraps native ETH to WETH before bridging as ERC-20 via the OP Standard Bridge, and unwraps WETH back to native ETH on the receiving end. Allows `NATIVE_TOKEN` to map to ERC-20 addresses. |
17
- | [`JBArbitrumSucker`](src/JBArbitrumSucker.sol) | Ethereum and Arbitrum | Uses the [Arbitrum Inbox](https://docs.arbitrum.io/build-decentralized-apps/cross-chain-messaging) and the [Arbitrum Gateway](https://docs.arbitrum.io/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/get-started). Handles L1<->L2 retryable tickets and address aliasing. |
12
+ Suckers bridge a project by tracking claims in append-only Merkle trees:
18
13
 
19
- Suckers use two [merkle trees](https://en.wikipedia.org/wiki/Merkle_tree) to track project token claims associated with each terminal token they support:
14
+ - users call `prepare` to burn tokens and create a bridge claim in the local outbox tree
15
+ - anyone can relay the current root to the peer chain with `toRemote`
16
+ - claimants prove inclusion against the peer inbox tree to mint on the destination chain
20
17
 
21
- - The **outbox tree** tracks tokens on the local chain -- the network that the sucker is on.
22
- - The **inbox tree** tracks tokens which have been bridged from the peer chain -- the network that the sucker's peer is on.
18
+ The base implementation is extended for multiple bridge families so the same project model can work across different networks.
23
19
 
24
- For example, a sucker which supports bridging ETH and USDC would have four trees -- an inbox and outbox tree for each token. These trees are append-only, and when they're bridged over to the other chain, they aren't deleted -- they only update the remote inbox tree with the latest root.
20
+ Use this repo when the requirement is canonical project-token movement across chains. Do not use it if the project is single-chain or if the bridge assumptions for the target networks are unacceptable.
25
21
 
26
- To insert project tokens into the outbox tree, users call `JBSucker.prepare(...)` with the amount of project tokens to bridge and the terminal token to bridge with them. The sucker cashes out those project tokens to reclaim the chosen terminal token from the project's primary terminal. Then it inserts a claim with this information into the outbox tree.
22
+ The main idea is not "bridge the token contract." The main idea is "bridge a Juicebox cash-out claim plus enough information to recreate the project-token position on the remote chain."
27
23
 
28
- Anyone can bridge an outbox tree to the peer chain by calling `JBSucker.toRemote(token)`. The outbox tree then _becomes_ the peer sucker's inbox tree for that token. Users can claim their tokens on the peer chain by providing a merkle proof which shows that their claim is in the inbox tree.
24
+ ## Key Contracts
29
25
 
30
- ## Architecture
26
+ | Contract | Role |
27
+ | --- | --- |
28
+ | `JBSucker` | Base bridge logic for prepare, relay, claim, token mapping, and lifecycle controls. |
29
+ | `JBSuckerRegistry` | Registry for per-project sucker deployments, deployer allowlists, and shared bridge fee settings. |
30
+ | `JBOptimismSucker` | OP Stack bridge implementation. |
31
+ | `JBBaseSucker` | Base-flavored OP Stack implementation. |
32
+ | `JBCeloSucker` | OP Stack implementation adapted for Celo's native asset behavior. |
33
+ | `JBArbitrumSucker` | Arbitrum bridge implementation. |
34
+ | `JBCCIPSucker` | Chainlink CCIP-based implementation for CCIP-connected chains. |
31
35
 
32
- On each network:
36
+ ## Mental Model
33
37
 
34
- ```mermaid
35
- graph TD;
36
- A[JBSuckerRegistry] -->|exposes| B["deploySuckersFor(...)"]
37
- B -->|calls| C[IJBSuckerDeployer]
38
- C -->|deploys| D[JBSucker]
39
- A -->|tracks| D
40
- ```
38
+ Each sucker pair has two jobs:
41
39
 
42
- For an example project deployed on mainnet and Optimism with a `JBOptimismSucker` on each network:
43
-
44
- ```mermaid
45
- graph TD;
46
- subgraph Mainnet
47
- A[Project] -->|cashed-out funds| B[JBOptimismSucker]
48
- B -->|burns/mints tokens| A
49
- end
50
- subgraph Optimism
51
- C[Project] -->|cashed-out funds| D[JBOptimismSucker]
52
- D -->|burns/mints tokens| C
53
- end
54
- B <-->|merkle roots/funds| D
55
- ```
40
+ 1. destroy or lock the local economic position into a claimable message
41
+ 2. recreate the remote position from a bridged Merkle root plus transported value
42
+
43
+ That means every bridge path has two trust surfaces:
44
+
45
+ - the shared sucker accounting and Merkle logic
46
+ - the bridge-specific transport implementation
56
47
 
57
- ### Contracts
48
+ The shortest useful reading order is:
58
49
 
59
50
  | Contract | Description |
60
51
  |----------|-------------|
@@ -64,7 +55,7 @@ graph TD;
64
55
  | [`JBBaseSucker`](src/JBBaseSucker.sol) | Thin wrapper around `JBOptimismSucker` with Base chain IDs (Ethereum 1 <-> Base 8453, Sepolia 11155111 <-> Base Sepolia 84532). |
65
56
  | [`JBCeloSucker`](src/JBCeloSucker.sol) | Extends `JBOptimismSucker` for Celo (OP Stack, custom gas token CELO). Wraps native ETH → WETH before bridging as ERC-20. Unwraps received WETH → native ETH via `_addToBalance` override. Removes `NATIVE_TOKEN → NATIVE_TOKEN` restriction. Sends messenger messages with `nativeValue = 0` (Celo's native token is CELO, not ETH). |
66
57
  | [`JBArbitrumSucker`](src/JBArbitrumSucker.sol) | Extends `JBSucker`. Bridges via Arbitrum Inbox + Gateway Router. Uses `unsafeCreateRetryableTicket` for L1->L2 (to avoid address aliasing of refund address) and `ArbSys.sendTxToL1` for L2->L1. Requires `msg.value` for L1->L2 transport payment. |
67
- | [`JBSuckerRegistry`](src/JBSuckerRegistry.sol) | Tracks all suckers per project. Manages deployer allowlist (owner-only). Entry point for `deploySuckersFor`. Can remove deprecated suckers via `removeDeprecatedSucker`. Owns the global `toRemoteFee` (ETH fee in wei, capped at `MAX_TO_REMOTE_FEE` = 0.001 ether), adjustable by the registry owner via `setToRemoteFee()`. All sucker clones read this fee from the registry. |
58
+ | [`JBSuckerRegistry`](src/JBSuckerRegistry.sol) | Tracks all suckers per project. Manages deployer allowlist (owner-only). Entry point for `deploySuckersFor`. Can remove deprecated suckers via `removeDeprecatedSucker`. Owns the global `toRemoteFee` (ETH fee in wei, capped at `MAX_TO_REMOTE_FEE` = 0.001 ether), adjustable by the registry owner via `setToRemoteFee()`. All sucker clones read this fee from the registry. Existing-project deployments are deploy-and-map operations, so the registry also needs to be arranged as an authorized `MAP_SUCKER_TOKEN` operator for those projects. |
68
59
  | [`JBSuckerDeployer`](src/JBSuckerDeployer.sol) | Abstract base deployer. Clones a singleton sucker via `LibClone.cloneDeterministic` and initializes it. Two-phase setup: `setChainSpecificConstants` then `configureSingleton`. |
69
60
  | [`JBCCIPSuckerDeployer`](src/deployers/JBCCIPSuckerDeployer.sol) | Deployer for `JBCCIPSucker`. Stores CCIP router, remote chain ID, and CCIP chain selector. |
70
61
  | [`JBOptimismSuckerDeployer`](src/deployers/JBOptimismSuckerDeployer.sol) | Deployer for `JBOptimismSucker`. Stores OP Messenger and OP Bridge addresses. |
@@ -76,397 +67,78 @@ graph TD;
76
67
  | [`ARBAddresses`](src/libraries/ARBAddresses.sol) | Arbitrum bridge contract addresses (Inbox, Gateway Router) for mainnet and Sepolia. |
77
68
  | [`ARBChains`](src/libraries/ARBChains.sol) | Arbitrum chain ID constants. |
78
69
 
79
- ## Bridging Flow
80
-
81
- ```
82
- Chain A Chain B
83
- | |
84
- | 1. prepare(tokenCount, ...) |
85
- | - transfers project tokens |
86
- | - cashes out for terminal tkn |
87
- | - inserts leaf into outbox |
88
- | |
89
- | 2. toRemote(token) |
90
- | - sends merkle root + funds -->|
91
- | |
92
- | 3. fromRemote(root)
93
- | - validates message version
94
- | - updates inbox tree (if nonce > current)
95
- | |
96
- | 4. claim(proof)
97
- | - verifies merkle proof
98
- | - mints project tokens
99
- | - adds funds to balance
100
- ```
101
-
102
- Each `toRemote` call increments the outbox nonce and sends the complete merkle root. On the receiving side, `fromRemote` only accepts roots with a nonce strictly greater than the current inbox nonce. If nonces arrive out of order (possible with CCIP), earlier nonces are silently skipped and their claims become unclaimable on that chain. The sender would need to use the emergency hatch on the source chain to recover funds from skipped roots.
103
-
104
- Messages include a `MESSAGE_VERSION` (currently `1`) to reject incompatible messages from peers running different protocol versions.
105
-
106
- ## Bridging Tokens
107
-
108
- Imagine that "OhioDAO" is deployed on Ethereum mainnet and Optimism:
109
-
110
- - It has the $OHIO ERC-20 project token and a `JBOptimismSucker` deployed on each network.
111
- - Its suckers map mainnet ETH to Optimism ETH, and vice versa.
112
-
113
- Each sucker has mappings from terminal tokens on the local chain to associated terminal tokens on the remote chain.
114
-
115
- _Here's how Jimmy can bridge his $OHIO tokens (and the corresponding ETH) from mainnet to Optimism._
116
-
117
- **1. Pay the project.** Jimmy pays OhioDAO 1 ETH on Ethereum mainnet:
118
-
119
- ```solidity
120
- JBMultiTerminal.pay{value: 1 ether}({
121
- projectId: 12,
122
- token: JBConstants.NATIVE_TOKEN,
123
- amount: 1 ether,
124
- beneficiary: jimmy,
125
- minReturnedTokens: 0,
126
- memo: "OhioDAO rules",
127
- metadata: ""
128
- });
129
- ```
130
-
131
- OhioDAO's ruleset has a `weight` of `1e18`, so Jimmy receives 1 $OHIO (`1e18` tokens).
132
-
133
- **2. Approve the sucker.** Before bridging, Jimmy approves the `JBOptimismSucker` to transfer his $OHIO:
134
-
135
- ```solidity
136
- JBERC20.approve({
137
- spender: address(optimismSucker),
138
- value: 1e18
139
- });
140
- ```
141
-
142
- **3. Prepare the bridge.** Jimmy calls `prepare(...)` on the mainnet sucker. Note that `beneficiary` is `bytes32` for cross-VM compatibility (e.g., Solana public keys):
143
-
144
- ```solidity
145
- JBOptimismSucker.prepare({
146
- projectTokenCount: 1e18,
147
- beneficiary: bytes32(uint256(uint160(jimmy))), // bytes32 for cross-VM compat
148
- minTokensReclaimed: 0,
149
- token: JBConstants.NATIVE_TOKEN
150
- });
151
- ```
152
-
153
- The sucker transfers Jimmy's $OHIO to itself, cashes them out using OhioDAO's primary ETH terminal, and inserts a leaf into the ETH outbox tree. The leaf is a `keccak256` hash of the beneficiary (bytes32), the project token count, and the terminal token amount reclaimed. Both amounts are capped at `uint128` for SVM compatibility.
154
-
155
- **4. Bridge to remote.** Jimmy (or anyone) calls `toRemote(...)`:
156
-
157
- ```solidity
158
- JBOptimismSucker.toRemote(JBConstants.NATIVE_TOKEN);
159
- ```
160
-
161
- This sends the outbox merkle root and the accumulated ETH to the peer sucker on Optimism. The outbox balance is cleared and the nonce incremented. After the bridge completes, the Optimism sucker's ETH inbox tree is updated with the new root containing Jimmy's claim.
162
-
163
- **5. Claim on the remote chain.** Jimmy claims his $OHIO on Optimism by calling `claim(...)` with a [`JBClaim`](src/structs/JBClaim.sol):
164
-
165
- ```solidity
166
- struct JBClaim {
167
- address token; // The terminal token to claim
168
- JBLeaf leaf; // The leaf data
169
- bytes32[32] proof; // Merkle proof (TREE_DEPTH = 32)
170
- }
171
- ```
172
-
173
- The [`JBLeaf`](src/structs/JBLeaf.sol):
174
-
175
- ```solidity
176
- struct JBLeaf {
177
- uint256 index; // Position in the merkle tree
178
- bytes32 beneficiary; // Recipient address (bytes32 for cross-VM compat)
179
- uint256 projectTokenCount; // Project tokens to mint
180
- uint256 terminalTokenAmount; // Terminal tokens reclaimed
181
- }
182
- ```
183
-
184
- Building these claims manually requires tracking every insertion and computing merkle proofs. The [`juicerkle`](https://github.com/Bananapus/juicerkle) service simplifies this -- `POST` a JSON request to `/claims`:
185
-
186
- ```json
187
- {
188
- "chainId": 10,
189
- "sucker": "0x5678...",
190
- "token": "0x000000000000000000000000000000000000EEEe",
191
- "beneficiary": "0x1234..."
192
- }
193
- ```
194
-
195
- | Field | Type | Description |
196
- |-------|------|-------------|
197
- | `chainId` | `int` | Network ID for the sucker being claimed from. |
198
- | `sucker` | `string` | Address of the sucker being claimed from. |
199
- | `token` | `string` | Terminal token whose inbox tree is being claimed from. |
200
- | `beneficiary` | `string` | Address to get available claims for. |
201
-
202
- The service looks through the entire inbox tree and returns all available claims as `JBClaim` structs ready to pass to `claim(...)`.
203
-
204
- The bridged ETH is added to OhioDAO's Optimism balance when the claim is processed.
205
-
206
- ## Token Mapping
207
-
208
- Token mappings define which local terminal token corresponds to which remote terminal token. Key rules:
209
-
210
- - **`remoteToken` is `bytes32`**, not `address` -- this supports cross-VM compatibility (e.g., Solana program addresses). For EVM addresses, left-pad with zeros: `bytes32(uint256(uint160(address)))`.
211
- - **Immutable once used.** After an outbox tree has entries for a token, the mapping cannot be changed to a different remote token. It can only be disabled (by setting `remoteToken` to `bytes32(0)`), which triggers a final root flush to settle outstanding claims. A disabled mapping can be re-enabled back to the same remote token.
212
- - **Minimum gas enforcement.** ERC-20 mappings must specify `minGas >= MESSENGER_ERC20_MIN_GAS_LIMIT` (200,000). Native token mappings on the base `JBSucker` do not require minimum gas, but `JBCCIPSucker` requires it for all tokens (because CCIP wraps native to WETH, an ERC-20 transfer).
213
- - **Native token rules.** On `JBSucker` (OP/Arb), `NATIVE_TOKEN` can only map to `NATIVE_TOKEN` or `bytes32(0)`. `JBCCIPSucker` and `JBCeloSucker` override this to allow `NATIVE_TOKEN` mapping to any remote address (for chains where ETH is an ERC-20).
214
- - **`toRemoteFee`** is a global fee (ETH, in wei) stored on the `JBSuckerRegistry` and adjustable by the registry owner via `setToRemoteFee()`, up to a hard cap of `MAX_TO_REMOTE_FEE` (0.001 ether). Each sucker clone reads the fee from its immutable `REGISTRY` reference via `REGISTRY.toRemoteFee()`. It is paid into the fee project (determined by `FEE_PROJECT_ID`, typically project ID 1) via `terminal.pay()` on each `toRemote()` call, making spam economically costly. The caller receives fee project tokens in return (incentivizing relayers). The fee is best-effort: if the fee project has no native token terminal, or if `terminal.pay()` reverts, `toRemote()` proceeds without collecting the fee. A "nothing to send" guard also prevents free repeated calls when nothing has changed.
215
-
216
- ```solidity
217
- struct JBTokenMapping {
218
- address localToken; // Local terminal token address
219
- uint32 minGas; // Minimum gas for bridging
220
- bytes32 remoteToken; // Remote token (bytes32 for cross-VM compat)
221
- }
222
- ```
223
-
224
- ## Deprecation Lifecycle
225
-
226
- Suckers have a four-state deprecation lifecycle controlled by `setDeprecation(timestamp)`:
227
-
228
- ```
229
- ENABLED --> DEPRECATION_PENDING --> SENDING_DISABLED --> DEPRECATED
230
- ```
231
-
232
- | State | Condition | Behavior |
233
- |-------|-----------|----------|
234
- | `ENABLED` | `deprecatedAfter == 0` | Fully functional. All operations allowed. |
235
- | `DEPRECATION_PENDING` | `now < deprecatedAfter - _maxMessagingDelay()` | Warning state. All operations still allowed. Deprecation can be cancelled by setting timestamp to `0`. |
236
- | `SENDING_DISABLED` | `now < deprecatedAfter` | No new `prepare` or `toRemote` calls. Incoming `fromRemote` still accepted. Emergency exit available for all tokens. |
237
- | `DEPRECATED` | `now >= deprecatedAfter` | Fully shut down. No new `fromRemote` accepted. Emergency exit available for all tokens. |
70
+ ## Read These Files First
238
71
 
239
- The deprecation timestamp must be at least `_maxMessagingDelay()` (14 days) in the future. This ensures in-flight messages have time to arrive before the sucker stops accepting them. Once in `SENDING_DISABLED` or `DEPRECATED` state, the deprecation can no longer be modified.
72
+ 1. `src/JBSucker.sol`
73
+ 2. `src/JBSuckerRegistry.sol`
74
+ 3. the chain-specific implementation under `src/`
75
+ 4. the matching deployer under `src/deployers/`
76
+ 5. `src/utils/MerkleLib.sol`
240
77
 
241
- Permission: `SET_SUCKER_DEPRECATION` from the project owner.
78
+ ## Integration Traps
242
79
 
243
- ## Emergency Hatch
80
+ - do not reason about suckers as if they were generic ERC-20 bridges; they are project-token plus treasury-state bridges
81
+ - root ordering and message delivery semantics matter as much as the claim proof format
82
+ - token mapping is part of the economic invariant, not just a convenience config
83
+ - emergency and deprecation paths are not edge tooling; they are part of normal operational safety
244
84
 
245
- The emergency hatch lets users exit on the chain where they deposited when the bridge is broken or a token is no longer compatible.
85
+ ## Where State Lives
246
86
 
247
- - **Per-token activation.** Call `enableEmergencyHatchFor(tokens)` with `SUCKER_SAFETY` permission. This is irreversible -- once opened for a token, that token can never be bridged by this sucker again.
248
- - **Automatic activation.** When the sucker reaches `SENDING_DISABLED` or `DEPRECATED` state, all tokens automatically allow emergency exit.
249
- - **Who can exit.** Only users whose leaves have NOT already been sent to the remote chain (i.e., leaf index >= `numberOfClaimsSent`) can use the emergency hatch. This prevents double-spending where the same leaf is claimed on both chains.
250
- - **How it works.** Users call `exitThroughEmergencyHatch(claimData)` with a proof against the **outbox** tree (not the inbox tree). The sucker mints project tokens and adds terminal tokens to the project balance, just like a normal claim.
87
+ - per-claim and tree progression state live in the sucker pair itself
88
+ - deployment inventory and shared operational config live in `JBSuckerRegistry`
89
+ - bridge transport assumptions live in the chain-specific implementation and its external counterparties
251
90
 
252
- ## Launching Suckers
91
+ When reviewing a bridge incident, check local state transition correctness before blaming the transport layer.
253
92
 
254
- Requirements for deploying a sucker pair:
255
-
256
- 1. **Projects on both chains.** Project IDs don't have to match.
257
- 2. **0% cash out tax rate.** Both projects must have a `cashOutTaxRate` of `0` so suckers can fully cash out project tokens for terminal tokens.
258
- 3. **Owner minting enabled.** Both projects must have `allowOwnerMinting` set to `true` so suckers can mint bridged project tokens.
259
- 4. **ERC-20 project token.** Both projects must have a deployed ERC-20 token (via `JBController.deployERC20For(...)`). The sucker uses `safeTransferFrom` to pull project tokens from the caller.
260
-
261
- Suckers are deployed through the [`JBSuckerRegistry`](src/JBSuckerRegistry.sol) on each chain. The registry maps local tokens to remote tokens during deployment, so it needs permission:
262
-
263
- ```solidity
264
- // Give the registry MAP_SUCKER_TOKEN permission for project 12
265
- uint256[] memory permissionIds = new uint256[](1);
266
- permissionIds[0] = JBPermissionIds.MAP_SUCKER_TOKEN;
267
-
268
- permissions.setPermissionsFor(
269
- projectOwner,
270
- JBPermissionsData({
271
- operator: address(registry),
272
- projectId: 12,
273
- permissionIds: permissionIds
274
- })
275
- );
276
- ```
277
-
278
- Now deploy the suckers with a token mapping:
279
-
280
- ```solidity
281
- // Map mainnet ETH to Optimism ETH
282
- JBTokenMapping[] memory mappings = new JBTokenMapping[](1);
283
- mappings[0] = JBTokenMapping({
284
- localToken: JBConstants.NATIVE_TOKEN,
285
- minGas: 200_000,
286
- remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))) // bytes32
287
- });
288
-
289
- JBSuckerDeployerConfig[] memory configs = new JBSuckerDeployerConfig[](1);
290
- configs[0] = JBSuckerDeployerConfig({
291
- deployer: IJBSuckerDeployer(optimismSuckerDeployer),
292
- mappings: mappings
293
- });
294
-
295
- // Must use the same salt and caller on both chains
296
- bytes32 salt = keccak256("my-project-suckers-v1");
297
- address[] memory suckers = registry.deploySuckersFor(12, salt, configs);
298
- ```
299
-
300
- - The [`JBTokenMapping`](src/structs/JBTokenMapping.sol) maps local mainnet ETH to remote Optimism ETH.
301
- - `remoteToken` is `bytes32`, not `address`. For EVM addresses, use `bytes32(uint256(uint160(addr)))`.
302
- - `minGas` requires a gas limit of at least 200,000 for ERC-20s. If your token has expensive transfer logic, you may need more.
303
- - The fee for `toRemote()` is set globally on the `JBSuckerRegistry` by the registry owner via `setToRemoteFee()`, up to `MAX_TO_REMOTE_FEE` (0.001 ether). Each sucker clone reads this fee from the registry. It is not part of the token mapping.
304
- - The [`JBSuckerDeployerConfig`](src/structs/JBSuckerDeployerConfig.sol) specifies which deployer to use. You can only use approved deployers through the registry -- check for `SuckerDeployerAllowed` events or contact the registry's owner.
305
- - **For the suckers to be peers, the `salt` has to match on both chains and the same address must call `deploySuckersFor(...)`.**
306
-
307
- Finally, give the sucker permission to mint bridged project tokens:
308
-
309
- ```solidity
310
- uint256[] memory mintPermissionIds = new uint256[](1);
311
- mintPermissionIds[0] = JBPermissionIds.MINT_TOKENS;
312
-
313
- permissions.setPermissionsFor(
314
- projectOwner,
315
- JBPermissionsData({
316
- operator: suckers[0],
317
- projectId: 12,
318
- permissionIds: mintPermissionIds
319
- })
320
- );
321
- ```
322
-
323
- Repeat this process on the other chain to deploy the peer sucker, and the project is ready for bridging.
324
-
325
- ## Managing Suckers
326
-
327
- Once configured, suckers manage themselves. Stay up-to-date on changes to the bridge infrastructure used by your sucker of choice. If a change causes suckers to become incompatible with the underlying bridge, there are two options.
328
-
329
- **Always perform these actions on BOTH sides of the sucker pair.**
330
-
331
- ### Disable a token
332
-
333
- If a bridge change affects only certain tokens, call `mapToken(...)` with `remoteToken` set to `bytes32(0)` to disable that token. This triggers a final `toRemote` to flush remaining outbox funds to the peer. If the bridge won't allow a final transfer with the remaining funds, activate the emergency hatch for the affected tokens instead.
334
-
335
- The emergency hatch lets depositors withdraw their funds on the chain where they deposited. Only those whose funds have not been sent to the remote chain can withdraw. Once opened for a token, that token can never be bridged by this sucker again -- deploy a new sucker instead.
336
-
337
- ### Deprecate the suckers
338
-
339
- If the bridging infrastructure will no longer work, deprecate the sucker to begin shutdown. Call `setDeprecation(timestamp)` with a timestamp at least 14 days (`_maxMessagingDelay()`) in the future. The sucker transitions through `DEPRECATION_PENDING` -> `SENDING_DISABLED` -> `DEPRECATED`. After full deprecation, all tokens allow exit through the emergency hatch and no new messages are accepted. This protects against future fake or malicious bridge messages.
340
-
341
- When deprecating, ensure no pending bridge messages need retrying -- once deprecation completes, those messages will be rejected.
342
-
343
- ## Using the Relayer
344
-
345
- Bridging from L1 to L2 is straightforward. Bridging from L2 to L1 requires extra steps to finalize the withdrawal. For OP Stack networks like Optimism or Base, this follows the [withdrawal flow](https://docs.optimism.io/stack/protocol/withdrawal-flow):
346
-
347
- 1. The **withdrawal initiating transaction**, which the user submits on L2.
348
- 2. The **withdrawal proving transaction**, which the user submits on L1 to prove that the withdrawal is legitimate (based on a merkle patricia trie root).
349
- 3. The **withdrawal finalizing transaction**, which the user submits on L1 after the fault challenge period has passed.
350
-
351
- Users can do this manually, but it's a hassle. The [`bananapus-sucker-relayer`](https://github.com/Bananapus/bananapus-sucker-relayer) automates proving and finalizing withdrawals using [OpenZeppelin Defender](https://www.openzeppelin.com/defender). Project creators set up a Defender account, configure a relayer through their dashboard, and fund it with ETH for gas.
352
-
353
- ## Resources
354
-
355
- - [`MerkleLib`](src/utils/MerkleLib.sol) -- Incremental merkle tree based on [Nomad's implementation](https://github.com/nomad-xyz/nomad-monorepo/blob/main/solidity/nomad-core/libs/Merkle.sol) and the eth2 deposit contract.
356
- - [`juicerkle`](https://github.com/Bananapus/juicerkle) -- Service that returns available claims for a beneficiary (generates merkle proofs). Includes a [Go merkle tree implementation](https://github.com/Bananapus/juicerkle/blob/master/tree/tree.go) for computing roots and building/verifying proofs.
357
- - [`juicerkle-tester`](https://github.com/Bananapus/juicerkle-tester) -- End-to-end bridging test: deploys projects, tokens, and suckers, then bridges between them. Useful as a bridging walkthrough.
358
-
359
- ## Risks
360
-
361
- - **Out-of-order nonce delivery (CCIP).** `fromRemote` only accepts roots with a nonce strictly greater than the current inbox nonce. If CCIP delivers messages out of order, earlier nonces are silently skipped and every claim in those skipped roots becomes permanently unclaimable on the destination chain. Affected users must use the emergency hatch on the source chain to recover their funds.
362
- - **Emergency hatch is irreversible per-token.** Calling `enableEmergencyHatchFor` permanently disables bridging for that token on that sucker. Once opened, the token can never be bridged by this sucker again -- a new sucker must be deployed to restore bridging for that token.
363
- - **`msg.value` required for Arbitrum L1-to-L2 transport.** `JBArbitrumSucker` uses `unsafeCreateRetryableTicket` for L1-to-L2 messages, which requires `msg.value` to cover the retryable ticket cost. OP Stack suckers (`JBOptimismSucker`, `JBBaseSucker`, `JBCeloSucker`) do not require `msg.value` for transport. Callers must know which sucker type they are interacting with, or the `toRemote` call will revert.
364
- - **Merkle tree depth is fixed at 32.** The outbox tree supports approximately 4 billion leaves, which is practically unlimited, but the tree is append-only and never pruned. Every leaf persists for the lifetime of the sucker, and proofs always require exactly 32 siblings regardless of tree size.
365
- - **Token mapping immutability.** Once an outbox tree has any entries for a given token, the mapping between local and remote token is locked forever. The mapping can be disabled (set `remoteToken` to `bytes32(0)`) and later re-enabled, but only to the same remote token. Mapping to a different remote token requires deploying a new sucker.
366
-
367
- ## Repository Layout
368
-
369
- ```
370
- nana-suckers-v6/
371
- ├── script/
372
- │ ├── Deploy.s.sol - Deployment script.
373
- │ └── helpers/
374
- │ └── SuckerDeploymentLib.sol - Internal helpers for deployment.
375
- ├── src/
376
- │ ├── JBSucker.sol - Abstract base sucker implementation.
377
- │ ├── JBCCIPSucker.sol - Chainlink CCIP bridge implementation.
378
- │ ├── JBOptimismSucker.sol - OP Stack bridge implementation.
379
- │ ├── JBBaseSucker.sol - Base-specific wrapper around JBOptimismSucker.
380
- │ ├── JBCeloSucker.sol - Celo-specific wrapper around JBOptimismSucker (custom gas token).
381
- │ ├── JBArbitrumSucker.sol - Arbitrum bridge implementation.
382
- │ ├── JBSuckerRegistry.sol - Registry tracking suckers per project.
383
- │ ├── deployers/ - Deployers for each kind of sucker.
384
- │ ├── enums/ - JBLayer, JBSuckerState.
385
- │ ├── interfaces/ - Contract interfaces.
386
- │ ├── libraries/ - ARBAddresses, ARBChains, CCIPHelper.
387
- │ ├── structs/ - JBClaim, JBLeaf, JBMessageRoot, JBOutboxTree, etc.
388
- │ └── utils/
389
- │ └── MerkleLib.sol - Incremental merkle tree (depth 32).
390
- └── test/
391
- ├── Fork.t.sol - Fork tests.
392
- ├── ForkArbitrum.t.sol - Arbitrum fork tests.
393
- ├── ForkCelo.t.sol - Celo fork tests.
394
- ├── ForkClaim.t.sol - Fork claim tests.
395
- ├── ForkMainnet.t.sol - Mainnet fork tests.
396
- ├── ForkOPStack.t.sol - OP Stack fork tests.
397
- ├── InteropCompat.t.sol - Cross-VM compatibility tests.
398
- ├── SuckerAttacks.t.sol - Security-focused attack tests.
399
- ├── SuckerDeepAttacks.t.sol - Deep attack scenario tests.
400
- ├── SuckerRegressions.t.sol - Regression tests.
401
- ├── TestAuditGaps.sol - Audit gap coverage tests.
402
- ├── mocks/ - Mock contracts for testing.
403
- ├── regression/ - Regression-specific tests.
404
- └── unit/ - Unit tests (merkle, registry, deployer, emergency, arb, ccip, invariants).
405
- ```
406
-
407
- ## Usage
408
-
409
- ### Install
410
-
411
- For projects using `npm` to manage dependencies (recommended):
93
+ ## Install
412
94
 
413
95
  ```bash
414
96
  npm install @bananapus/suckers-v6
415
97
  ```
416
98
 
417
- For projects using `forge` to manage dependencies:
418
-
419
- ```bash
420
- forge install Bananapus/nana-suckers-v6
421
- ```
422
-
423
- If you're using `forge`, add `@bananapus/suckers-v6/=lib/nana-suckers-v6/` to `remappings.txt`. You'll also need to install `nana-suckers-v6`'s dependencies and add similar remappings for them.
424
-
425
- ### Develop
426
-
427
- `nana-suckers-v6` uses [npm](https://www.npmjs.com/) (version >=20.0.0) for package management and the [Foundry](https://github.com/foundry-rs/foundry) development toolchain for builds, tests, and deployments. To get set up, [install Node.js](https://nodejs.org/en/download) and install [Foundry](https://github.com/foundry-rs/foundry):
99
+ ## Development
428
100
 
429
101
  ```bash
430
- curl -L https://foundry.paradigm.xyz | sh
102
+ npm install
103
+ forge build
104
+ forge test
431
105
  ```
432
106
 
433
- Download and install dependencies with:
434
-
435
- ```bash
436
- npm ci && forge install
437
- ```
438
-
439
- If you run into trouble with `forge install`, try using `git submodule update --init --recursive` to ensure that nested submodules have been properly initialized.
440
-
441
- | Command | Description |
442
- |---------|-------------|
443
- | `forge build` | Compile the contracts and write artifacts to `out`. |
444
- | `forge test` | Run the tests. |
445
- | `forge test -vvvv` | Run tests with full traces. |
446
- | `forge fmt` | Lint. |
447
- | `forge coverage` | Generate a test coverage report. |
448
- | `forge build --sizes` | Get contract sizes. |
449
- | `forge clean` | Remove build artifacts and cache. |
450
- | `foundryup` | Update Foundry. Run this periodically. |
107
+ Useful scripts:
451
108
 
452
- To learn more, visit the [Foundry Book](https://book.getfoundry.sh/) docs.
109
+ - `npm run deploy:mainnets`
110
+ - `npm run deploy:testnets`
111
+ - `npm run analyze`
453
112
 
454
- ### Scripts
113
+ ## Deployment Notes
455
114
 
456
- | Command | Description |
457
- |---------|-------------|
458
- | `npm test` | Run local tests. |
459
- | `npm run coverage` | Generate an LCOV test coverage report. |
460
- | `npm run artifacts` | Fetch Sphinx artifacts and write them to `deployments/`. |
115
+ This package supports multiple bridge families and is intentionally split into bridge-specific deployers. It is commonly used directly and through the Omnichain and Revnet deployer packages.
461
116
 
462
- ### Tips
463
-
464
- To view test coverage, run `npm run coverage` to generate an LCOV test report. You can use an extension like [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) to view coverage in your editor.
465
-
466
- If you're using Nomic Foundation's [Solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension in VSCode, you may run into LSP errors because the extension cannot find dependencies outside of `lib`. You can often fix this by running:
467
-
468
- ```bash
469
- forge remappings >> remappings.txt
470
- ```
117
+ ## Repository Layout
471
118
 
472
- This makes the extension aware of default remappings.
119
+ ```text
120
+ src/
121
+ bridge implementations
122
+ JBSucker.sol
123
+ JBSuckerRegistry.sol
124
+ deployers/
125
+ enums/
126
+ interfaces/
127
+ libraries/
128
+ structs/
129
+ utils/
130
+ test/
131
+ unit, fork, interoperability, attack, audit, and regression coverage
132
+ script/
133
+ Deploy.s.sol
134
+ helpers/
135
+ ```
136
+
137
+ ## Risks And Notes
138
+
139
+ - out-of-order root delivery can make some claims unclaimable until an operator uses an emergency path
140
+ - bridge-specific transport assumptions matter as much as the shared sucker logic
141
+ - token mapping and deprecation controls are governance-sensitive surfaces
142
+ - a bridge that stays live operationally still may not be economically safe for every asset or chain pair
143
+
144
+ When debugging a bad cross-chain outcome, first decide whether the failure is in claim construction, message transport, inbox/outbox root progression, or remote settlement. Those are different bug classes.
package/RISKS.md CHANGED
@@ -1,8 +1,20 @@
1
- # RISKS.md -- nana-suckers-v6
1
+ # Suckers Risk Register
2
2
 
3
- Forward-looking risk catalog for the JBSucker cross-chain bridging system.
3
+ This file focuses on the bridge-like risks in the sucker system: merkle-root progression, token mapping, cross-chain consistency, and the explicit non-atomicity of source burn and destination mint.
4
4
 
5
- ---
5
+ ## How to use this file
6
+
7
+ - Read `Priority risks` first; they summarize the bridge failure modes with real user-fund implications.
8
+ - Use the detailed sections for merkle, fee, emergency, and deprecation reasoning.
9
+ - Treat `Accepted Behaviors` as explicit tradeoffs in the bridge model, not oversights.
10
+
11
+ ## Priority risks
12
+
13
+ | Priority | Risk | Why it matters | Primary controls |
14
+ |----------|------|----------------|------------------|
15
+ | P0 | Out-of-order or asymmetric cross-chain state | If roots or peer suckers do not progress symmetrically, claims can become unavailable or one-way only. | Nonce checks, peer verification, and emergency hatch recovery. |
16
+ | P0 | Bad token mapping or registry trust | Incorrect local or remote token mapping can mint or route the wrong asset across chains. | Strict mapping controls, deploy-time review, and registry or operator scrutiny. |
17
+ | P1 | Non-atomic bridge semantics | Users can experience delays, skipped roots, or recovery flows because burn and mint are not one atomic operation. | Explicit user and operator docs, emergency procedures, and monitoring of bridge liveness. |
6
18
 
7
19
  ## 1. Trust Assumptions
8
20
 
@@ -46,7 +58,7 @@ Forward-looking risk catalog for the JBSucker cross-chain bridging system.
46
58
 
47
59
  ## 5. Fee Collection Risks
48
60
 
49
- - **Best-effort fee collection.** `toRemoteFee` is a centralized storage variable on `JBSuckerRegistry` (ETH, in wei) — uniform across all suckers and all tokens, non-bypassable by integrators. It is paid into `FEE_PROJECT_ID` (typically project ID 1) via `terminal.pay()`. If the fee project has no primary terminal for `NATIVE_TOKEN`, or if `terminal.pay()` reverts for any reason, `toRemote()` silently proceeds without collecting the fee. This means fee collection is best-effort it can fail if the fee project's terminal is misconfigured, paused, or removed but the fee amount cannot be set to 0 by users calling `toRemote()`.
61
+ - **Best-effort fee collection.** `toRemoteFee` is a centralized storage variable on `JBSuckerRegistry` (ETH, in wei) — uniform across all suckers and all tokens, non-bypassable by integrators. It is paid into `FEE_PROJECT_ID` (typically project ID 1) via `terminal.pay()`. If the fee project has no primary terminal for `NATIVE_TOKEN`, or if `terminal.pay()` reverts for any reason, `toRemote()` still proceeds, but the fee ETH is retained by the sucker contract and later becomes sweepable through the normal claim path. Fee collection is therefore best-effort at the protocol-fee destination even though users still supply the fee amount.
50
62
  - **Renounced registry ownership risk.** If the registry owner calls `renounceOwnership()`, `setToRemoteFee()` becomes permanently uncallable and the fee is frozen at its current value across all suckers. This is a deliberate trade-off: it allows the registry owner to credibly commit to a fee level, but eliminates the ability to respond to future ETH price changes. The fee is still capped at `MAX_TO_REMOTE_FEE`, so the maximum downside is bounded.
51
63
  - **Immutable fee project.** `FEE_PROJECT_ID` is set at construction and cannot be changed. If the fee project is abandoned or its terminal removed, there is no way to redirect fees without deploying new suckers.
52
64
  - **Cross-reference: sucker registration path.** Suckers are deployed via `JBSuckerRegistry.deploySuckersFor`, which requires `DEPLOY_SUCKERS` permission from the project owner. The registry's `deploy` function uses `CREATE2` with a deployer-specific salt. The sucker's `peer()` address is deterministic — a misconfigured peer means the sucker accepts messages from the wrong remote address. See [nana-omnichain-deployers-v6 RISKS.md](../nana-omnichain-deployers-v6/RISKS.md) for deployer-level risks.
@@ -79,6 +91,7 @@ Forward-looking risk catalog for the JBSucker cross-chain bridging system.
79
91
  - **uint128 cap for SVM compatibility.** `_insertIntoTree` reverts if `projectTokenCount` or `terminalTokenAmount` exceeds `type(uint128).max`. This is enforced for cross-VM compatibility but limits EVM-only use cases to ~3.4e38 wei per leaf.
80
92
  - **Arbitrum retryable ticket pricing.** `_toL2` uses `block.basefee` as `maxFeePerGas`. If L2 gas prices spike above L1's `block.basefee`, the retryable ticket may not auto-redeem and requires manual retry.
81
93
  - **CCIP fee volatility.** `_sendRootOverAMB` checks `CCIP_ROUTER.getFee()` at call time. If fees spike between estimation and execution, the transaction reverts with `JBSucker_InsufficientMsgValue`. No retry mechanism exists.
94
+ - **`toRemote` fee fallback can strand ETH.** If the fee project's terminal is missing or `terminal.pay()` reverts, `toRemote()` keeps the fee ETH in the sucker so zero-cost bridges can still proceed with `transportPayment = msg.value - fee`. That retained ETH does increase `amountToAddToBalanceOf(NATIVE_TOKEN)`, but later `claim()` calls only forward each leaf's `terminalTokenAmount`, so the retained fee is not swept into the project's balance. There is no dedicated recovery function. This is an accepted tradeoff, and the stuck amount is bounded by `MAX_TO_REMOTE_FEE` (currently `0.001 ether`) per affected call.
82
95
  - **CCIP transport payment refund failure.** If `_msgSender()` is a non-payable contract, the refund `call` fails silently. The excess ETH (transportPayment - fees) is permanently stuck in the sucker. The contract emits `TransportPaymentRefundFailed` but has no sweep mechanism.
83
96
  - **Unbounded sucker count per project.** `JBSuckerRegistry._suckersOf` uses an EnumerableMap with no cap. `suckerPairsOf` iterates all suckers with external calls per iteration. Extremely large sucker counts could cause view functions to exceed gas limits.
84
97
  - **Unrestricted `receive()`.** Anyone can send ETH to the sucker, inflating `amountToAddToBalanceOf`. This is by design (needed for bridge/terminal returns) but means the project can receive unexpected balance additions.
@@ -112,4 +125,8 @@ The registry owner can adjust `toRemoteFee` via `JBSuckerRegistry.setToRemoteFee
112
125
 
113
126
  ### 10.4 Fee is paid to the protocol project, not the sucker's project
114
127
 
115
- The fee is paid to `FEE_PROJECT_ID` (the protocol project), not to the sucker's own `projectId()`. The protocol project always has a native token terminal, ensuring reliable fee collection. The sucker's project does not directly benefit from the anti-spam fee.
128
+ The fee is paid to `FEE_PROJECT_ID` (the protocol project), not to the sucker's own `projectId()`. This centralizes fee collection, but it is still only best-effort: if the fee project's native terminal is missing or its `pay` call reverts, the fee ETH stays in the sucker contract and is later recoverable through the normal claim path. The sucker's project does not directly benefit from the anti-spam fee.
129
+
130
+ ### 10.5 `mapTokens` does not refund ETH on enable-only batches
131
+
132
+ `mapTokens()` only uses `msg.value` when one or more mappings are being disabled and need transport payment for the final root flush. If every mapping in the batch is enable-only, `numberToDisable == 0`, each `_mapToken` call receives `transportPaymentValue == 0`, and any ETH sent with the transaction is not used or refunded. Operators should avoid sending ETH on enable-only calls.