@bananapus/suckers-v6 0.0.25 → 0.0.27

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/ADMINISTRATION.md CHANGED
@@ -1,211 +1,26 @@
1
1
  # Administration
2
2
 
3
- Admin privileges and their scope in nana-suckers-v6.
4
-
5
3
  ## At A Glance
6
4
 
7
5
  | Item | Details |
8
- |------|---------|
9
- | Scope | Cross-chain bridge deployment, token mapping, deprecation, emergency hatch controls, and layer-specific bridge configurator setup. |
10
- | Operators | Registry owner, project owners and delegates, sucker deployers, token mappers, safety admins, deprecation admins, and one-time bridge configurators. |
11
- | Highest-risk actions | Mapping the wrong remote token, enabling an emergency hatch, setting deprecation, or misconfiguring bridge singletons and chain constants. |
12
- | Recovery posture | The normal recovery path is to deploy or registry-enable a new sucker/deployer and migrate traffic; several safety actions are intentionally irreversible. |
13
-
14
- ## Routine Operations
15
-
16
- - Allow only the sucker deployer implementations you are prepared to support operationally on the registry side.
17
- - Map remote tokens carefully and early, before outbox activity makes mistakes harder to unwind.
18
- - Use deprecation timestamps to create a safe runway for shutdown rather than trying to disable a bridge abruptly.
19
- - Treat emergency hatch activation as a last-resort safety action when the normal bridge path is no longer trustworthy.
20
- - Verify one-time deployer configuration (`configureSingleton`, `setChainSpecificConstants`) before using a deployer in production.
21
-
22
- ## One-Way Or High-Risk Actions
23
-
24
- - `enableEmergencyHatchFor` is irreversible for the affected token mapping.
25
- - Once a sucker reaches `SENDING_DISABLED` or `DEPRECATED`, the lifecycle cannot be reversed.
26
- - Deployer singleton and chain-specific constant configuration are one-time setup operations.
27
- - A bad token mapping can strand users or route liquidity to the wrong remote asset.
28
-
29
- ## Recovery Notes
30
-
31
- - If a bridge path is unhealthy, prefer deprecating the affected sucker and standing up a replacement rather than forcing traffic through a degraded route.
32
- - If a token is emergency-hatched, users recover through the documented exit flow; you cannot simply re-enable the old path afterward.
33
-
34
- ## Roles
35
-
36
- ### Registry Owner
37
-
38
- - **How assigned:** Set at `JBSuckerRegistry` construction via the `initialOwner` parameter. Transferable via OpenZeppelin `Ownable`.
39
- - **Scope:** Controls which sucker deployer contracts are approved for use by the registry, and sets the global `toRemoteFee` applied to all suckers. Initially expected to be JuiceboxDAO (project ID 1).
40
- - **Permission ID:** None (uses `onlyOwner` modifier from OpenZeppelin `Ownable`).
41
-
42
- ### Project Owner
43
-
44
- - **How assigned:** Owner of the ERC-721 project NFT in `JBProjects`. All project-scoped permissions are checked against `PROJECTS.ownerOf(projectId)` or `DIRECTORY.PROJECTS().ownerOf(projectId)`.
45
- - **Scope:** Controls sucker deployment, token mapping, deprecation, and emergency hatch for their project. Can delegate any of these permissions to other addresses via `JBPermissions`.
46
- - **Permission ID:** N/A (is the root authority that grants the permission IDs below).
47
-
48
- ### Sucker Deployer (delegated role)
49
-
50
- - **How assigned:** Granted `DEPLOY_SUCKERS` permission by the project owner via `JBPermissions`.
51
- - **Scope:** Can deploy new suckers for a specific project through the registry.
52
- - **Permission ID:** `JBPermissionIds.DEPLOY_SUCKERS`
53
-
54
- ### Token Mapper (delegated role)
55
-
56
- - **How assigned:** Granted `MAP_SUCKER_TOKEN` permission by the project owner via `JBPermissions`. Commonly granted to the `JBSuckerRegistry` address so it can call `mapTokens` during deployment.
57
- - **Scope:** Can map or disable local-to-remote token pairs on a specific sucker.
58
- - **Permission ID:** `JBPermissionIds.MAP_SUCKER_TOKEN`
59
-
60
- For existing-project deployments, this permission is part of the same operational path as `DEPLOY_SUCKERS`: the registry deploys the sucker and then immediately calls `mapTokens` on it. If the project delegates deployment without arranging mapping authority for the registry, the single deployment transaction will revert during configuration.
61
-
62
- ### Safety Admin (delegated role)
63
-
64
- - **How assigned:** Granted `SUCKER_SAFETY` permission by the project owner via `JBPermissions`.
65
- - **Scope:** Can open the emergency hatch for specific tokens on a sucker. This is an irreversible action.
66
- - **Permission ID:** `JBPermissionIds.SUCKER_SAFETY`
67
-
68
- ### Deprecation Admin (delegated role)
69
-
70
- - **How assigned:** Granted `SET_SUCKER_DEPRECATION` permission by the project owner via `JBPermissions`.
71
- - **Scope:** Can set or cancel the deprecation timestamp on a sucker.
72
- - **Permission ID:** `JBPermissionIds.SET_SUCKER_DEPRECATION`
73
-
74
- ### Layer-Specific Configurator
75
-
76
- - **How assigned:** Set at deployer construction via the `configurator` parameter. Stored as the immutable `LAYER_SPECIFIC_CONFIGURATOR` address on each `JBSuckerDeployer`.
77
- - **Scope:** Can call `setChainSpecificConstants` and `configureSingleton` exactly once each on the deployer. These are one-time setup functions that configure bridge-specific addresses (messenger, bridge, router, inbox) and the singleton implementation used for cloning.
78
- - **Permission ID:** None (uses direct `_msgSender()` check against `LAYER_SPECIFIC_CONFIGURATOR`).
79
-
80
- ## Privileged Functions
81
-
82
- ### JBSuckerRegistry
83
-
84
- | Function | Required Role | Permission ID | Scope | What It Does |
85
- |----------|--------------|---------------|-------|--------------|
86
- | `allowSuckerDeployer(deployer)` | Registry Owner | N/A (`onlyOwner`) | Global | Adds a deployer contract to the allowlist, enabling it to be used when deploying suckers. |
87
- | `allowSuckerDeployers(deployers)` | Registry Owner | N/A (`onlyOwner`) | Global | Batch version: adds multiple deployer contracts to the allowlist. |
88
- | `removeSuckerDeployer(deployer)` | Registry Owner | N/A (`onlyOwner`) | Global | Removes a deployer contract from the allowlist, preventing future sucker deployments through it. |
89
- | `setToRemoteFee(fee)` | Registry Owner | N/A (`onlyOwner`) | Global | Sets the `toRemoteFee` applied to all suckers. The fee must be <= `MAX_TO_REMOTE_FEE` (0.001 ether). Emits `ToRemoteFeeChanged`. |
90
- | `deploySuckersFor(projectId, salt, configs)` | Project owner or delegate | `DEPLOY_SUCKERS` | Per-project | Deploys one or more suckers for a project using allowlisted deployers. Hashes salt with `_msgSender()` (which equals `msg.sender` for direct calls, or the relayed sender for ERC-2771 meta-transactions) for deterministic cross-chain addresses. Also calls `mapTokens` on each newly created sucker, so the registry must be able to satisfy the corresponding `MAP_SUCKER_TOKEN` checks for the project. |
91
- | `removeDeprecatedSucker(projectId, sucker)` | Anyone | None | Per-project | Removes a fully `DEPRECATED` sucker from the registry. Permissionless but only succeeds if the sucker is in the `DEPRECATED` state. |
92
-
93
- ### JBSucker
94
-
95
- | Function | Required Role | Permission ID | Scope | What It Does |
96
- |----------|--------------|---------------|-------|--------------|
97
- | `mapToken(map)` | Project owner or delegate | `MAP_SUCKER_TOKEN` | Per-sucker | Maps a local terminal token to a remote token, enabling bridging. Setting `remoteToken` to `bytes32(0)` disables bridging and sends a final root to flush remaining outbox entries. |
98
- | `mapTokens(maps)` | Project owner or delegate | `MAP_SUCKER_TOKEN` | Per-sucker | Batch version: maps multiple local-to-remote token pairs. Each mapping requires the same permission. |
99
- | `enableEmergencyHatchFor(tokens)` | Project owner or delegate | `SUCKER_SAFETY` | Per-sucker | Opens the emergency hatch for specified tokens (irreversible). Sets `emergencyHatch = true` and `enabled = false` on each token's remote mapping. Allows users to exit through the outbox on the chain they deposited on. |
100
- | `setDeprecation(timestamp)` | Project owner or delegate | `SET_SUCKER_DEPRECATION` | Per-sucker | Sets the timestamp after which the sucker becomes fully deprecated. Must be at least `_maxMessagingDelay()` (14 days) in the future. Set to `0` to cancel a pending deprecation. Reverts if already in `SENDING_DISABLED` or `DEPRECATED` state. |
101
-
102
- ### JBSuckerDeployer (base and all subclasses)
103
-
104
- | Function | Required Role | Permission ID | Scope | What It Does |
105
- |----------|--------------|---------------|-------|--------------|
106
- | `configureSingleton(singleton)` | Layer-Specific Configurator | N/A (direct address check) | Per-deployer | Sets the singleton implementation contract used to clone suckers via `LibClone`. Can only be called once. |
107
- | `setChainSpecificConstants(...)` | Layer-Specific Configurator | N/A (direct address check) | Per-deployer | Configures bridge-specific addresses (messenger, bridge, router, inbox, etc.) for the deployer. Can only be called once. Parameters vary by bridge type. |
108
-
109
- ## Deprecation Lifecycle
110
-
111
- The sucker deprecation lifecycle progresses through four states, controlled by `setDeprecation(timestamp)`:
112
-
113
- ```
114
- ENABLED --> DEPRECATION_PENDING --> SENDING_DISABLED --> DEPRECATED
115
- ```
116
-
117
- | State | Condition | Behavior |
118
- |-------|-----------|----------|
119
- | `ENABLED` | `deprecatedAfter == 0` | Fully functional. No deprecation is set. |
120
- | `DEPRECATION_PENDING` | `block.timestamp < deprecatedAfter - _maxMessagingDelay()` | Fully functional but a warning to users that deprecation is coming. |
121
- | `SENDING_DISABLED` | `block.timestamp < deprecatedAfter` (but past the messaging delay window) | `prepare()` and `toRemote()` revert. No new outbox entries or root sends. Incoming roots from the remote peer are still accepted. Users can `claim()` incoming tokens and `exitThroughEmergencyHatch()`. |
122
- | `DEPRECATED` | `block.timestamp >= deprecatedAfter` | Fully shut down. No new inbox roots are accepted (`fromRemote` skips the update). Users can still `claim()` against previously accepted inbox roots and `exitThroughEmergencyHatch()` against outbox entries that were never sent. |
123
-
124
- **Who controls transitions:**
125
- - The project owner (or holder of `SET_SUCKER_DEPRECATION` permission) sets the `deprecatedAfter` timestamp via `setDeprecation(timestamp)`.
126
- - The timestamp must be at least `_maxMessagingDelay()` (14 days) in the future to allow in-flight messages to arrive.
127
- - A pending deprecation can be cancelled by calling `setDeprecation(0)`, but only while in `ENABLED` or `DEPRECATION_PENDING` state.
128
- - Once `SENDING_DISABLED` or `DEPRECATED`, the deprecation cannot be reversed.
129
-
130
- **Removing from registry:** Once a sucker reaches `DEPRECATED`, anyone can call `JBSuckerRegistry.removeDeprecatedSucker(projectId, sucker)` to remove it from the project's sucker list.
131
-
132
- ## Emergency Hatch
133
-
134
- The emergency hatch is a per-token escape mechanism for when a bridge becomes non-functional for specific tokens.
135
-
136
- **Activation:** The project owner (or holder of `SUCKER_SAFETY` permission) calls `enableEmergencyHatchFor(tokens)` on the sucker. This is irreversible -- once opened for a token, that token can never be bridged by this sucker again.
137
-
138
- **Effect:** Sets `emergencyHatch = true` and `enabled = false` on each specified token's `JBRemoteToken` mapping. This:
139
- - Prevents new `prepare()` calls for the token (the token is no longer mapped/enabled).
140
- - Prevents `toRemote()` calls for the token (reverts with `JBSucker_TokenHasInvalidEmergencyHatchState`).
141
- - Prevents `mapToken()` from remapping or re-enabling the token (reverts with `JBSucker_TokenHasInvalidEmergencyHatchState`).
142
- - Enables `exitThroughEmergencyHatch()` for users whose outbox entries were never sent to the remote peer (entries with `index >= numberOfClaimsSent`).
143
-
144
- **Who can exit:** Any user with a valid outbox merkle proof for a leaf that was never sent over the bridge. The exit mints project tokens to the beneficiary and returns the terminal tokens to the project's terminal balance. The beneficiary can then cash out the minted project tokens through the normal Juicebox mechanism if desired.
145
-
146
- **Safety constraint:** Only leaves that were never communicated to the remote peer (i.e., `index >= outbox.numberOfClaimsSent`) can be emergency-exited. Leaves already sent over the bridge (index < `numberOfClaimsSent`) cannot be emergency-exited because they may have already been claimed on the remote chain.
147
-
148
- ## Immutable Configuration
149
-
150
- The following values are set at deploy time and cannot be changed:
151
-
152
- | Property | Contract | Set By | Description |
153
- |----------|----------|--------|-------------|
154
- | `DIRECTORY` | `JBSucker`, `JBSuckerRegistry`, all deployers | Constructor | The Juicebox directory contract. |
155
- | `FEE_PROJECT_ID` | `JBSucker` | Constructor | The project that receives `toRemoteFee` payments via `terminal.pay()` on each `toRemote()` call. Typically project ID 1 (the protocol project). Best-effort: fee is silently skipped if the fee project has no native token terminal or if `terminal.pay()` reverts. |
156
- | `REGISTRY` | `JBSucker` | Constructor | The `JBSuckerRegistry` that this sucker reads the `toRemoteFee` from. |
157
- | `MAX_TO_REMOTE_FEE` | `JBSuckerRegistry` | Constant | Hard cap on what `toRemoteFee` can be set to (0.001 ether). Prevents the registry owner from setting an excessively high fee. |
158
- | `TOKENS` | `JBSucker`, all deployers | Constructor | The Juicebox token management contract. |
159
- | `PROJECTS` | `JBSuckerRegistry` | Derived from `DIRECTORY.PROJECTS()` | The ERC-721 project ownership contract. |
160
- | `OPBRIDGE` | `JBOptimismSucker`, `JBBaseSucker`, `JBCeloSucker` | Constructor (from deployer callback) | The OP Standard Bridge address. |
161
- | `OPMESSENGER` | `JBOptimismSucker`, `JBBaseSucker`, `JBCeloSucker` | Constructor (from deployer callback) | The OP Cross-Domain Messenger address. |
162
- | `ARBINBOX` | `JBArbitrumSucker` | Constructor (from deployer callback) | The Arbitrum Inbox address. |
163
- | `GATEWAYROUTER` | `JBArbitrumSucker` | Constructor (from deployer callback) | The Arbitrum Gateway Router address. |
164
- | `LAYER` | `JBArbitrumSucker` | Constructor (from deployer callback) | Whether this is an L1 or L2 sucker. |
165
- | `CCIP_ROUTER` | `JBCCIPSucker` | Constructor (from deployer callback) | The Chainlink CCIP Router address. |
166
- | `REMOTE_CHAIN_ID` | `JBCCIPSucker` | Constructor (from deployer callback) | The remote chain's chain ID. |
167
- | `REMOTE_CHAIN_SELECTOR` | `JBCCIPSucker` | Constructor (from deployer callback) | The CCIP chain selector for the remote chain. |
168
- | `WRAPPED_NATIVE` | `JBCeloSucker` | Constructor (from deployer callback) | The wrapped native token (WETH) on the local chain. |
169
- | `LAYER_SPECIFIC_CONFIGURATOR` | All deployers | Constructor | The address authorized to call one-time setup functions. |
170
- | `peer()` | `JBSucker` | Deterministic (returns `bytes32` via `_toBytes32(address(this))`) | The remote peer sucker address as `bytes32`. Defaults to the local address, relying on CREATE2 giving the same address on both chains. |
171
- | `deployer` | `JBSucker` | Set once in `initialize()` by `msg.sender` | The address that deployed this sucker clone (the deployer contract). |
172
- | `projectId()` | `JBSucker` | Set once in `initialize()` | The local project ID this sucker serves. |
173
- | Bridge/messenger/router addresses | All deployers | `setChainSpecificConstants()` (one-time) | Bridge infrastructure addresses, set once by the configurator. |
174
- | Singleton implementation | All deployers | `configureSingleton()` (one-time) | The singleton contract used as the clone template. |
175
-
176
- ## Admin Boundaries
177
-
178
- What admins **cannot** do:
179
-
180
- - **Cannot remap tokens after outbox activity.** Once a token has outbox tree entries (`_outboxOf[token].tree.count != 0`), it cannot be remapped to a different remote token. It can only be disabled (set to `bytes32(0)`) or re-enabled to the same remote address. This prevents double-spending across two different remote tokens.
181
-
182
- - **Cannot reverse an emergency hatch.** Once `enableEmergencyHatchFor` is called for a token, `emergencyHatch` is permanently `true`. The token can never be re-enabled for bridging on this sucker. A new sucker must be deployed if bridging needs to resume.
183
-
184
- - **Cannot reverse deprecation past `SENDING_DISABLED`.** Once the sucker enters `SENDING_DISABLED` or `DEPRECATED` state, `setDeprecation()` reverts. The deprecation cannot be cancelled.
185
-
186
- - **Cannot bypass bridge security.** Cross-chain message authentication is enforced by each bridge implementation's `_isRemotePeer()` check. The project owner has no way to override this -- only messages from the legitimate remote peer (verified by the OP Messenger, Arbitrum Bridge/Outbox, or CCIP Router) are accepted by `fromRemote()`.
187
-
188
- - **Cannot reconfigure deployers.** `setChainSpecificConstants()` and `configureSingleton()` can each only be called once per deployer. Bridge addresses and the singleton implementation are permanent after initial configuration.
189
-
190
- - **Cannot steal user funds via emergency hatch.** Emergency exit only returns tokens to the original beneficiary specified in the outbox merkle tree, and only for leaves that were never sent over the bridge (index >= `numberOfClaimsSent`). Leaves already bridged cannot be double-claimed.
191
-
192
- - **Cannot claim on behalf of others.** While `claim()` and `exitThroughEmergencyHatch()` are permissionless (anyone can call them), the project tokens are always minted to the beneficiary encoded in the merkle tree leaf, not to the caller.
193
-
194
- - **Cannot change the peer address.** The peer is determined by deterministic deployment (CREATE2 with sender-specific salt). There is no admin function to change which remote address is trusted.
195
-
196
- - **Cannot change the project ID.** The `projectId` is set once during `initialize()` and is immutable thereafter (enforced by OpenZeppelin `Initializable`).
6
+ | --- | --- |
7
+ | Scope | Cross-chain claim movement, token mapping, fees, and deprecation controls |
8
+ | Control posture | Mixed registry-owner, project-permission, and bridge-specific trust |
9
+ | Highest-risk actions | Wrong token mapping, wrong peer assumptions, and bad emergency or deprecation handling |
10
+ | Recovery posture | Often one-way; many recovery paths are intentionally irreversible |
197
11
 
198
- - **Cannot change the fee project.** `FEE_PROJECT_ID` is set at construction and is immutable. If the fee project's terminal changes or is removed, fee payments silently stop (best-effort design), but `toRemote()` still works.
12
+ ## Purpose
199
13
 
200
- - **Can adjust the fee amount, within bounds.** The registry owner can call `setToRemoteFee()` on `JBSuckerRegistry` to adjust the fee globally for all suckers, but it is capped at `MAX_TO_REMOTE_FEE` (0.001 ether).
14
+ This repo controls the shared lifecycle around bridging project positions, not just the transport call itself.
201
15
 
202
- ## Bridge Infrastructure Risks
16
+ ## Control Model
203
17
 
204
- Sucker security ultimately depends on the underlying bridge infrastructure (OP Cross-Domain Messenger, Arbitrum Inbox/Outbox, CCIP Router). Each sucker's `_isRemotePeer()` check trusts these bridges to faithfully report the sender of cross-chain messages. If a bridge itself is compromised, an attacker could forge messages that pass the peer check, potentially minting unbacked project tokens on the destination chain.
18
+ - registry owner controls shared fee settings and deployer allowlists
19
+ - project-level permissions control token mapping and safety paths
20
+ - bridge-specific implementations inherit external trust assumptions
205
21
 
206
- **Mitigations available to project owners:**
22
+ ## Recovery
207
23
 
208
- - **Emergency hatch.** If a bridge is suspected to be compromised for a specific token, the project owner (or `SUCKER_SAFETY` delegate) can call `enableEmergencyHatchFor()` to immediately disable bridging for that token and allow users to exit with their outbox entries locally.
209
- - **Deprecation.** If the bridge is broadly compromised, the project owner (or `SET_SUCKER_DEPRECATION` delegate) can deprecate the entire sucker via `setDeprecation()`, shutting down all new outbound and eventually inbound activity.
24
+ - emergency hatch and deprecation are the main recovery tools
25
+ - both are intentionally conservative and often one-way
210
26
 
211
- **Fee delivery is best-effort.** The `toRemoteFee` is collected by calling `terminal.pay()` to the fee project during `toRemote()`. If this call reverts (e.g., the fee project has no native token terminal or its terminal is misconfigured), the fee is silently skipped and `toRemote()` proceeds normally. This ensures bridging availability is never blocked by fee infrastructure issues, but it also means fee revenue can be lost without any on-chain signal.
package/ARCHITECTURE.md CHANGED
@@ -2,59 +2,33 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- `nana-suckers-v6` moves Juicebox project token value across chains. A sucker pair lets a holder burn or cash out locally, bridge the resulting terminal token plus the latest merkle root, and then claim equivalent project-token value on the remote chain.
5
+ `nana-suckers-v6` bridges Juicebox project positions across chains by turning local burns into claimable remote mints.
6
6
 
7
- ## Boundaries
7
+ ## System Overview
8
8
 
9
- - `JBSucker` owns the chain-agnostic lifecycle.
10
- - Chain-specific subclasses own transport details for OP Stack, Arbitrum, CCIP, and Celo variants.
11
- - The registry and deployers own pair creation and global policy.
12
- - Core protocol accounting still happens through project terminals on each chain.
9
+ `JBSucker` handles prepare, relay, claim, token mapping, deprecation, and emergency exits. `JBSuckerRegistry` tracks deployments, deployer allowlists, and shared fee settings. Bridge-specific implementations handle transport details.
13
10
 
14
- ## Main Components
11
+ ## Core Invariants
15
12
 
16
- | Component | Responsibility |
17
- | --- | --- |
18
- | `JBSucker` | Prepare, root management, claim verification, token mapping, and deprecation controls |
19
- | chain-specific suckers | Bridge transport and token-wrapping details for each supported network family |
20
- | `JBSuckerRegistry` | Tracks deployed suckers, allowlists deployers, and stores global bridge-fee policy |
21
- | deployers | Clone and initialize deterministic sucker instances |
22
- | `MerkleLib` and helper libraries | Incremental tree logic plus chain-specific constants |
13
+ - Merkle trees stay append-only
14
+ - nonce progression stays monotonic
15
+ - token mapping stays coherent across peers
16
+ - claims and emergency exits do not double-spend
17
+ - outbox balance accounting stays consistent through send and recovery flows
23
18
 
24
- ## Runtime Model
19
+ ## Trust Boundaries
25
20
 
26
- ```text
27
- holder prepares a bridge
28
- -> sucker cashes out or consumes the local project-token position
29
- -> sucker inserts a merkle leaf into the outbox tree
30
- -> someone bridges funds and the latest root to the remote sucker
31
- -> claimant proves inclusion against the remote inbox tree
32
- -> remote sucker releases or remints the destination-side value
33
- ```
21
+ - shared logic lives in `JBSucker`
22
+ - transport security lives in the bridge-specific implementation and external bridge counterparties
23
+ - registry decisions can widen or constrain the allowed deployment surface
34
24
 
35
- ## Critical Invariants
25
+ ## Security Model
36
26
 
37
- - Inbox and outbox tree updates must remain append-only and proof-compatible across chains.
38
- - Token mapping is part of bridge correctness; a wrong remote token mapping is a value-loss bug.
39
- - Deprecation and emergency controls must not compromise already-bridged claims.
40
- - The chain-specific transport layers may differ, but they must preserve the same logical prepare-to-claim lifecycle.
27
+ - the biggest risks are non-atomic cross-chain state, bad token mapping, and broken peer assumptions
28
+ - bridge liveness and correct peer identity are real trust assumptions
41
29
 
42
- ## Where Complexity Lives
30
+ ## Source Map
43
31
 
44
- - The abstract lifecycle is simple, but every transport backend has its own failure modes and native-token quirks.
45
- - Tree state, token mapping, and claim replay protection all carry cross-chain blast radius.
46
- - Registry policy matters because deployment mistakes are hard to repair after pairs exist on multiple chains.
47
-
48
- ## Dependencies
49
-
50
- - `nana-core-v6` terminals and project-token semantics
51
- - Native bridge infrastructure for OP Stack, Arbitrum, CCIP, and supported variants
52
- - `nana-permission-ids-v6` for deploy, map, safety, and deprecation permissions
53
-
54
- ## Safe Change Guide
55
-
56
- - Review every cross-chain change from both sides of the pair.
57
- - Do not change merkle leaf encoding casually; proofs and remote compatibility depend on it.
58
- - Keep registry policy, deployer configuration, and singleton logic aligned.
59
- - Test chain-specific native-token wrapping paths separately from the abstract lifecycle.
60
- - Assume bridge-edge behavior is the primary review target, not the happy path.
32
+ - `src/JBSucker.sol`
33
+ - `src/JBSuckerRegistry.sol`
34
+ - `src/utils/MerkleLib.sol`
@@ -1,128 +1,33 @@
1
1
  # Audit Instructions
2
2
 
3
- This repo bridges Juicebox project tokens and associated terminal assets across chains. Audit it as a conservation and replay-prevention system.
3
+ Audit this repo as cross-chain claim and recovery logic, not as a generic ERC-20 bridge.
4
4
 
5
- ## Objective
5
+ ## Audit Objective
6
6
 
7
7
  Find issues that:
8
- - allow double claim, replay, or claim on the wrong destination
9
- - lose or strand bridged backing assets
10
- - let deprecated or emergency paths violate intended safety rules
11
- - mis-handle root ordering, especially across asynchronous bridge transports
12
- - grant mapping or safety privileges more broadly than intended
8
+
9
+ - break Merkle-root or nonce progression
10
+ - allow bad token mapping or peer assumptions
11
+ - permit double-claim or bad emergency exit behavior
12
+ - make non-atomic bridge semantics unsafe
13
13
 
14
14
  ## Scope
15
15
 
16
16
  In scope:
17
- - all Solidity under `src/`
18
- - deployer contracts under `src/deployers/`
17
+
18
+ - `src/JBSucker.sol`
19
+ - `src/JBSuckerRegistry.sol`
20
+ - bridge-specific implementations and deployers
19
21
  - `src/utils/MerkleLib.sol`
20
- - libraries, enums, interfaces, and structs under `src/`
21
- - deployment scripts in `script/`
22
22
 
23
23
  ## Start Here
24
24
 
25
- Read in this order:
26
- - the shared flow in `JBSucker`
27
- - claim validation and execution tracking
28
- - token mapping and emergency-hatch logic
29
- - one native bridge implementation
30
- - `JBCCIPSucker`
31
- - deployers and registry assumptions
32
-
33
- That order gets you from the shared conservation model to the transport-specific deviations.
34
-
35
- ## System Model
36
-
37
- The bridge flow is:
38
- - burn or prepare project-token value on source chain
39
- - record a leaf into an outbox tree
40
- - send a merkle root and backing assets over a chain-specific transport
41
- - receive the root on the remote chain
42
- - claim by proving inclusion against the current inbox root
43
-
44
- This repo supports multiple transport implementations:
45
- - OP Stack variants
46
- - Arbitrum
47
- - CCIP
48
- - related deployers and registries
49
-
50
- One non-obvious property to audit explicitly:
51
- - roots and assets do not always arrive in a perfectly ordered, synchronous way
52
- - the system is intentionally designed to survive some transport mismatch without deadlocking
53
- - those recovery choices are exactly where conservation bugs tend to hide
54
-
55
- ## Critical Invariants
56
-
57
- 1. Cross-chain conservation
58
- For any prepared transfer, destination claimable value must not exceed what the source side actually prepared and backed.
59
-
60
- 2. Single execution
61
- Each bridged leaf must be claimable at most once on the destination and at most once via emergency exit.
62
-
63
- 3. Peer authenticity
64
- Only the intended remote peer and messenger path may update inbox roots.
65
-
66
- 4. Deprecation safety
67
- Deprecation and emergency-hatch controls must not let callers bypass intended restrictions or steal in-flight funds.
68
-
69
- 5. Token mapping integrity
70
- Remote token mappings must be immutable or mutable only exactly where the design allows.
25
+ 1. `src/JBSucker.sol`
26
+ 2. `src/JBSuckerRegistry.sol`
27
+ 3. the relevant bridge-specific implementation
71
28
 
72
- 6. Nonce progression is monotonic in the way each transport expects
73
- Later roots must not silently invalidate earlier user claims unless the protocol explicitly intends that recovery path.
29
+ ## Verification
74
30
 
75
- ## Threat Model
76
-
77
- Prioritize:
78
- - out-of-order nonce arrival
79
- - cross-sucker replay
80
- - trusted-forwarder or messenger spoofing
81
- - emergency-exit races
82
- - fee fallback and bridge-payment edge cases
83
- - deterministic deployer assumptions for peer pairing
84
-
85
- The strongest attacker models here are:
86
- - a caller trying to claim from the wrong root with a structurally valid proof
87
- - a privileged actor abusing token mapping or emergency controls after users already prepared transfers
88
- - a transport delivering messages out of order and exposing assumptions hidden in the happy path
89
-
90
- ## Hotspots
91
-
92
- - `prepare`, `toRemote`, `fromRemote`, and `claim`
93
- - bitmap execution tracking
94
- - root and nonce handling
95
- - token mapping and registry trust
96
- - chain-specific messenger authentication
97
- - deployer address derivation and clone setup
98
-
99
- ## Sequences Worth Replaying
100
-
101
- 1. Prepare multiple leaves -> send multiple roots -> receive them out of order -> attempt claims for each.
102
- 2. Prepare -> deprecate or enable emergency hatch -> claim and exit attempts racing each other.
103
- 3. Map token -> prepare transfer -> attempt remap or peer mismatch after value is already in flight.
104
- 4. Same logical transfer across different sucker implementations to check for replay or identity confusion.
105
-
106
- ## Finding Bar
107
-
108
- The strongest findings here usually show one of these:
109
- - a user can claim against value that was never actually prepared
110
- - a valid prepare becomes permanently unclaimable without the recovery path the protocol expects
111
- - transport-specific authentication is weaker than the shared model assumes
112
- - a privileged mapping or safety control can rewrite the meaning of already in-flight value
113
-
114
- ## Build And Verification
115
-
116
- Standard workflow:
117
31
  - `npm install`
118
32
  - `forge build`
119
33
  - `forge test`
120
-
121
- The current tests already target:
122
- - deep attack and regression scenarios
123
- - trusted-forwarder spoofing
124
- - fee fallback behavior
125
- - deterministic deployment
126
- - chain-specific fork flows
127
-
128
- High-value findings here show a break in conservation, replay resistance, or trusted-peer boundaries.
package/README.md CHANGED
@@ -3,7 +3,12 @@
3
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
5
  Docs: <https://docs.juicebox.money>
6
- Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
7
+ User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
8
+ Skills: [SKILLS.md](./SKILLS.md)
9
+ Risks: [RISKS.md](./RISKS.md)
10
+ Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
11
+ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
7
12
 
8
13
  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.
9
14
 
@@ -19,7 +24,7 @@ The base implementation is extended for multiple bridge families so the same pro
19
24
 
20
25
  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.
21
26
 
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
+ The main idea is not "bridge the token contract." The main idea is "bridge a Juicebox claim plus enough information to recreate the project-token position on the remote chain."
23
28
 
24
29
  ## Key Contracts
25
30
 
@@ -27,11 +32,7 @@ The main idea is not "bridge the token contract." The main idea is "bridge a Jui
27
32
  | --- | --- |
28
33
  | `JBSucker` | Base bridge logic for prepare, relay, claim, token mapping, and lifecycle controls. |
29
34
  | `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. |
35
+ | Chain-specific suckers | Transport-specific implementations for OP Stack, Arbitrum, CCIP, and related environments. |
35
36
 
36
37
  ## Mental Model
37
38
 
@@ -45,28 +46,6 @@ That means every bridge path has two trust surfaces:
45
46
  - the shared sucker accounting and Merkle logic
46
47
  - the bridge-specific transport implementation
47
48
 
48
- The shortest useful reading order is:
49
-
50
- | Contract | Description |
51
- |----------|-------------|
52
- | [`JBSucker`](src/JBSucker.sol) | Abstract base. Manages outbox/inbox merkle trees, `prepare`/`toRemote`/`claim` lifecycle, token mapping, deprecation, and emergency hatch. Deployed as clones via `Initializable`. Uses `ERC2771Context` for meta-transactions. Has immutable `FEE_PROJECT_ID` (typically project ID 1) and immutable `REGISTRY` reference. Reads the `toRemoteFee` from the registry via `REGISTRY.toRemoteFee()` on each `toRemote()` call. |
53
- | [`JBCCIPSucker`](src/JBCCIPSucker.sol) | Extends `JBSucker`. Bridges via Chainlink CCIP (`ccipSend`/`ccipReceive`). Supports any CCIP-connected chain pair. Wraps native ETH to WETH before bridging (CCIP only transports ERC-20s) and unwraps on the receiving end. Can map `NATIVE_TOKEN` to ERC-20 addresses on the remote chain (unlike OP/Arbitrum suckers). |
54
- | [`JBOptimismSucker`](src/JBOptimismSucker.sol) | Extends `JBSucker`. Bridges via OP Standard Bridge + OP Messenger. No `msg.value` required for transport. |
55
- | [`JBBaseSucker`](src/JBBaseSucker.sol) | Thin wrapper around `JBOptimismSucker` with Base chain IDs (Ethereum 1 <-> Base 8453, Sepolia 11155111 <-> Base Sepolia 84532). |
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). |
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. |
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. |
59
- | [`JBSuckerDeployer`](src/JBSuckerDeployer.sol) | Abstract base deployer. Clones a singleton sucker via `LibClone.cloneDeterministic` and initializes it. Two-phase setup: `setChainSpecificConstants` then `configureSingleton`. |
60
- | [`JBCCIPSuckerDeployer`](src/deployers/JBCCIPSuckerDeployer.sol) | Deployer for `JBCCIPSucker`. Stores CCIP router, remote chain ID, and CCIP chain selector. |
61
- | [`JBOptimismSuckerDeployer`](src/deployers/JBOptimismSuckerDeployer.sol) | Deployer for `JBOptimismSucker`. Stores OP Messenger and OP Bridge addresses. |
62
- | [`JBBaseSuckerDeployer`](src/deployers/JBBaseSuckerDeployer.sol) | Thin wrapper around `JBOptimismSuckerDeployer` for Base. |
63
- | [`JBCeloSuckerDeployer`](src/deployers/JBCeloSuckerDeployer.sol) | Deployer for `JBCeloSucker`. Extends `JBOptimismSuckerDeployer` with `wrappedNative` (`IWrappedNativeToken`) storage for the local chain's WETH address. |
64
- | [`JBArbitrumSuckerDeployer`](src/deployers/JBArbitrumSuckerDeployer.sol) | Deployer for `JBArbitrumSucker`. Stores Arbitrum Inbox, Gateway Router, and layer (`JBLayer.L1` or `JBLayer.L2`). |
65
- | [`MerkleLib`](src/utils/MerkleLib.sol) | Incremental merkle tree (depth 32, max ~4 billion leaves, modeled on eth2 deposit contract). Used for outbox/inbox trees. `insert` and `root` operate directly on `Tree storage` (not memory copies) to avoid redundant SLOAD/SSTORE round-trips. Gas-optimized with inline assembly for `root()` and `branchRoot()`. |
66
- | [`CCIPHelper`](src/libraries/CCIPHelper.sol) | CCIP router addresses, chain selectors, and WETH addresses per chain. Covers Ethereum, Optimism, Arbitrum, Base, Polygon, Avalanche, and BNB Chain (mainnet and testnets). |
67
- | [`ARBAddresses`](src/libraries/ARBAddresses.sol) | Arbitrum bridge contract addresses (Inbox, Gateway Router) for mainnet and Sepolia. |
68
- | [`ARBChains`](src/libraries/ARBChains.sol) | Arbitrum chain ID constants. |
69
-
70
49
  ## Read These Files First
71
50
 
72
51
  1. `src/JBSucker.sol`
@@ -77,18 +56,24 @@ The shortest useful reading order is:
77
56
 
78
57
  ## Integration Traps
79
58
 
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
59
+ - do not reason about suckers as if they were generic ERC-20 bridges
60
+ - root ordering and message delivery semantics matter as much as proof format
61
+ - token mapping is part of the economic invariant
62
+ - emergency and deprecation paths are part of normal operational safety
84
63
 
85
64
  ## Where State Lives
86
65
 
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
66
+ - per-claim and tree progression state: the sucker pair
67
+ - deployment inventory and shared operational config: `JBSuckerRegistry`
68
+ - bridge transport assumptions: the chain-specific implementation and its external counterparties
69
+
70
+ ## High-Signal Tests
90
71
 
91
- When reviewing a bridge incident, check local state transition correctness before blaming the transport layer.
72
+ 1. `test/unit/registry.t.sol`
73
+ 2. `test/unit/multi_chain_evolution.t.sol`
74
+ 3. `test/ForkClaimMainnet.t.sol`
75
+ 4. `test/audit/codex-PeerSnapshotDesync.t.sol`
76
+ 5. `test/audit/codex-ToRemoteFeeIrrecoverable.t.sol`
92
77
 
93
78
  ## Install
94
79
 
@@ -112,7 +97,7 @@ Useful scripts:
112
97
 
113
98
  ## Deployment Notes
114
99
 
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.
100
+ This package supports multiple bridge families and is intentionally split into bridge-specific deployers. Operational support is narrower than "all theoretically bridgeable chains" and should be taken from the configured deployers, helper libraries, and deployment scripts in this repo.
116
101
 
117
102
  ## Repository Layout
118
103
 
@@ -136,9 +121,13 @@ script/
136
121
 
137
122
  ## Risks And Notes
138
123
 
139
- - out-of-order root delivery can make some claims unclaimable until an operator uses an emergency path
124
+ - out-of-order root delivery can make some claims unavailable until an operator uses an emergency path
140
125
  - bridge-specific transport assumptions matter as much as the shared sucker logic
141
126
  - token mapping and deprecation controls are governance-sensitive surfaces
142
127
  - a bridge that stays live operationally still may not be economically safe for every asset or chain pair
143
128
 
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.
129
+ ## For AI Agents
130
+
131
+ - Do not summarize this repo as a generic token bridge.
132
+ - Always separate shared sucker logic from bridge-specific transport behavior.
133
+ - Use the chain-specific implementation and matching deployer together when answering operational questions.
package/RISKS.md CHANGED
@@ -62,6 +62,7 @@ This file focuses on the bridge-like risks in the sucker system: merkle-root pro
62
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.
63
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.
64
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.
65
+ - **Registry aggregate views fail open.** `JBSuckerRegistry.remoteBalanceOf`, `remoteSurplusOf`, and `remoteTotalSupplyOf` sum values across active suckers but silently skip any sucker that reverts. This preserves liveness for dashboards and cross-chain estimates, but it means the returned aggregate can understate remote state whenever one peer is broken, censored, deprecated incorrectly, or simply expensive to query.
65
66
 
66
67
  ## 6. Deprecation Lifecycle
67
68
 
@@ -127,10 +128,14 @@ The registry owner can adjust `toRemoteFee` via `JBSuckerRegistry.setToRemoteFee
127
128
 
128
129
  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
 
130
- ### 10.5 Hookless V4 spot pricing is sandwich-vulnerable by design
131
+ ### 10.5 Registry aggregate views prioritize liveness over completeness
132
+
133
+ `JBSuckerRegistry.remoteBalanceOf`, `remoteSurplusOf`, and `remoteTotalSupplyOf` intentionally use `try/catch` around each sucker and silently ignore peers that revert. This is accepted because a single bad peer should not brick every cross-chain dashboard or estimator. The trade-off is that these read surfaces are best-effort only: consumers must treat them as lower bounds, not exact reconciled totals, unless they independently verify that every active sucker responded successfully.
134
+
135
+ ### 10.6 Hookless V4 spot pricing is sandwich-vulnerable by design
131
136
 
132
137
  When the only available Uniswap pool for a cross-denomination swap is a hookless V4 pool (no V3 pool exists), `_getV4Quote` falls back to the instantaneous spot tick from `POOL_MANAGER.getSlot0()` instead of a TWAP oracle. This tick is manipulable via sandwich attacks, allowing an attacker to skew the `minAmountOut` and extract value from the swap. The sigmoid slippage model limits the damage but operates on a corrupted baseline. This is an accepted tradeoff: reverting when no TWAP is available would cause the CCIP message to fail, leaving bridged tokens stuck in the CCIP router until manual retry. Getting some value (even at a worse rate) is preferred over a stuck bridge message, especially since this path only triggers when no V3 pool with built-in TWAP exists at all. The hooked V4 path also falls back to spot if the hook's `observe()` reverts, with the same tradeoff.
133
138
 
134
- ### 10.6 `mapTokens` refunds ETH on enable-only batches
139
+ ### 10.7 `mapTokens` refunds ETH on enable-only batches
135
140
 
136
141
  `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`), the full `msg.value` is refunded to `_msgSender()`. If the refund transfer fails (e.g., the caller is a non-payable contract), the call reverts with `JBSucker_RefundFailed`. When disables are present, any dust remainder from integer division (`msg.value % numberToDisable`) is also refunded on a best-effort basis.