@bananapus/suckers-v6 0.0.6 → 0.0.8

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.
Files changed (47) hide show
  1. package/ADMINISTRATION.md +162 -0
  2. package/ARCHITECTURE.md +71 -0
  3. package/README.md +5 -1
  4. package/RISKS.md +48 -0
  5. package/SKILLS.md +3 -1
  6. package/STYLE_GUIDE.md +468 -0
  7. package/foundry.toml +2 -2
  8. package/package.json +3 -3
  9. package/script/Deploy.s.sol +20 -7
  10. package/src/JBArbitrumSucker.sol +15 -4
  11. package/src/JBBaseSucker.sol +8 -1
  12. package/src/JBCCIPSucker.sol +7 -7
  13. package/src/JBCeloSucker.sol +196 -0
  14. package/src/JBOptimismSucker.sol +2 -4
  15. package/src/JBSucker.sol +32 -22
  16. package/src/JBSuckerRegistry.sol +4 -6
  17. package/src/deployers/JBArbitrumSuckerDeployer.sol +10 -9
  18. package/src/deployers/JBBaseSuckerDeployer.sol +6 -3
  19. package/src/deployers/JBCCIPSuckerDeployer.sol +14 -13
  20. package/src/deployers/JBCeloSuckerDeployer.sol +86 -0
  21. package/src/deployers/JBOptimismSuckerDeployer.sol +9 -10
  22. package/src/deployers/JBSuckerDeployer.sol +24 -13
  23. package/src/interfaces/IArbGatewayRouter.sol +5 -3
  24. package/src/interfaces/IArbL1GatewayRouter.sol +4 -0
  25. package/src/interfaces/IArbL2GatewayRouter.sol +4 -0
  26. package/src/interfaces/ICCIPRouter.sol +4 -2
  27. package/src/interfaces/IJBArbitrumSucker.sol +8 -0
  28. package/src/interfaces/IJBArbitrumSuckerDeployer.sol +9 -1
  29. package/src/interfaces/IJBCCIPSuckerDeployer.sol +17 -2
  30. package/src/interfaces/IJBCeloSuckerDeployer.sol +25 -0
  31. package/src/interfaces/IJBOpSuckerDeployer.sol +11 -1
  32. package/src/interfaces/IJBOptimismSucker.sol +6 -0
  33. package/src/interfaces/IJBSucker.sol +64 -22
  34. package/src/interfaces/IJBSuckerDeployer.sol +7 -10
  35. package/src/interfaces/IJBSuckerExtended.sol +26 -1
  36. package/src/interfaces/IJBSuckerRegistry.sol +26 -1
  37. package/src/interfaces/IOPMessenger.sol +7 -1
  38. package/src/interfaces/IOPStandardBridge.sol +4 -15
  39. package/src/interfaces/IWrappedNativeToken.sol +6 -4
  40. package/src/utils/MerkleLib.sol +15 -5
  41. package/test/ForkCelo.t.sol +419 -0
  42. package/test/ForkMainnet.t.sol +436 -0
  43. package/test/JBSucker_AuditFindings.t.sol +361 -0
  44. package/test/regression/L47_MapTokensDust.t.sol +1 -1
  45. package/test/unit/deployer.t.sol +1 -0
  46. package/test/unit/merkle.t.sol +1 -0
  47. package/SECURITY.md +0 -55
@@ -0,0 +1,162 @@
1
+ # Administration
2
+
3
+ Admin privileges and their scope in nana-suckers-v6.
4
+
5
+ ## Roles
6
+
7
+ ### Registry Owner
8
+
9
+ - **How assigned:** Set at `JBSuckerRegistry` construction via the `initialOwner` parameter. Transferable via OpenZeppelin `Ownable`.
10
+ - **Scope:** Controls which sucker deployer contracts are approved for use by the registry. Initially expected to be JuiceboxDAO (project ID 1).
11
+ - **Permission ID:** None (uses `onlyOwner` modifier from OpenZeppelin `Ownable`).
12
+
13
+ ### Project Owner
14
+
15
+ - **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)`.
16
+ - **Scope:** Controls sucker deployment, token mapping, deprecation, and emergency hatch for their project. Can delegate any of these permissions to other addresses via `JBPermissions`.
17
+ - **Permission ID:** N/A (is the root authority that grants the permission IDs below).
18
+
19
+ ### Sucker Deployer (delegated role)
20
+
21
+ - **How assigned:** Granted `DEPLOY_SUCKERS` permission by the project owner via `JBPermissions`.
22
+ - **Scope:** Can deploy new suckers for a specific project through the registry.
23
+ - **Permission ID:** `JBPermissionIds.DEPLOY_SUCKERS`
24
+
25
+ ### Token Mapper (delegated role)
26
+
27
+ - **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.
28
+ - **Scope:** Can map or disable local-to-remote token pairs on a specific sucker.
29
+ - **Permission ID:** `JBPermissionIds.MAP_SUCKER_TOKEN`
30
+
31
+ ### Safety Admin (delegated role)
32
+
33
+ - **How assigned:** Granted `SUCKER_SAFETY` permission by the project owner via `JBPermissions`.
34
+ - **Scope:** Can open the emergency hatch for specific tokens on a sucker. This is an irreversible action.
35
+ - **Permission ID:** `JBPermissionIds.SUCKER_SAFETY`
36
+
37
+ ### Deprecation Admin (delegated role)
38
+
39
+ - **How assigned:** Granted `SET_SUCKER_DEPRECATION` permission by the project owner via `JBPermissions`.
40
+ - **Scope:** Can set or cancel the deprecation timestamp on a sucker.
41
+ - **Permission ID:** `JBPermissionIds.SET_SUCKER_DEPRECATION`
42
+
43
+ ### Layer-Specific Configurator
44
+
45
+ - **How assigned:** Set at deployer construction via the `configurator` parameter. Stored as the immutable `LAYER_SPECIFIC_CONFIGURATOR` address on each `JBSuckerDeployer`.
46
+ - **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.
47
+ - **Permission ID:** None (uses direct `_msgSender()` check against `LAYER_SPECIFIC_CONFIGURATOR`).
48
+
49
+ ## Privileged Functions
50
+
51
+ ### JBSuckerRegistry
52
+
53
+ | Function | Required Role | Permission ID | Scope | What It Does |
54
+ |----------|--------------|---------------|-------|--------------|
55
+ | `allowSuckerDeployer(deployer)` | Registry Owner | N/A (`onlyOwner`) | Global | Adds a deployer contract to the allowlist, enabling it to be used when deploying suckers. |
56
+ | `allowSuckerDeployers(deployers)` | Registry Owner | N/A (`onlyOwner`) | Global | Batch version: adds multiple deployer contracts to the allowlist. |
57
+ | `removeSuckerDeployer(deployer)` | Registry Owner | N/A (`onlyOwner`) | Global | Removes a deployer contract from the allowlist, preventing future sucker deployments through it. |
58
+ | `deploySuckersFor(projectId, salt, configs)` | Project Owner | `DEPLOY_SUCKERS` | Per-project | Deploys one or more suckers for a project using allowlisted deployers. Hashes salt with `msg.sender` for deterministic cross-chain addresses. Also calls `mapTokens` on each newly created sucker. |
59
+ | `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. |
60
+
61
+ ### JBSucker
62
+
63
+ | Function | Required Role | Permission ID | Scope | What It Does |
64
+ |----------|--------------|---------------|-------|--------------|
65
+ | `mapToken(map)` | Project Owner | `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. |
66
+ | `mapTokens(maps)` | Project Owner | `MAP_SUCKER_TOKEN` | Per-sucker | Batch version: maps multiple local-to-remote token pairs. Each mapping requires the same permission. |
67
+ | `enableEmergencyHatchFor(tokens)` | Project Owner | `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. |
68
+ | `setDeprecation(timestamp)` | Project Owner | `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. |
69
+
70
+ ### JBSuckerDeployer (base and all subclasses)
71
+
72
+ | Function | Required Role | Permission ID | Scope | What It Does |
73
+ |----------|--------------|---------------|-------|--------------|
74
+ | `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. |
75
+ | `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. |
76
+
77
+ ## Deprecation Lifecycle
78
+
79
+ The sucker deprecation lifecycle progresses through four states, controlled by `setDeprecation(timestamp)`:
80
+
81
+ ```
82
+ ENABLED --> DEPRECATION_PENDING --> SENDING_DISABLED --> DEPRECATED
83
+ ```
84
+
85
+ | State | Condition | Behavior |
86
+ |-------|-----------|----------|
87
+ | `ENABLED` | `deprecatedAfter == 0` | Fully functional. No deprecation is set. |
88
+ | `DEPRECATION_PENDING` | `block.timestamp < deprecatedAfter - _maxMessagingDelay()` | Fully functional but a warning to users that deprecation is coming. |
89
+ | `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()`. |
90
+ | `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. |
91
+
92
+ **Who controls transitions:**
93
+ - The project owner (or holder of `SET_SUCKER_DEPRECATION` permission) sets the `deprecatedAfter` timestamp via `setDeprecation(timestamp)`.
94
+ - The timestamp must be at least `_maxMessagingDelay()` (14 days) in the future to allow in-flight messages to arrive.
95
+ - A pending deprecation can be cancelled by calling `setDeprecation(0)`, but only while in `ENABLED` or `DEPRECATION_PENDING` state.
96
+ - Once `SENDING_DISABLED` or `DEPRECATED`, the deprecation cannot be reversed.
97
+
98
+ **Removing from registry:** Once a sucker reaches `DEPRECATED`, anyone can call `JBSuckerRegistry.removeDeprecatedSucker(projectId, sucker)` to remove it from the project's sucker list.
99
+
100
+ ## Emergency Hatch
101
+
102
+ The emergency hatch is a per-token escape mechanism for when a bridge becomes non-functional for specific tokens.
103
+
104
+ **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.
105
+
106
+ **Effect:** Sets `emergencyHatch = true` and `enabled = false` on each specified token's `JBRemoteToken` mapping. This:
107
+ - Prevents new `prepare()` calls for the token (the token is no longer mapped/enabled).
108
+ - Prevents `toRemote()` calls for the token (reverts with `JBSucker_TokenHasInvalidEmergencyHatchState`).
109
+ - Prevents `mapToken()` from remapping or re-enabling the token (reverts with `JBSucker_TokenHasInvalidEmergencyHatchState`).
110
+ - Enables `exitThroughEmergencyHatch()` for users whose outbox entries were never sent to the remote peer (entries with `index >= numberOfClaimsSent`).
111
+
112
+ **Who can exit:** Any user with a valid outbox merkle proof for a leaf that was never sent over the bridge. The exit returns both the project tokens and the terminal tokens that were cashed out during `prepare()`.
113
+
114
+ **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.
115
+
116
+ ## Immutable Configuration
117
+
118
+ The following values are set at deploy time and cannot be changed:
119
+
120
+ | Property | Contract | Set By | Description |
121
+ |----------|----------|--------|-------------|
122
+ | `DIRECTORY` | `JBSucker`, `JBSuckerRegistry`, all deployers | Constructor | The Juicebox directory contract. |
123
+ | `TOKENS` | `JBSucker`, all deployers | Constructor | The Juicebox token management contract. |
124
+ | `PROJECTS` | `JBSuckerRegistry` | Derived from `DIRECTORY.PROJECTS()` | The ERC-721 project ownership contract. |
125
+ | `ADD_TO_BALANCE_MODE` | `JBSucker` | Constructor | Whether balance is added on-claim or manually (`ON_CLAIM` or `MANUAL`). |
126
+ | `OPBRIDGE` | `JBOptimismSucker`, `JBBaseSucker`, `JBCeloSucker` | Constructor (from deployer callback) | The OP Standard Bridge address. |
127
+ | `OPMESSENGER` | `JBOptimismSucker`, `JBBaseSucker`, `JBCeloSucker` | Constructor (from deployer callback) | The OP Cross-Domain Messenger address. |
128
+ | `ARBINBOX` | `JBArbitrumSucker` | Constructor (from deployer callback) | The Arbitrum Inbox address. |
129
+ | `GATEWAYROUTER` | `JBArbitrumSucker` | Constructor (from deployer callback) | The Arbitrum Gateway Router address. |
130
+ | `LAYER` | `JBArbitrumSucker` | Constructor (from deployer callback) | Whether this is an L1 or L2 sucker. |
131
+ | `CCIP_ROUTER` | `JBCCIPSucker` | Constructor (from deployer callback) | The Chainlink CCIP Router address. |
132
+ | `REMOTE_CHAIN_ID` | `JBCCIPSucker` | Constructor (from deployer callback) | The remote chain's chain ID. |
133
+ | `REMOTE_CHAIN_SELECTOR` | `JBCCIPSucker` | Constructor (from deployer callback) | The CCIP chain selector for the remote chain. |
134
+ | `WRAPPED_NATIVE` | `JBCeloSucker` | Constructor (from deployer callback) | The wrapped native token (WETH) on the local chain. |
135
+ | `LAYER_SPECIFIC_CONFIGURATOR` | All deployers | Constructor | The address authorized to call one-time setup functions. |
136
+ | `peer()` | `JBSucker` | Deterministic (defaults to `address(this)` via CREATE2) | The remote peer sucker address. Assumes identical deployment on both chains. |
137
+ | `deployer` | `JBSucker` | Set once in `initialize()` by `msg.sender` | The address that deployed this sucker clone (the deployer contract). |
138
+ | `projectId()` | `JBSucker` | Set once in `initialize()` | The local project ID this sucker serves. |
139
+ | Bridge/messenger/router addresses | All deployers | `setChainSpecificConstants()` (one-time) | Bridge infrastructure addresses, set once by the configurator. |
140
+ | Singleton implementation | All deployers | `configureSingleton()` (one-time) | The singleton contract used as the clone template. |
141
+
142
+ ## Admin Boundaries
143
+
144
+ What admins **cannot** do:
145
+
146
+ - **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.
147
+
148
+ - **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.
149
+
150
+ - **Cannot reverse deprecation past `SENDING_DISABLED`.** Once the sucker enters `SENDING_DISABLED` or `DEPRECATED` state, `setDeprecation()` reverts. The deprecation cannot be cancelled.
151
+
152
+ - **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()`.
153
+
154
+ - **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.
155
+
156
+ - **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.
157
+
158
+ - **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.
159
+
160
+ - **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.
161
+
162
+ - **Cannot change the project ID.** The `projectId` is set once during `initialize()` and is immutable thereafter (enforced by OpenZeppelin `Initializable`).
@@ -0,0 +1,71 @@
1
+ # nana-suckers-v6 — Architecture
2
+
3
+ ## Purpose
4
+
5
+ Cross-chain token bridging for Juicebox V6. Allows project tokens and funds to move between EVM chains via Optimism, Arbitrum, and Chainlink CCIP bridges. Uses dual merkle trees (outbox/inbox) for claim verification.
6
+
7
+ ## Contract Map
8
+
9
+ ```
10
+ src/
11
+ ├── JBSucker.sol — Abstract base: merkle tree management, prepare/claim logic
12
+ ├── JBBaseSucker.sol — OP Stack base (Optimism, Base)
13
+ ├── JBOptimismSucker.sol — Optimism/Base bridge implementation
14
+ ├── JBArbitrumSucker.sol — Arbitrum bridge implementation
15
+ ├── JBCCIPSucker.sol — Chainlink CCIP bridge implementation
16
+ ├── JBSuckerRegistry.sol — Registry of suckers per project, deployment permissions
17
+ ├── deployers/
18
+ │ ├── JBOptimismSuckerDeployer.sol
19
+ │ ├── JBArbitrumSuckerDeployer.sol
20
+ │ └── JBCCIPSuckerDeployer.sol
21
+ ├── enums/
22
+ │ └── JBSuckerState.sol — ENABLED → DEPRECATION_PENDING → SENDING_DISABLED → DEPRECATED
23
+ ├── libraries/
24
+ │ ├── JBAddressBytes.sol — Address/bytes32 conversion
25
+ │ └── MerkleLib.sol — Merkle tree operations
26
+ └── structs/ — Token mappings, sucker pairs, claims
27
+ ```
28
+
29
+ ## Key Data Flows
30
+
31
+ ### Outbound (Prepare + Bridge)
32
+ ```
33
+ User → JBSucker.prepare()
34
+ → Cash out project tokens (0% tax via sucker privilege)
35
+ → Insert {beneficiary, token, amount} into outbox merkle tree
36
+ → When tree is full or manually triggered:
37
+ → Bridge tokens via OP/Arb/CCIP messenger
38
+ → Send merkle root to remote sucker
39
+ ```
40
+
41
+ ### Inbound (Claim)
42
+ ```
43
+ Remote root arrives → stored in inbox tree
44
+ User → JBSucker.claim(proof)
45
+ → Verify merkle proof against inbox root
46
+ → Check leaf not already claimed (bitmap)
47
+ → Mint project tokens to beneficiary (or transfer bridged tokens)
48
+ → Mark leaf as executed
49
+ ```
50
+
51
+ ### Deprecation Lifecycle
52
+ ```
53
+ ENABLED → DEPRECATION_PENDING → SENDING_DISABLED → DEPRECATED
54
+ (owner) (pending period) (no new outbox) (fully disabled)
55
+ ```
56
+
57
+ ## Extension Points
58
+
59
+ | Point | Interface | Purpose |
60
+ |-------|-----------|---------|
61
+ | Bridge transport | Chain-specific sucker | OP, Arb, CCIP implementations |
62
+ | Sucker deployer | `IJBSuckerDeployer` | Factory for new suckers |
63
+ | Registry | `IJBSuckerRegistry` | Discovery and access control |
64
+
65
+ ## Dependencies
66
+ - `@bananapus/core-v6` — Terminal, controller, token interfaces
67
+ - `@bananapus/permission-ids-v6` — DEPLOY_SUCKERS, MAP_SUCKER_TOKEN, etc.
68
+ - `@arbitrum/nitro-contracts` — Arbitrum bridge interfaces
69
+ - `@chainlink/contracts-ccip` — CCIP router interfaces
70
+ - `@openzeppelin/contracts` — MerkleProof, SafeERC20
71
+ - `solady` — LibBitmap
package/README.md CHANGED
@@ -38,6 +38,7 @@ _If you're having trouble understanding this contract, take a look at the [core
38
38
  | [`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. |
39
39
  | [`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). |
40
40
  | [`JBBaseSucker`](src/JBBaseSucker.sol) | Ethereum and Base | A thin wrapper around `JBOptimismSucker` with Base chain IDs. |
41
+ | [`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. |
41
42
  | [`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. |
42
43
 
43
44
  Suckers use two [merkle trees](https://en.wikipedia.org/wiki/Merkle_tree) to track project token claims associated with each terminal token they support:
@@ -86,12 +87,14 @@ graph TD;
86
87
  | [`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). |
87
88
  | [`JBOptimismSucker`](src/JBOptimismSucker.sol) | Extends `JBSucker`. Bridges via OP Standard Bridge + OP Messenger. No `msg.value` required for transport. |
88
89
  | [`JBBaseSucker`](src/JBBaseSucker.sol) | Thin wrapper around `JBOptimismSucker` with Base chain IDs (Ethereum 1 <-> Base 8453, Sepolia 11155111 <-> Base Sepolia 84532). |
90
+ | [`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). |
89
91
  | [`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. |
90
92
  | [`JBSuckerRegistry`](src/JBSuckerRegistry.sol) | Tracks all suckers per project. Manages deployer allowlist (owner-only). Entry point for `deploySuckersFor`. Can remove deprecated suckers via `removeDeprecatedSucker`. |
91
93
  | [`JBSuckerDeployer`](src/JBSuckerDeployer.sol) | Abstract base deployer. Clones a singleton sucker via `LibClone.cloneDeterministic` and initializes it. Two-phase setup: `setChainSpecificConstants` then `configureSingleton`. |
92
94
  | [`JBCCIPSuckerDeployer`](src/deployers/JBCCIPSuckerDeployer.sol) | Deployer for `JBCCIPSucker`. Stores CCIP router, remote chain ID, and CCIP chain selector. |
93
95
  | [`JBOptimismSuckerDeployer`](src/deployers/JBOptimismSuckerDeployer.sol) | Deployer for `JBOptimismSucker`. Stores OP Messenger and OP Bridge addresses. |
94
96
  | [`JBBaseSuckerDeployer`](src/deployers/JBBaseSuckerDeployer.sol) | Thin wrapper around `JBOptimismSuckerDeployer` for Base. |
97
+ | [`JBCeloSuckerDeployer`](src/deployers/JBCeloSuckerDeployer.sol) | Deployer for `JBCeloSucker`. Extends `JBOptimismSuckerDeployer` with `wrappedNative` (`IWrappedNativeToken`) storage for the local chain's WETH address. |
95
98
  | [`JBArbitrumSuckerDeployer`](src/deployers/JBArbitrumSuckerDeployer.sol) | Deployer for `JBArbitrumSucker`. Stores Arbitrum Inbox, Gateway Router, and layer (`JBLayer.L1` or `JBLayer.L2`). |
96
99
  | [`MerkleLib`](src/utils/MerkleLib.sol) | Incremental merkle tree (depth 32, max ~4 billion leaves, modeled on eth2 deposit contract). Used for outbox/inbox trees. Gas-optimized with inline assembly for `root()` and `branchRoot()`. |
97
100
  | [`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). |
@@ -232,7 +235,7 @@ Token mappings define which local terminal token corresponds to which remote ter
232
235
  - **`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)))`.
233
236
  - **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.
234
237
  - **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).
235
- - **Native token rules.** On `JBSucker` (OP/Arb), `NATIVE_TOKEN` can only map to `NATIVE_TOKEN` or `bytes32(0)`. `JBCCIPSucker` overrides this to allow `NATIVE_TOKEN` mapping to any remote address (for chains where ETH is an ERC-20).
238
+ - **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).
236
239
  - **`minBridgeAmount`** prevents spam by requiring a minimum outbox balance before `toRemote` can be called.
237
240
 
238
241
  ```solidity
@@ -393,6 +396,7 @@ nana-suckers-v6/
393
396
  │ ├── JBCCIPSucker.sol - Chainlink CCIP bridge implementation.
394
397
  │ ├── JBOptimismSucker.sol - OP Stack bridge implementation.
395
398
  │ ├── JBBaseSucker.sol - Base-specific wrapper around JBOptimismSucker.
399
+ │ ├── JBCeloSucker.sol - Celo-specific wrapper around JBOptimismSucker (custom gas token).
396
400
  │ ├── JBArbitrumSucker.sol - Arbitrum bridge implementation.
397
401
  │ ├── JBSuckerRegistry.sol - Registry tracking suckers per project.
398
402
  │ ├── deployers/ - Deployers for each kind of sucker.
package/RISKS.md ADDED
@@ -0,0 +1,48 @@
1
+ # nana-suckers-v6 — Risks
2
+
3
+ ## Trust Assumptions
4
+
5
+ 1. **Bridge Infrastructure** — Trusts OP Stack, Arbitrum, and CCIP bridges to deliver messages and tokens faithfully. Bridge compromise = fund loss.
6
+ 2. **Remote Peer** — Each sucker trusts its configured remote peer (the sucker on the other chain). Root messages only accepted from authenticated peer.
7
+ 3. **Project Owner** — Can deploy suckers, set token mappings, initiate deprecation, and enable emergency hatch. Full control over cross-chain configuration.
8
+ 4. **Core Protocol** — Suckers mint tokens via JBController with special permission (0% cashout tax). Relies on controller to enforce supply rules.
9
+
10
+ ## Known Risks
11
+
12
+ | Risk | Description | Mitigation |
13
+ |------|-------------|------------|
14
+ | Token mapping immutability | Once outbox tree has entries, token mapping cannot be changed (only disabled) | Verify mappings before first bridge operation |
15
+ | Emergency hatch abuse | Project owner can enable emergency hatch instantly (no timelock) to recover stuck tokens | Trust assumption on project owner |
16
+ | CCIP amount validation skip | Amount validation intentionally skipped (M-28) to prevent token lockup | Accepted risk to avoid permanent fund lock |
17
+ | Bridge liveness | If bridge goes down, tokens in transit are stuck until bridge recovers | Use deprecation lifecycle; emergency hatch for recovery |
18
+ | Surplus fragmentation | Cash-out bonding curve on each chain only sees that chain's surplus | Users must bridge to chain with more surplus for fair cash-out |
19
+
20
+ ## INTEROP-6: Cross-Chain NATIVE_TOKEN Semantic Divergence
21
+
22
+ **Severity:** Medium
23
+ **Status:** Acknowledged — by design
24
+
25
+ `JBConstants.NATIVE_TOKEN` represents different real-world assets on different chains (ETH on Ethereum/OP/Base/Arbitrum, CELO on Celo, MATIC on Polygon). When a project maps `NATIVE_TOKEN → NATIVE_TOKEN` across chains where the native token differs, the protocol treats different assets as equivalent.
26
+
27
+ **Impact:**
28
+ - Issuance mispricing — payments in non-ETH native tokens priced as ETH without a price feed
29
+ - Sucker bridging failure — incompatible token operations on non-ETH chains
30
+ - Surplus fragmentation — bonding curve only sees local chain surplus
31
+
32
+ **Safe chains:** Ethereum, Optimism, Base, Arbitrum (all ETH-native)
33
+ **Affected chains:** Celo (CELO), Polygon (MATIC), Avalanche (AVAX), BNB Chain (BNB)
34
+
35
+ **Mitigation:** On non-ETH chains, use WETH ERC20 as accounting context and map `WETH → WETH` instead of `NATIVE_TOKEN → NATIVE_TOKEN`.
36
+
37
+ ## Privileged Roles
38
+
39
+ | Role | Permission IDs | Scope |
40
+ |------|---------------|-------|
41
+ | Project owner | `DEPLOY_SUCKERS`, `MAP_SUCKER_TOKEN`, `SUCKER_SAFETY`, `SET_SUCKER_DEPRECATION` | Per-project |
42
+ | Remote peer | Sends merkle roots, triggers claims | Per-sucker-pair |
43
+ | Bridge messenger | Delivers cross-chain messages | Infrastructure |
44
+
45
+ ## Deprecation Lifecycle
46
+ - `ENABLED` → `DEPRECATION_PENDING` → `SENDING_DISABLED` → `DEPRECATED`
47
+ - Each state progressively restricts operations
48
+ - No way to re-enable once deprecated (intentional)
package/SKILLS.md CHANGED
@@ -12,12 +12,14 @@ Cross-chain token and fund bridging for Juicebox V6 projects, using merkle trees
12
12
  | `JBCCIPSucker` | CCIP bridge implementation. Implements `IAny2EVMMessageReceiver.ccipReceive`. Wraps native ETH to WETH before bridging (CCIP only transports ERC-20s), unwraps on receive. Overrides `_validateTokenMapping` to allow `NATIVE_TOKEN` mapping to ERC-20 addresses (for chains where ETH is not native). Refunds excess transport payment after `ccipSend` via low-level call (does not revert on refund failure). |
13
13
  | `JBOptimismSucker` | OP Stack bridge implementation. Uses `IOPMessenger.sendMessage` for merkle roots and `IOPStandardBridge.bridgeERC20To` for ERC-20s. No transport payment required (`msg.value` must be 0 for ERC-20 bridging). Native tokens are sent as `msg.value` on `sendMessage`. |
14
14
  | `JBBaseSucker` | Extends `JBOptimismSucker` with Base<->Ethereum chain ID mapping (1<->8453, 11155111<->84532). |
15
+ | `JBCeloSucker` | 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). |
15
16
  | `JBArbitrumSucker` | Arbitrum bridge implementation. Uses `unsafeCreateRetryableTicket` for L1->L2 (avoids address aliasing of refund address), `ArbSys.sendTxToL1` for L2->L1. Uses `IArbL1GatewayRouter.outboundTransferCustomRefund` for L1->L2 ERC-20 bridging, `IArbL2GatewayRouter.outboundTransfer` for L2->L1. Requires `msg.value` for L1->L2 transport. Verifies remote peer via Arbitrum bridge outbox on L1, via `AddressAliasHelper` on L2. |
16
17
  | `JBSuckerRegistry` | Entry point for deploying and tracking suckers. Manages deployer allowlist (owner-only). Requires `DEPLOY_SUCKERS` permission to deploy. Tracks suckers via `EnumerableMap`. Can remove deprecated suckers via `removeDeprecatedSucker` (callable by anyone). |
17
18
  | `JBSuckerDeployer` | Abstract deployer base. Uses Solady `LibClone.cloneDeterministic` to deploy suckers as minimal proxies. Two-phase setup: `setChainSpecificConstants` (bridge addresses) then `configureSingleton` (sucker implementation). Both are one-shot calls restricted to `LAYER_SPECIFIC_CONFIGURATOR`. |
18
19
  | `JBCCIPSuckerDeployer` | CCIP-specific deployer. Stores `ccipRouter` (`ICCIPRouter`), `ccipRemoteChainId` (`uint256`), `ccipRemoteChainSelector` (`uint64`). |
19
20
  | `JBOptimismSuckerDeployer` | OP-specific deployer. Stores `opMessenger` (`IOPMessenger`), `opBridge` (`IOPStandardBridge`). |
20
21
  | `JBBaseSuckerDeployer` | Thin wrapper around `JBOptimismSuckerDeployer` for separate Base artifact. |
22
+ | `JBCeloSuckerDeployer` | Extends `JBOptimismSuckerDeployer` with `wrappedNative` (`IWrappedNativeToken`) for the local chain's WETH. Extended `setChainSpecificConstants` accepts messenger, bridge, and wrapped native token. |
21
23
  | `JBArbitrumSuckerDeployer` | Arbitrum-specific deployer. Stores `arbInbox` (`IInbox`), `arbGatewayRouter` (`IArbGatewayRouter`), `arbLayer` (`JBLayer`). |
22
24
  | `MerkleLib` | Incremental merkle tree (depth 32, max 2^32 - 1 leaves). `insert` appends leaves, `root` computes current root (gas-optimized assembly), `branchRoot` verifies proofs (assembly). Modeled on eth2 deposit contract. |
23
25
 
@@ -105,7 +107,7 @@ Cross-chain token and fund bridging for Juicebox V6 projects, using merkle trees
105
107
  - `peer()` defaults to `bytes32(uint256(uint160(address(this))))` -- suckers expect to be deployed at matching addresses on both chains via deterministic deployment. Can be overridden for cross-VM peers (e.g., Solana PDA addresses).
106
108
  - `fromRemote` is `external payable` and does NOT revert on stale nonce or deprecated state -- it silently ignores the update and emits `StaleRootRejected` to avoid losing native tokens sent along with the message.
107
109
  - `JBCCIPSucker.ccipReceive` calls `this.fromRemote(root)` (external self-call) so that `_isRemotePeer` sees `msg.sender == address(this)` rather than the CCIP router.
108
- - `_validateTokenMapping` in `JBSucker` enforces that native token (`NATIVE_TOKEN`) can only map to `NATIVE_TOKEN` or `bytes32(0)`. `JBCCIPSucker` overrides this to only enforce minimum gas (allowing `NATIVE_TOKEN` to map to any remote address since the remote chain may not have native ETH).
110
+ - `_validateTokenMapping` in `JBSucker` enforces that native token (`NATIVE_TOKEN`) can only map to `NATIVE_TOKEN` or `bytes32(0)`. `JBCCIPSucker` and `JBCeloSucker` override this to only enforce minimum gas (allowing `NATIVE_TOKEN` to map to any remote address since the remote chain may not have native ETH).
109
111
  - Emergency hatch is irreversible per-token. Once opened, the token can never be bridged again by that sucker. The invariant is: if `emergencyHatch == true` then `enabled == false`.
110
112
  - `setDeprecation` requires the timestamp to be at least `_maxMessagingDelay()` (14 days) in the future. Once in `SENDING_DISABLED` or `DEPRECATED` state, deprecation cannot be modified.
111
113
  - Deployer `setChainSpecificConstants` and `configureSingleton` are both one-shot functions -- they revert if called twice.