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