@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/ADMINISTRATION.md +36 -5
- package/ARCHITECTURE.md +42 -103
- package/AUDIT_INSTRUCTIONS.md +116 -386
- package/CHANGELOG.md +72 -0
- package/README.md +87 -415
- package/RISKS.md +22 -5
- package/SKILLS.md +29 -250
- package/STYLE_GUIDE.md +58 -21
- package/USER_JOURNEYS.md +47 -311
- package/package.json +3 -4
- package/references/operations.md +25 -0
- package/references/runtime.md +28 -0
- package/script/Deploy.s.sol +7 -7
- package/src/JBCeloSucker.sol +4 -2
- package/src/JBSucker.sol +4 -4
- package/src/JBSuckerRegistry.sol +5 -1
- package/src/interfaces/IJBSuckerRegistry.sol +2 -0
- package/src/libraries/ARBAddresses.sol +1 -1
- package/src/libraries/ARBChains.sol +1 -1
- package/test/audit/codex-ToRemoteFeeIrrecoverable.t.sol +238 -0
- package/CHANGE_LOG.md +0 -484
package/USER_JOURNEYS.md
CHANGED
|
@@ -1,336 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# User Journeys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Who This Repo Serves
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- projects that want canonical cross-chain movement of project-token positions
|
|
6
|
+
- operators deploying and registering sucker pairs on supported bridge families
|
|
7
|
+
- users bridging a project position from one chain to another
|
|
8
|
+
- teams responsible for bridge fees, token mappings, deprecation, and emergency controls
|
|
6
9
|
|
|
7
|
-
## 1
|
|
10
|
+
## Journey 1: Launch A Cross-Chain Sucker Pair For A Project
|
|
8
11
|
|
|
9
|
-
**
|
|
12
|
+
**Starting state:** the project exists on multiple chains or plans to, and the team has chosen the bridge family it trusts.
|
|
10
13
|
|
|
11
|
-
**
|
|
14
|
+
**Success:** paired suckers are deployed, registered, and ready to transport claims between the chains they serve.
|
|
12
15
|
|
|
13
|
-
**
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `mappings` -- Array of `JBTokenMapping` structs for initial token mappings
|
|
16
|
+
**Flow**
|
|
17
|
+
1. Choose the chain-specific sucker implementation and deployer, such as Arbitrum, OP Stack, Celo, or CCIP.
|
|
18
|
+
2. Configure token mappings, bridge counterparties, and per-project registry state in `JBSuckerRegistry`.
|
|
19
|
+
3. Deploy the pair so each side knows its remote peer and expected transport assumptions.
|
|
20
|
+
4. Frontends and operators can now reason about the bridge as a known project surface instead of ad hoc per-transfer logic.
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
1. Registry enforces `DEPLOY_SUCKERS` permission from the project owner.
|
|
22
|
-
2. Computes `salt = keccak256(abi.encode(_msgSender(), salt))` (sender-specific determinism). Note: the deployer also hashes the salt again with its own `_msgSender()` via `keccak256(abi.encodePacked(_msgSender(), salt))`, so the final CREATE2 salt is double-hashed (registry + deployer).
|
|
23
|
-
3. For each configuration:
|
|
24
|
-
- Validates the deployer is in the allowlist; reverts with `JBSuckerRegistry_InvalidDeployer` if not.
|
|
25
|
-
- Calls `deployer.createForSender(projectId, salt)` which deploys a clone via CREATE2 and internally calls `initialize(projectId)` on the clone (setting `_localProjectId` and `deployer`). There is no separate `initialize()` call -- it happens inside `createForSender`.
|
|
26
|
-
- Stores the sucker address in `_suckersOf[projectId]`.
|
|
27
|
-
- Calls `sucker.mapTokens(configuration.mappings)` to set initial token mappings.
|
|
28
|
-
4. Project owner repeats on the remote chain with the **same salt and same sender address** to deploy the matching peer sucker.
|
|
22
|
+
## Journey 2: Bridge A Position From One Chain To Another
|
|
29
23
|
|
|
30
|
-
**
|
|
24
|
+
**Starting state:** a user holds project-token exposure on the source chain and wants the corresponding position on the destination chain.
|
|
31
25
|
|
|
32
|
-
**
|
|
33
|
-
- Reverts with `JBSuckerRegistry_InvalidDeployer` if any deployer is not on the allowlist
|
|
34
|
-
- The sender must be the same address on both chains for CREATE2 addresses to match (peer recognition)
|
|
35
|
-
- Project owner repeats the call on the remote chain with the **same salt and same sender** to deploy the matching peer
|
|
26
|
+
**Success:** the source position becomes a claim, the claim is relayed, and the destination position is minted after proof verification.
|
|
36
27
|
|
|
37
|
-
|
|
28
|
+
**Flow**
|
|
29
|
+
1. The user calls `prepare` on the source-chain sucker to burn or lock the relevant local position into a claimable leaf.
|
|
30
|
+
2. The source sucker appends that leaf into its Merkle outbox tree.
|
|
31
|
+
3. Someone relays the new root to the remote chain using `toRemote`.
|
|
32
|
+
4. The claimant proves inclusion against the remote inbox tree and receives the recreated project-token position there.
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
**Failure cases that matter:** wrong token mappings, transport-layer fee shortages, root ordering mistakes, and assuming the bridge is generic ERC-20 transport when it is really project-position transport.
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
## Journey 3: Map Treasury Assets And Project Tokens Correctly Across Chains
|
|
42
37
|
|
|
43
|
-
**
|
|
38
|
+
**Starting state:** the project supports multiple assets or wrappers across chains and wants users to bridge without silent economic mismatch.
|
|
44
39
|
|
|
45
|
-
**
|
|
46
|
-
- `map` -- A `JBTokenMapping` struct containing:
|
|
47
|
-
- `localToken` -- The local terminal token address (e.g., `NATIVE_TOKEN` or an ERC-20)
|
|
48
|
-
- `minGas` -- Minimum gas for bridging; must be >= `MESSENGER_ERC20_MIN_GAS_LIMIT` (200,000) for non-native tokens
|
|
49
|
-
- `remoteToken` -- The remote token address as `bytes32`. Set to `bytes32(0)` to disable bridging
|
|
40
|
+
**Success:** the remote claim recreates the intended exposure instead of a superficially similar but economically different asset.
|
|
50
41
|
|
|
51
|
-
**
|
|
52
|
-
1.
|
|
53
|
-
2.
|
|
54
|
-
3.
|
|
55
|
-
4. Immutability check: if `_remoteTokenFor[token].addr != bytes32(0)` AND the new `remoteToken` differs from the current mapping AND `remoteToken != bytes32(0)` AND `_outboxOf[token].tree.count != 0`, reverts with `JBSucker_TokenAlreadyMapped`. All four conditions must be true for the revert -- notably, the mapping is only considered immutable when both the current remote address is set and the outbox has entries.
|
|
56
|
-
5. If disabling a mapping (`remoteToken == bytes32(0)`) and the outbox has unsent entries, `_sendRoot()` is called first to flush them.
|
|
57
|
-
6. Stores the mapping: `_remoteTokenFor[token] = JBRemoteToken{enabled: true/false, emergencyHatch: false, minGas, addr: remoteToken}`. When disabling, `addr` retains the original remote address for re-enabling.
|
|
42
|
+
**Flow**
|
|
43
|
+
1. Configure remote token metadata and mapping with the sucker pair.
|
|
44
|
+
2. Make sure the destination chain can mint or settle the project-token representation the bridge expects.
|
|
45
|
+
3. Audit chain-specific native-asset handling, especially on Celo or other non-identical environments.
|
|
58
46
|
|
|
59
|
-
|
|
47
|
+
## Journey 4: Operate The Bridge Safely Over Time
|
|
60
48
|
|
|
61
|
-
**
|
|
62
|
-
- Reverts with `JBSucker_TokenAlreadyMapped` if the outbox has entries and remapping to a different remote token
|
|
63
|
-
- Reverts with `JBSucker_TokenHasInvalidEmergencyHatchState` if the emergency hatch is already enabled for the token
|
|
64
|
-
- Reverts with `JBSucker_InvalidNativeRemoteAddress` if mapping native token to a non-native, non-zero remote
|
|
65
|
-
- Reverts with `JBSucker_BelowMinGas` if `minGas < MESSENGER_ERC20_MIN_GAS_LIMIT` for non-native tokens
|
|
66
|
-
- Re-enabling a previously disabled mapping (to the same remote token) is supported
|
|
67
|
-
- CCIP and Celo suckers override `_validateTokenMapping` to allow native-to-ERC20 mapping
|
|
68
|
-
- `msg.value` is used as `transportPayment` when flushing the outbox during disable
|
|
49
|
+
**Starting state:** the bridge is live and now needs operational stewardship rather than just deployment.
|
|
69
50
|
|
|
70
|
-
**
|
|
51
|
+
**Success:** fee policy, deprecation, trusted counterparties, and emergency paths remain coherent as conditions change.
|
|
71
52
|
|
|
72
|
-
|
|
53
|
+
**Flow**
|
|
54
|
+
1. Use `JBSuckerRegistry` to manage deployer allowlists and shared operational config.
|
|
55
|
+
2. Watch fee fallback paths and transport assumptions because delivery failure is part of the intended threat model.
|
|
56
|
+
3. Use deprecation or emergency surfaces when a bridge family or remote destination should no longer be used.
|
|
73
57
|
|
|
74
|
-
##
|
|
58
|
+
## Journey 5: Recover Value Through The Emergency Hatch When Normal Delivery Breaks
|
|
75
59
|
|
|
76
|
-
**
|
|
60
|
+
**Starting state:** a claim cannot complete through the normal inbox or remote-delivery path.
|
|
77
61
|
|
|
78
|
-
**
|
|
62
|
+
**Success:** users can recover through the explicit emergency mechanism without double-spending the same claim.
|
|
79
63
|
|
|
80
|
-
**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- `token` -- The terminal token to cash out into (e.g., `NATIVE_TOKEN` or an ERC-20)
|
|
64
|
+
**Flow**
|
|
65
|
+
1. Enable or enter the emergency mode the sucker pair exposes for the affected path.
|
|
66
|
+
2. Use `exitThroughEmergencyHatch(...)` with the relevant claim data.
|
|
67
|
+
3. Treat emergency execution slots as distinct state that still must not allow the same economic position to be claimed twice.
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
1. Validates `beneficiary != bytes32(0)`; reverts with `JBSucker_ZeroBeneficiary` if zero.
|
|
88
|
-
2. Validates the project has a deployed ERC-20 token; reverts with `JBSucker_ZeroERC20Token` if not.
|
|
89
|
-
3. Validates `_remoteTokenFor[token].enabled == true`; reverts with `JBSucker_TokenNotMapped` if disabled.
|
|
90
|
-
4. Validates sucker state is `ENABLED` or `DEPRECATION_PENDING`; reverts with `JBSucker_Deprecated` otherwise.
|
|
91
|
-
5. Transfers `projectTokenCount` project tokens from caller to the sucker via `safeTransferFrom`.
|
|
92
|
-
6. `_pullBackingAssets()`: calls `terminal.cashOutTokensOf()` with `beneficiary: payable(address(this))` (the sucker itself receives the reclaimed tokens) at 0% cashOutTaxRate (set by JBOmnichainDeployer as data hook). Records the reclaimed amount and asserts the balance delta matches.
|
|
93
|
-
7. `_insertIntoTree()`: builds a leaf hash, inserts into `_outboxOf[token].tree`, and increments `_outboxOf[token].balance`.
|
|
69
|
+
## Hand-Offs
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
**Edge cases**:
|
|
98
|
-
- Reverts with `JBSucker_ZeroBeneficiary` if `beneficiary == bytes32(0)`
|
|
99
|
-
- Reverts with `JBSucker_ZeroERC20Token` if the project has no deployed ERC-20 token
|
|
100
|
-
- Reverts with `JBSucker_TokenNotMapped` if the token mapping is not enabled
|
|
101
|
-
- Reverts with `JBSucker_Deprecated` if sucker state is `SENDING_DISABLED` or `DEPRECATED`
|
|
102
|
-
- Reverts with `JBSucker_AmountExceedsUint128` if `terminalTokenAmount` or `projectTokenCount` exceeds `uint128` (SVM compatibility)
|
|
103
|
-
- The project tokens are burned via the cash out, and the backing assets are held by the sucker until `toRemote()` is called
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## 4. Bridge (toRemote)
|
|
108
|
-
|
|
109
|
-
**Entry point**: `JBSucker.toRemote(address token)` (payable)
|
|
110
|
-
|
|
111
|
-
**Who can call**: Anyone. Typically called by a relayer. Requires `msg.value >= REGISTRY.toRemoteFee()` plus any bridge-specific transport payment.
|
|
112
|
-
|
|
113
|
-
**Parameters**:
|
|
114
|
-
- `token` -- The terminal token whose outbox tree root and backing assets should be sent to the remote chain
|
|
115
|
-
|
|
116
|
-
**State changes**:
|
|
117
|
-
1. Validates emergency hatch is not enabled for the token; reverts with `JBSucker_TokenHasInvalidEmergencyHatchState`.
|
|
118
|
-
2. Validates the outbox has something to send; reverts with `JBSucker_NothingToSend` if `outbox.balance == 0 && outbox.tree.count == outbox.numberOfClaimsSent`.
|
|
119
|
-
3. Validates `msg.value >= REGISTRY.toRemoteFee()`; reverts with `JBSucker_InsufficientMsgValue` if insufficient. This check is a hard revert -- it happens before any best-effort logic.
|
|
120
|
-
4. Fee deduction: deducts `toRemoteFee` from `msg.value` to compute `transportPayment = msg.value - toRemoteFee`. Then attempts to pay the fee into the fee project (ID 1) via `terminal.pay()`. The fee payment itself is best-effort: if the fee project has no native-token terminal or `pay()` reverts (via try-catch), the fee is returned to `transportPayment` and the call proceeds. Only the bridge transport cost uses the remaining `transportPayment`.
|
|
121
|
-
5. `_sendRoot()`:
|
|
122
|
-
- Reads `outbox.tree.count` and `outbox.balance`.
|
|
123
|
-
- Clears `outbox.balance = 0`.
|
|
124
|
-
- Increments `outbox.nonce`.
|
|
125
|
-
- Computes `outbox.tree.root()`.
|
|
126
|
-
- Sets `outbox.numberOfClaimsSent = tree.count`.
|
|
127
|
-
6. `_sendRootOverAMB()` (chain-specific): bridges assets and merkle root message to the remote peer.
|
|
128
|
-
- **OP Stack**: `OPMESSENGER.sendMessage{value: amount}()` bridges ETH and encodes `JBSucker.fromRemote(messageRoot)`
|
|
129
|
-
- **Arbitrum**: Two retryable tickets -- one for ERC-20 via gateway router, one for the merkle root message via inbox
|
|
130
|
-
- **CCIP**: Wraps native ETH to WETH, calls `CCIP_ROUTER.ccipSend()` with token amounts and message data
|
|
131
|
-
|
|
132
|
-
**Events**: `RootToRemote(root, token, index, nonce, caller)`
|
|
133
|
-
|
|
134
|
-
**Edge cases**:
|
|
135
|
-
- Reverts with `JBSucker_TokenHasInvalidEmergencyHatchState` if the emergency hatch is enabled for this token
|
|
136
|
-
- Reverts with `JBSucker_NothingToSend` if `outbox.balance == 0 && outbox.tree.count == outbox.numberOfClaimsSent`
|
|
137
|
-
- Reverts with `JBSucker_InsufficientMsgValue` if `msg.value < REGISTRY.toRemoteFee()`
|
|
138
|
-
- Reverts with `JBSucker_Deprecated` if sucker state is `SENDING_DISABLED` or `DEPRECATED`
|
|
139
|
-
- If the outbox tree is empty (`count == 0`), `_sendRoot` returns early without sending
|
|
140
|
-
- The `toRemoteFee` is global across all suckers, set by the registry owner via `setToRemoteFee()`, capped at `MAX_TO_REMOTE_FEE` (0.001 ether)
|
|
141
|
-
- Transport payment (0 for OP bridges, non-zero for Arbitrum L1->L2 and CCIP) is the remainder after fee deduction
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## 5. Receive Root (fromRemote)
|
|
146
|
-
|
|
147
|
-
**Entry point**: `JBSucker.fromRemote(JBMessageRoot root)` (payable)
|
|
148
|
-
|
|
149
|
-
**Who can call**: Only the authenticated bridge messenger representing the remote peer. Validated via `_isRemotePeer(_msgSender())`.
|
|
150
|
-
|
|
151
|
-
**Parameters**:
|
|
152
|
-
- `root` -- A `JBMessageRoot` struct containing:
|
|
153
|
-
- `version` -- Message format version (must equal `MESSAGE_VERSION`, currently 1)
|
|
154
|
-
- `token` -- The remote token address as `bytes32` (converted to local address for inbox lookup)
|
|
155
|
-
- `amount` -- The amount of terminal tokens being delivered
|
|
156
|
-
- `remoteRoot` -- A `JBInboxTreeRoot` with `nonce` and `root` (the merkle root)
|
|
157
|
-
|
|
158
|
-
**State changes**:
|
|
159
|
-
1. If `root.remoteRoot.nonce > inbox.nonce` AND state is not `DEPRECATED`:
|
|
160
|
-
1. `_inboxOf[localToken].nonce = root.remoteRoot.nonce` -- Updates the inbox nonce
|
|
161
|
-
2. `_inboxOf[localToken].root = root.remoteRoot.root` -- Updates the inbox merkle root
|
|
162
|
-
|
|
163
|
-
**Events**:
|
|
164
|
-
- On success: `NewInboxTreeRoot(token, nonce, root, caller)`
|
|
165
|
-
- On rejection: `StaleRootRejected(token, receivedNonce, currentNonce)` -- emitted if the nonce is not newer OR if the sucker is `DEPRECATED` (even with a valid newer nonce, deprecated suckers reject new roots to prevent double-spend with emergency hatch withdrawals)
|
|
166
|
-
|
|
167
|
-
**Edge cases**:
|
|
168
|
-
- Reverts with `JBSucker_NotPeer` if the sender is not the authenticated remote peer
|
|
169
|
-
- Reverts with `JBSucker_InvalidMessageVersion` if `root.version != MESSAGE_VERSION`
|
|
170
|
-
- Does NOT revert for stale nonces -- emits `StaleRootRejected` instead (because reverting could lose native tokens delivered with the message)
|
|
171
|
-
- Accepts roots for unmapped tokens (rejecting would permanently lose bridged tokens; future mapping enables claims)
|
|
172
|
-
- Nonce gaps are expected -- some bridges (e.g., CCIP) do not guarantee in-order delivery
|
|
173
|
-
- Deprecated suckers reject new roots to prevent double-spend (project owner may have enabled emergency hatch for local withdrawals)
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## 6. Claim (Bridge In)
|
|
178
|
-
|
|
179
|
-
**Entry point**: `JBSucker.claim(JBClaim claimData)` or `JBSucker.claim(JBClaim[] claims)` for batch
|
|
180
|
-
|
|
181
|
-
**Who can call**: Anyone. The beneficiary receives the tokens regardless of who calls.
|
|
182
|
-
|
|
183
|
-
**Parameters**:
|
|
184
|
-
- `claimData` -- A `JBClaim` struct containing:
|
|
185
|
-
- `token` -- The local terminal token address
|
|
186
|
-
- `leaf` -- A `JBLeaf` struct with `index`, `beneficiary` (bytes32), `projectTokenCount`, `terminalTokenAmount`
|
|
187
|
-
- `proof` -- A `bytes32[32]` merkle proof (32 = `_TREE_DEPTH`)
|
|
188
|
-
|
|
189
|
-
**State changes**:
|
|
190
|
-
1. `_validate()`:
|
|
191
|
-
1. `_executedFor[token].get(index)` -- Checks leaf not already claimed
|
|
192
|
-
2. `_executedFor[token].set(index)` -- Marks leaf as claimed
|
|
193
|
-
3. Computes `MerkleLib.branchRoot(leafHash, proof, index)` and compares to `_inboxOf[token].root`
|
|
194
|
-
2. `_handleClaim()`:
|
|
195
|
-
1. If `terminalTokenAmount > 0`: calls `terminal.addToBalanceOf{value: amount}(projectId, token, amount, false, "", "")` -- Adds backing assets to the project's terminal balance
|
|
196
|
-
2. `controller.mintTokensOf(projectId, projectTokenCount, beneficiary, "", false)` -- Mints project tokens for the beneficiary with `useReservedPercent = false` (suckers bypass reserved percent)
|
|
197
|
-
|
|
198
|
-
**Events**: `Claimed(beneficiary, token, projectTokenCount, terminalTokenAmount, index, caller)`
|
|
199
|
-
|
|
200
|
-
**Edge cases**:
|
|
201
|
-
- Reverts with `JBSucker_LeafAlreadyExecuted` if the leaf at `index` was already claimed
|
|
202
|
-
- Reverts with `JBSucker_InvalidProof` if the merkle proof does not match the inbox root
|
|
203
|
-
- Reverts if the project's controller is misconfigured or the project doesn't exist on the destination chain (permanently blocks claims -- a deployment concern)
|
|
204
|
-
- Off-chain: a relayer must compute the merkle proof from `InsertToOutboxTree` events on the source chain
|
|
205
|
-
- If nonces arrive out of order, users need regenerated proofs against the current root (the merkle tree is append-only, so all leaves remain provable)
|
|
206
|
-
- Batch `claim(JBClaim[])` simply loops over each claim
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## 7. Deprecate Sucker
|
|
211
|
-
|
|
212
|
-
**Entry point**: `JBSucker.setDeprecation(uint40 timestamp)`
|
|
213
|
-
|
|
214
|
-
**Who can call**: The project owner, or any address with the project owner's `SET_SUCKER_DEPRECATION` permission.
|
|
215
|
-
|
|
216
|
-
**Parameters**:
|
|
217
|
-
- `timestamp` -- The time after which the sucker is deprecated. Must be `0` (cancel deprecation) or `>= block.timestamp + _maxMessagingDelay()` (14 days minimum). Type is `uint40`.
|
|
218
|
-
|
|
219
|
-
**State changes**:
|
|
220
|
-
1. `deprecatedAfter = timestamp` -- Sets or clears the deprecation timestamp
|
|
221
|
-
|
|
222
|
-
**Events**: `DeprecationTimeUpdated(timestamp, caller)`
|
|
223
|
-
|
|
224
|
-
**State progression over time**:
|
|
225
|
-
- **Now -> timestamp - 14 days**: `DEPRECATION_PENDING`. All operations work normally. Users are warned.
|
|
226
|
-
- **timestamp - 14 days -> timestamp**: `SENDING_DISABLED`. `prepare()` and `toRemote()` revert. `fromRemote()` still accepts roots. `claim()` still works. `exitThroughEmergencyHatch()` works (even without enabling the emergency hatch per-token).
|
|
227
|
-
- **After timestamp**: `DEPRECATED`. `fromRemote()` rejects new roots. `claim()` still works for existing inbox roots. `exitThroughEmergencyHatch()` works (even without enabling the emergency hatch per-token).
|
|
228
|
-
|
|
229
|
-
**Edge cases**:
|
|
230
|
-
- Reverts with `JBSucker_Deprecated` if state is already `SENDING_DISABLED` or `DEPRECATED`
|
|
231
|
-
- Reverts with `JBSucker_DeprecationTimestampTooSoon` if `timestamp != 0` and `timestamp < block.timestamp + _maxMessagingDelay()`
|
|
232
|
-
- Cancellation: call `setDeprecation(0)` before reaching `SENDING_DISABLED` state
|
|
233
|
-
|
|
234
|
-
**Registry cleanup**: Anyone calls `JBSuckerRegistry.removeDeprecatedSucker(uint256 projectId, address sucker)` after the sucker reaches `DEPRECATED` state. This removes the sucker from `_suckersOf[projectId]` and emits `SuckerDeprecated(projectId, sucker, caller)`. Reverts with `JBSuckerRegistry_SuckerDoesNotBelongToProject` if the sucker is not registered, or `JBSuckerRegistry_SuckerIsNotDeprecated` if not yet deprecated.
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## 8. Enable Emergency Hatch
|
|
239
|
-
|
|
240
|
-
**Entry point**: `JBSucker.enableEmergencyHatchFor(address[] tokens)`
|
|
241
|
-
|
|
242
|
-
**Who can call**: The project owner, or any address with the project owner's `SUCKER_SAFETY` permission.
|
|
243
|
-
|
|
244
|
-
**Parameters**:
|
|
245
|
-
- `tokens` -- Array of terminal token addresses to enable the emergency hatch for
|
|
246
|
-
|
|
247
|
-
**State changes**:
|
|
248
|
-
1. For each token:
|
|
249
|
-
1. `_remoteTokenFor[token].enabled = false` -- Disables bridging for the token
|
|
250
|
-
2. `_remoteTokenFor[token].emergencyHatch = true` -- Enables emergency exit
|
|
251
|
-
|
|
252
|
-
**Events**: `EmergencyHatchOpened(tokens, caller)`
|
|
253
|
-
|
|
254
|
-
**Edge cases**:
|
|
255
|
-
- **Irreversible**: Once the emergency hatch is enabled for a token, it cannot be disabled
|
|
256
|
-
- No explicit revert for empty array (no-op)
|
|
257
|
-
- Does not require the sucker to be in any particular state
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
## 9. Exit Through Emergency Hatch
|
|
262
|
-
|
|
263
|
-
**Entry point**: `JBSucker.exitThroughEmergencyHatch(JBClaim claimData)`
|
|
264
|
-
|
|
265
|
-
**Who can call**: Anyone. The beneficiary receives the tokens regardless of who calls.
|
|
266
|
-
|
|
267
|
-
**Parameters**:
|
|
268
|
-
- `claimData` -- A `JBClaim` struct containing:
|
|
269
|
-
- `token` -- The local terminal token address
|
|
270
|
-
- `leaf` -- A `JBLeaf` struct with `index`, `beneficiary` (bytes32), `projectTokenCount`, `terminalTokenAmount`
|
|
271
|
-
- `proof` -- A `bytes32[32]` merkle proof against the **outbox** tree root (not inbox)
|
|
272
|
-
|
|
273
|
-
**State changes**:
|
|
274
|
-
1. `_validateForEmergencyExit()`:
|
|
275
|
-
1. Checks emergency hatch is enabled for the token (or sucker is `DEPRECATED`/`SENDING_DISABLED`)
|
|
276
|
-
2. Checks `index >= numberOfClaimsSent` or `numberOfClaimsSent == 0` (the leaf was NOT sent to the remote peer)
|
|
277
|
-
3. Uses a separate bitmap slot (`address(bytes20(keccak256(abi.encode(token))))`) to prevent collision with regular claims
|
|
278
|
-
4. Marks the leaf as executed in the emergency exit bitmap
|
|
279
|
-
5. Validates the merkle proof against `_outboxOf[token].tree.root()`
|
|
280
|
-
2. `_outboxOf[token].balance -= terminalTokenAmount` -- Decreases the outbox balance
|
|
281
|
-
3. `_handleClaim()`:
|
|
282
|
-
1. If `terminalTokenAmount > 0`: `terminal.addToBalanceOf(projectId, token, amount, ...)` -- Adds tokens back to project balance
|
|
283
|
-
2. `controller.mintTokensOf(projectId, projectTokenCount, beneficiary, "", false)` -- Mints project tokens for the beneficiary
|
|
284
|
-
|
|
285
|
-
**Events**: `EmergencyExit(beneficiary, token, terminalTokenAmount, projectTokenCount, caller)`
|
|
286
|
-
|
|
287
|
-
**Edge cases**:
|
|
288
|
-
- Reverts with `JBSucker_TokenHasInvalidEmergencyHatchState` if emergency hatch is not enabled AND sucker is not `DEPRECATED`/`SENDING_DISABLED`
|
|
289
|
-
- Reverts with `JBSucker_LeafAlreadyExecuted` if `index < numberOfClaimsSent` (leaf was already sent to remote peer) or if already emergency-exited
|
|
290
|
-
- Reverts with `JBSucker_InvalidProof` if the merkle proof does not match the outbox root
|
|
291
|
-
- **Important limitation**: If `_sendRoot()` was called (incrementing `numberOfClaimsSent`) but the bridge message was never delivered, leaves with `index < numberOfClaimsSent` are blocked from emergency exit even though they cannot be claimed remotely. This is a conservative failure mode (funds locked, not double-spent). The deprecation flow provides an alternative exit path.
|
|
292
|
-
- Only leaves NOT already sent to the remote peer can be emergency-exited
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## 10. Register Sucker Deployer
|
|
297
|
-
|
|
298
|
-
**Entry point**: `JBSuckerRegistry.allowSuckerDeployer(address deployer)` or `JBSuckerRegistry.allowSuckerDeployers(address[] deployers)` for batch
|
|
299
|
-
|
|
300
|
-
**Who can call**: Only the registry `owner` (Ownable). Initially JuiceboxDAO / project #1.
|
|
301
|
-
|
|
302
|
-
**Parameters**:
|
|
303
|
-
- `deployer` -- The address of the sucker deployer contract to add to the allowlist
|
|
304
|
-
- `deployers` (batch variant) -- Array of deployer addresses
|
|
305
|
-
|
|
306
|
-
**State changes**:
|
|
307
|
-
1. `suckerDeployerIsAllowed[deployer] = true` -- Adds the deployer to the allowlist
|
|
308
|
-
|
|
309
|
-
**Events**: `SuckerDeployerAllowed(deployer, caller)` -- One per deployer
|
|
310
|
-
|
|
311
|
-
**Edge cases**:
|
|
312
|
-
- Reverts with `OwnableUnauthorizedAccount` if caller is not the owner
|
|
313
|
-
- Idempotent: calling again on an already-allowed deployer is a no-op (re-sets to `true`)
|
|
314
|
-
|
|
315
|
-
**Removing a deployer**: `JBSuckerRegistry.removeSuckerDeployer(address deployer)` -- Sets `suckerDeployerIsAllowed[deployer] = false`. Emits `SuckerDeployerRemoved(deployer, caller)`. Existing suckers deployed by this deployer continue operating.
|
|
316
|
-
|
|
317
|
-
---
|
|
318
|
-
|
|
319
|
-
## 11. Set toRemote Fee
|
|
320
|
-
|
|
321
|
-
**Entry point**: `JBSuckerRegistry.setToRemoteFee(uint256 fee)`
|
|
322
|
-
|
|
323
|
-
**Who can call**: Only the registry `owner` (Ownable).
|
|
324
|
-
|
|
325
|
-
**Parameters**:
|
|
326
|
-
- `fee` -- The new ETH fee in wei, paid into the fee project on each `toRemote()` call
|
|
327
|
-
|
|
328
|
-
**State changes**:
|
|
329
|
-
1. `toRemoteFee = fee` -- Updates the global fee
|
|
330
|
-
|
|
331
|
-
**Events**: `ToRemoteFeeChanged(oldFee, fee, caller)`
|
|
332
|
-
|
|
333
|
-
**Edge cases**:
|
|
334
|
-
- Reverts with `JBSuckerRegistry_FeeExceedsMax` if `fee > MAX_TO_REMOTE_FEE` (0.001 ether)
|
|
335
|
-
- The fee is initialized to `MAX_TO_REMOTE_FEE` in the constructor
|
|
336
|
-
- Setting to 0 effectively disables the fee
|
|
71
|
+
- Use [nana-omnichain-deployers-v6](../nana-omnichain-deployers-v6/USER_JOURNEYS.md) when a project wants suckers packaged into its launch flow instead of deployed separately.
|
|
72
|
+
- Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) or [revnet-core-v6](../revnet-core-v6/USER_JOURNEYS.md) for the treasury and runtime project behavior that suckers transport across chains.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,9 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@arbitrum/nitro-contracts": "^1.2.1",
|
|
22
|
-
"@bananapus/
|
|
23
|
-
"@bananapus/
|
|
24
|
-
"@bananapus/permission-ids-v6": "^0.0.14",
|
|
22
|
+
"@bananapus/core-v6": "^0.0.31",
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.15",
|
|
25
24
|
"@chainlink/contracts-ccip": "^1.5.0",
|
|
26
25
|
"@chainlink/local": "github:smartcontractkit/chainlink-local",
|
|
27
26
|
"@openzeppelin/contracts": "^5.6.1",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Suckers Operations
|
|
2
|
+
|
|
3
|
+
## Configuration Surface
|
|
4
|
+
|
|
5
|
+
- [`src/JBSuckerRegistry.sol`](../src/JBSuckerRegistry.sol) is the first stop for deployer allowlists, shared fees, project inventory, and deprecation helpers.
|
|
6
|
+
- Transport-specific deployers in [`src/deployers/`](../src/deployers/) are where chain-specific constants and bridge addresses live.
|
|
7
|
+
- [`script/`](../script/) is where deployment-time environment wiring belongs.
|
|
8
|
+
|
|
9
|
+
## Change Checklist
|
|
10
|
+
|
|
11
|
+
- If you edit base sucker accounting, verify claim flow across at least one chain-specific implementation.
|
|
12
|
+
- If you edit token mapping logic, re-check the registry and deployer assumptions that feed it.
|
|
13
|
+
- If you edit deprecation or emergency paths, verify the intended operator workflow still works end to end.
|
|
14
|
+
- If you touch bridge-specific code, confirm whether the real bug is transport-side or shared accounting-side.
|
|
15
|
+
|
|
16
|
+
## Common Failure Modes
|
|
17
|
+
|
|
18
|
+
- Cross-chain issue is blamed on transport when the root or token mapping was wrong before message delivery.
|
|
19
|
+
- Registry configuration drifts from what a deployer or external operator expects.
|
|
20
|
+
- Emergency hatches or deprecation paths are stale because nobody exercises them until stress conditions arrive.
|
|
21
|
+
|
|
22
|
+
## Useful Proof Points
|
|
23
|
+
|
|
24
|
+
- [`test/audit/`](../test/audit/) for security-sensitive assumptions.
|
|
25
|
+
- [`script/helpers/`](../script/helpers/) when the problem is deployment wiring rather than runtime logic.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Suckers Runtime
|
|
2
|
+
|
|
3
|
+
## Core Roles
|
|
4
|
+
|
|
5
|
+
- [`src/JBSucker.sol`](../src/JBSucker.sol) owns the shared prepare, relay, claim, token-mapping, and lifecycle logic.
|
|
6
|
+
- [`src/JBSuckerRegistry.sol`](../src/JBSuckerRegistry.sol) owns project-to-sucker inventory, deployer allowlists, and shared remote-fee settings.
|
|
7
|
+
- Chain-specific sucker contracts under [`src/`](../src/) own the transport-specific message delivery and verification path.
|
|
8
|
+
- Matching deployers under [`src/deployers/`](../src/deployers/) own clone and transport configuration.
|
|
9
|
+
|
|
10
|
+
## Runtime Path
|
|
11
|
+
|
|
12
|
+
1. Local state is prepared into a claimable Merkle leaf.
|
|
13
|
+
2. A root is relayed to the peer chain through the bridge-specific transport.
|
|
14
|
+
3. The remote side records the root in its inbox state.
|
|
15
|
+
4. Claimants prove inclusion and recreate their position on the destination chain.
|
|
16
|
+
|
|
17
|
+
## High-Risk Areas
|
|
18
|
+
|
|
19
|
+
- Token mapping: mapping mistakes break economic equivalence, not just UX.
|
|
20
|
+
- Root ordering and replay protection: message sequencing is part of correctness.
|
|
21
|
+
- Emergency and deprecation paths: these are operational safety surfaces that must remain reliable.
|
|
22
|
+
- Shared accounting vs transport logic: many incidents stem from confusing these layers.
|
|
23
|
+
|
|
24
|
+
## Tests To Trust First
|
|
25
|
+
|
|
26
|
+
- [`test/fork/`](../test/fork/) for real transport assumptions.
|
|
27
|
+
- [`test/regression/`](../test/regression/) for pinned cross-chain edge cases.
|
|
28
|
+
- [`test/`](../test/) broadly when the bug could involve base logic, registry behavior, or a specific bridge implementation.
|
package/script/Deploy.s.sol
CHANGED
|
@@ -5,8 +5,7 @@ import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol";
|
|
|
5
5
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
6
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
7
7
|
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
8
|
-
|
|
9
|
-
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
8
|
+
import {CoreDeployment, CoreDeploymentLib} from "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
10
9
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
11
10
|
import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
|
|
12
11
|
import {Script} from "forge-std/Script.sol";
|
|
@@ -132,12 +131,13 @@ contract DeployScript is Script, Sphinx {
|
|
|
132
131
|
_arbitrumSucker();
|
|
133
132
|
_ccipSucker();
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
// Synchronize any deployers discovered or resumed during this run into the registry as long as the
|
|
135
|
+
// current safe still controls it. This keeps partial-deployment recovery idempotent.
|
|
136
|
+
if (PRE_APPROVED_DEPLOYERS.length != 0 && Ownable(address(REGISTRY)).owner() == safeAddress()) {
|
|
137
|
+
REGISTRY.allowSuckerDeployers(PRE_APPROVED_DEPLOYERS);
|
|
138
|
+
}
|
|
140
139
|
|
|
140
|
+
if (!registryAlreadyDeployed) {
|
|
141
141
|
// Check what safe this is, if this is the same one as the fee-project owner, then we do not need to
|
|
142
142
|
// transfer. If its not then we transfer to the fee-project safe.
|
|
143
143
|
// NOTE: If this is ran after the configuration of the fee-project, this would transfer it to the
|
package/src/JBCeloSucker.sol
CHANGED
|
@@ -87,9 +87,11 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
87
87
|
uint256 _projectId = projectId();
|
|
88
88
|
|
|
89
89
|
// Unwrap WETH → native ETH.
|
|
90
|
+
// slither-disable-next-line calls-loop
|
|
90
91
|
WRAPPED_NATIVE.withdraw(amount);
|
|
91
92
|
|
|
92
93
|
// Get the project's primary terminal for native token.
|
|
94
|
+
// slither-disable-next-line calls-loop
|
|
93
95
|
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId: _projectId, token: JBConstants.NATIVE_TOKEN});
|
|
94
96
|
|
|
95
97
|
if (address(terminal) == address(0)) {
|
|
@@ -97,7 +99,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
// Add native ETH to the project's balance.
|
|
100
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
102
|
+
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
101
103
|
terminal.addToBalanceOf{value: amount}({
|
|
102
104
|
projectId: _projectId,
|
|
103
105
|
token: JBConstants.NATIVE_TOKEN,
|
|
@@ -140,7 +142,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
140
142
|
if (amount != 0) {
|
|
141
143
|
if (token == JBConstants.NATIVE_TOKEN) {
|
|
142
144
|
// Wrap native ETH → WETH so it can be bridged as ERC-20.
|
|
143
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
145
|
+
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
144
146
|
WRAPPED_NATIVE.deposit{value: amount}();
|
|
145
147
|
|
|
146
148
|
// Approve the bridge to spend the WETH.
|
package/src/JBSucker.sol
CHANGED
|
@@ -712,7 +712,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
712
712
|
// transportPayment.
|
|
713
713
|
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId: FEE_PROJECT_ID, token: JBConstants.NATIVE_TOKEN});
|
|
714
714
|
if (address(terminal) != address(0)) {
|
|
715
|
-
// slither-disable-next-line unused-return
|
|
715
|
+
// slither-disable-next-line unused-return,reentrancy-events
|
|
716
716
|
try terminal.pay{value: _toRemoteFee}({
|
|
717
717
|
projectId: FEE_PROJECT_ID,
|
|
718
718
|
token: JBConstants.NATIVE_TOKEN,
|
|
@@ -726,12 +726,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
726
726
|
) {}
|
|
727
727
|
catch {
|
|
728
728
|
// Fee payment failed — fee ETH stays in this contract, transportPayment unchanged.
|
|
729
|
-
//
|
|
730
|
-
//
|
|
729
|
+
// There is no dedicated sweep path for this retained ETH. This is an accepted tradeoff
|
|
730
|
+
// to avoid DoS on zero-cost bridges that revert on non-zero transport payment.
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
// If no terminal exists, fee ETH stays in this contract. transportPayment is already correct.
|
|
734
|
-
//
|
|
734
|
+
// This retained ETH is an accepted stuck-funds edge case; later `claim` calls do not sweep it.
|
|
735
735
|
|
|
736
736
|
// Send the merkle root to the remote chain.
|
|
737
737
|
_sendRoot({transportPayment: transportPayment, token: token, remoteToken: remoteToken});
|
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -187,6 +187,8 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
187
187
|
|
|
188
188
|
/// @notice Deploy one or more suckers for the specified project.
|
|
189
189
|
/// @dev The caller must be the project's owner or have `JBPermissionIds.DEPLOY_SUCKERS` from the project's owner.
|
|
190
|
+
/// Each newly created sucker is immediately configured by calling `mapTokens`, so successful execution also
|
|
191
|
+
/// depends on this registry being authorized to perform `MAP_SUCKER_TOKEN` for the project.
|
|
190
192
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
191
193
|
/// @param salt The salt used to deploy the contract. For the suckers to be peers, this must be the same value on
|
|
192
194
|
/// each chain where suckers are deployed.
|
|
@@ -209,7 +211,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
209
211
|
suckers = new address[](configurations.length);
|
|
210
212
|
|
|
211
213
|
// Calculate the salt using the sender's address and the provided `salt`.
|
|
212
|
-
// This
|
|
214
|
+
// This is an intentional part of the same-address peer invariant: if projects deploy suckers from
|
|
215
|
+
// different sender addresses on different chains, the resulting sucker addresses will differ and the
|
|
216
|
+
// default peer symmetry assumption will not hold.
|
|
213
217
|
salt = keccak256(abi.encode(_msgSender(), salt));
|
|
214
218
|
|
|
215
219
|
// Iterate through the configurations and deploy the suckers.
|
|
@@ -91,6 +91,8 @@ interface IJBSuckerRegistry {
|
|
|
91
91
|
function allowSuckerDeployers(address[] calldata deployers) external;
|
|
92
92
|
|
|
93
93
|
/// @notice Deploy one or more suckers for the specified project.
|
|
94
|
+
/// @dev This call also applies each configuration's token mappings on the deployed suckers. Projects using this
|
|
95
|
+
/// path need the registry to be able to satisfy the corresponding `MAP_SUCKER_TOKEN` checks.
|
|
94
96
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
95
97
|
/// @param salt The salt used for deterministic deployment.
|
|
96
98
|
/// @param configurations The deployer configs to use.
|