@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/SKILLS.md
CHANGED
|
@@ -1,263 +1,42 @@
|
|
|
1
1
|
# Juicebox Suckers
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
Cross-chain token and fund bridging for Juicebox V6 projects, using merkle trees to batch claims and chain-specific bridges (Chainlink CCIP, OP Stack, Arbitrum) to move assets. Suckers are deployed in pairs on two chains -- each one cashes out project tokens locally, batches claims into a merkle tree, bridges the root and funds to its peer, and lets beneficiaries claim minted project tokens on the remote chain.
|
|
6
|
-
|
|
7
|
-
## Contracts
|
|
8
|
-
|
|
9
|
-
| Contract | Role |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `JBSucker` | Abstract base with full lifecycle: prepare, toRemote, fromRemote, claim, emergency hatch, deprecation. Manages outbox/inbox merkle trees per terminal token. Uses `ERC2771Context` for meta-transactions. Deployed as minimal clones via `Initializable`. Has immutable `FEE_PROJECT_ID` (typically project ID 1) and immutable `REGISTRY` (`IJBSuckerRegistry`). Reads the global `toRemoteFee` from the registry via `REGISTRY.toRemoteFee()` -- fee administration is centralized in `JBSuckerRegistry`, not per-clone. |
|
|
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
|
-
| `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
|
-
| `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). |
|
|
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. |
|
|
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). Owns the global `toRemoteFee` (ETH fee in wei, capped at `MAX_TO_REMOTE_FEE` = 0.001 ether), adjustable by the registry owner via `setToRemoteFee()`. Initialized to `MAX_TO_REMOTE_FEE` at construction. All suckers read the fee from the registry -- one call changes the fee everywhere. |
|
|
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`. |
|
|
19
|
-
| `JBCCIPSuckerDeployer` | CCIP-specific deployer. Stores `ccipRouter` (`ICCIPRouter`), `ccipRemoteChainId` (`uint256`), `ccipRemoteChainSelector` (`uint64`). |
|
|
20
|
-
| `JBOptimismSuckerDeployer` | OP-specific deployer. Stores `opMessenger` (`IOPMessenger`), `opBridge` (`IOPStandardBridge`). |
|
|
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. |
|
|
23
|
-
| `JBArbitrumSuckerDeployer` | Arbitrum-specific deployer. Stores `arbInbox` (`IInbox`), `arbGatewayRouter` (`IArbGatewayRouter`), `arbLayer` (`JBLayer`). |
|
|
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. |
|
|
25
|
-
|
|
26
|
-
## Key Functions
|
|
27
|
-
|
|
28
|
-
| Function | Contract | What it does |
|
|
29
|
-
|----------|----------|--------------|
|
|
30
|
-
| `prepare(projectTokenCount, beneficiary, minTokensReclaimed, token)` | `JBSucker` | Transfers project tokens (ERC-20) from caller via `safeTransferFrom`, cashes them out at the project's primary terminal for the specified terminal token, inserts a leaf into the outbox merkle tree. `beneficiary` is `bytes32` for cross-VM compatibility. Amounts are capped at `uint128` for SVM compatibility. Reverts if token not mapped, sucker deprecated/sending-disabled, beneficiary is zero, or project has no ERC-20 token. |
|
|
31
|
-
| `toRemote(token)` | `JBSucker` | Sends the outbox merkle root and accumulated funds for `token` to the peer sucker via the bridge. Deducts `toRemoteFee` from `msg.value` (paid into fee project), passes remainder as transport payment. Increments outbox nonce and updates `numberOfClaimsSent`. Reverts if outbox is empty or emergency hatch is open. |
|
|
32
|
-
| `setToRemoteFee(fee)` | `JBSuckerRegistry` | Sets the global `toRemoteFee` for all suckers. Restricted to the registry owner via OpenZeppelin `Ownable` (`onlyOwner`). The fee must be <= `MAX_TO_REMOTE_FEE` (0.001 ether). Emits `ToRemoteFeeChanged`. |
|
|
33
|
-
| `fromRemote(root)` | `JBSucker` | Receives a merkle root from the remote peer. Validates `MESSAGE_VERSION` (reverts on mismatch). Updates inbox tree only if received nonce > current inbox nonce. Accepts roots in all states including `DEPRECATED` to prevent stranding already-sent tokens. Does NOT revert on stale nonce -- emits `StaleRootRejected` instead (to avoid losing native tokens sent with the message). |
|
|
34
|
-
| `claim(claimData)` | `JBSucker` | Verifies a merkle proof against the inbox tree, marks the leaf as executed (prevents double-spend), mints project tokens for the beneficiary via `IJBController.mintTokensOf` (with `useReservedPercent: false`), and adds terminal tokens to the project's balance. |
|
|
35
|
-
| `claim(claims[])` | `JBSucker` | Batch version -- iterates and calls `claim(JBClaim)` for each. |
|
|
36
|
-
| `mapToken(map)` | `JBSucker` | Maps a local terminal token to a remote token. Requires `MAP_SUCKER_TOKEN` permission. Setting `remoteToken` to `bytes32(0)` disables bridging and sends a final root to flush remaining outbox. Cannot remap to a different remote token once outbox has entries (prevents double-spend). Can re-enable a previously disabled token to the same remote address. Reverts if emergency hatch is active for the token. |
|
|
37
|
-
| `mapTokens(maps[])` | `JBSucker` | Batch version of `mapToken`. Splits `msg.value` evenly across mappings that need a final root flush (disable with pending outbox entries). Refunds any remainder (dust) from integer division back to the caller on a best-effort basis (L-47). |
|
|
38
|
-
| `enableEmergencyHatchFor(tokens)` | `JBSucker` | Opens emergency hatch for specified tokens (irreversible). Sets `emergencyHatch = true` and `enabled = false` on each token's `JBRemoteToken`. Requires `SUCKER_SAFETY` permission from the project owner. |
|
|
39
|
-
| `exitThroughEmergencyHatch(claimData)` | `JBSucker` | Lets users reclaim tokens on the chain they deposited, using their **outbox** proof (not inbox). Only works when emergency hatch is open for the token OR sucker is `SENDING_DISABLED`/`DEPRECATED`. Only allows exit for leaves with index >= `numberOfClaimsSent` (leaves not yet sent to remote). Decreases `outbox.balance`. Uses a separate execution bitmap slot (derived from `keccak256(abi.encode(terminalToken))`) to avoid collision with inbox claim tracking. |
|
|
40
|
-
| `setDeprecation(timestamp)` | `JBSucker` | Sets when the sucker becomes fully deprecated. Must be at least `_maxMessagingDelay()` (14 days) in the future. Set to `0` to cancel pending deprecation. Reverts if already in `SENDING_DISABLED` or `DEPRECATED` state. Requires `SET_SUCKER_DEPRECATION` permission from the project owner. |
|
|
41
|
-
| `ccipReceive(any2EvmMessage)` | `JBCCIPSucker` | CCIP entry point. Validates `msg.sender == CCIP_ROUTER`, decodes sender and verifies it matches `peer()` and `REMOTE_CHAIN_SELECTOR`. Unwraps WETH to native ETH if `root.token == NATIVE_TOKEN`. Calls `this.fromRemote(root)` (external self-call so `_isRemotePeer` sees `msg.sender == address(this)`). |
|
|
42
|
-
| `deploySuckersFor(projectId, salt, configs)` | `JBSuckerRegistry` | Deploys one or more suckers for a project. Requires `DEPLOY_SUCKERS` permission. Salt is hashed with `msg.sender`: `keccak256(abi.encode(msg.sender, salt))`. For each config, clones via the deployer, maps tokens, and tracks the sucker. |
|
|
43
|
-
| `allowSuckerDeployer(deployer)` | `JBSuckerRegistry` | Adds a deployer to the allowlist. Owner-only (`onlyOwner`). |
|
|
44
|
-
| `allowSuckerDeployers(deployers[])` | `JBSuckerRegistry` | Batch version. Owner-only. |
|
|
45
|
-
| `removeDeprecatedSucker(projectId, sucker)` | `JBSuckerRegistry` | Removes a deprecated sucker from the registry. Callable by anyone. Reverts if sucker state is not `DEPRECATED`. |
|
|
46
|
-
| `removeSuckerDeployer(deployer)` | `JBSuckerRegistry` | Removes a deployer from the allowlist. Owner-only. |
|
|
47
|
-
| `createForSender(localProjectId, salt)` | `JBSuckerDeployer` | Clones the singleton sucker deterministically (salt = `keccak256(abi.encodePacked(msg.sender, salt))`) and initializes it with the project ID. |
|
|
48
|
-
| `configureSingleton(singleton)` | `JBSuckerDeployer` | One-time configuration of the sucker implementation to clone. Must be called by `LAYER_SPECIFIC_CONFIGURATOR` after `setChainSpecificConstants`. |
|
|
49
|
-
| `setChainSpecificConstants(...)` | Deployers | One-time configuration of bridge-specific addresses. Callable only by `LAYER_SPECIFIC_CONFIGURATOR`. Varies by deployer type. |
|
|
50
|
-
|
|
51
|
-
## Integration Points
|
|
52
|
-
|
|
53
|
-
| Dependency | Import | Used For |
|
|
54
|
-
|------------|--------|----------|
|
|
55
|
-
| `@bananapus/core-v6` | `IJBDirectory`, `IJBController`, `IJBTokens`, `IJBTerminal`, `IJBCashOutTerminal` | Project lookup (`controllerOf`, `primaryTerminalOf`), token minting (`mintTokensOf`), cash-outs (`cashOutTokensOf`), `addToBalanceOf` |
|
|
56
|
-
| `@bananapus/core-v6` | `JBConstants` | `NATIVE_TOKEN` sentinel address (`0x000...EEEe`) |
|
|
57
|
-
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | `MAP_SUCKER_TOKEN`, `DEPLOY_SUCKERS`, `SUCKER_SAFETY`, `SET_SUCKER_DEPRECATION`, `MINT_TOKENS` |
|
|
58
|
-
| `@chainlink/contracts-ccip` | `Client`, `IAny2EVMMessageReceiver` | CCIP message encoding/decoding (`EVM2AnyMessage`, `Any2EVMMessage`), receiver interface |
|
|
59
|
-
| `@arbitrum/nitro-contracts` | `IInbox`, `IOutbox`, `IBridge`, `ArbSys`, `AddressAliasHelper` | Arbitrum retryable tickets, L2->L1 messages, L1/L2 address aliasing verification |
|
|
60
|
-
| `@openzeppelin/contracts` | `SafeERC20`, `BitMaps`, `ERC165`, `Initializable`, `Ownable`, `ERC2771Context`, `EnumerableMap` | Token safety, leaf execution tracking, clone initialization, registry ownership and fee administration, meta-transactions, sucker enumeration. Note: `Ownable` is used by `JBSuckerRegistry` (not `JBSucker`). |
|
|
61
|
-
| `solady` | `LibClone` | Deterministic minimal proxy deployment (`cloneDeterministic`) |
|
|
62
|
-
|
|
63
|
-
## Key Types
|
|
64
|
-
|
|
65
|
-
| Struct/Enum | Fields | Used In |
|
|
66
|
-
|-------------|--------|---------|
|
|
67
|
-
| `JBClaim` | `token` (address), `leaf` (`JBLeaf`), `proof` (`bytes32[32]`) | `claim`, `exitThroughEmergencyHatch` |
|
|
68
|
-
| `JBLeaf` | `index` (uint256), `beneficiary` (bytes32), `projectTokenCount` (uint256), `terminalTokenAmount` (uint256) | Merkle tree leaves -- hash is `keccak256(abi.encode(projectTokenCount, terminalTokenAmount, beneficiary))` |
|
|
69
|
-
| `JBOutboxTree` | `nonce` (uint64), `balance` (uint256), `tree` (MerkleLib.Tree), `numberOfClaimsSent` (uint256) | Per-token outbox state in `JBSucker` |
|
|
70
|
-
| `JBInboxTreeRoot` | `nonce` (uint64), `root` (bytes32) | Per-token inbox state in `JBSucker` |
|
|
71
|
-
| `JBMessageRoot` | `version` (uint8), `token` (bytes32), `amount` (uint256), `remoteRoot` (`JBInboxTreeRoot`) | Cross-chain message payload sent via bridge |
|
|
72
|
-
| `JBRemoteToken` | `enabled` (bool), `emergencyHatch` (bool), `minGas` (uint32), `addr` (bytes32) | Token mapping config stored in `_remoteTokenFor[token]` |
|
|
73
|
-
| `JBTokenMapping` | `localToken` (address), `minGas` (uint32), `remoteToken` (bytes32) | Input for `mapToken`/`mapTokens` |
|
|
74
|
-
| `JBSuckerDeployerConfig` | `deployer` (`IJBSuckerDeployer`), `mappings` (`JBTokenMapping[]`) | Input for `deploySuckersFor` |
|
|
75
|
-
| `JBSuckersPair` | `local` (address), `remote` (bytes32), `remoteChainId` (uint256) | Return type for `suckerPairsOf` |
|
|
76
|
-
| `JBSuckerState` | `ENABLED` (0), `DEPRECATION_PENDING` (1), `SENDING_DISABLED` (2), `DEPRECATED` (3) | Deprecation lifecycle states |
|
|
77
|
-
| `JBLayer` | `L1` (0), `L2` (1) | Arbitrum sucker layer identification |
|
|
78
|
-
|
|
79
|
-
## Constants
|
|
80
|
-
|
|
81
|
-
| Name | Value | Context |
|
|
82
|
-
|------|-------|---------|
|
|
83
|
-
| `FEE_PROJECT_ID` | Set at construction (typically `1`) | The project that receives `toRemoteFee` payments via `terminal.pay()`. Immutable on `JBSucker`. |
|
|
84
|
-
| `REGISTRY` | Set at construction | The `IJBSuckerRegistry` that manages the global `toRemoteFee`. Immutable on `JBSucker`. |
|
|
85
|
-
| `toRemoteFee` | Storage variable on `JBSuckerRegistry`, initialized to `MAX_TO_REMOTE_FEE` | ETH fee (in wei) paid into the fee project on each `toRemote()` call. Adjustable by the registry owner via `setToRemoteFee()`, capped at `MAX_TO_REMOTE_FEE`. Global -- applies to all suckers. |
|
|
86
|
-
| `MAX_TO_REMOTE_FEE` | `0.001 ether` | Hard cap on what `toRemoteFee` can be set to. Constant on `JBSuckerRegistry`. |
|
|
87
|
-
| `MESSENGER_BASE_GAS_LIMIT` | `300_000` | Minimum gas for cross-chain `fromRemote` call |
|
|
88
|
-
| `MESSENGER_ERC20_MIN_GAS_LIMIT` | `200_000` | Minimum gas for ERC-20 transfer on remote chain |
|
|
89
|
-
| `_TREE_DEPTH` | `32` | Merkle tree depth (max ~4B leaves) |
|
|
90
|
-
| `MESSAGE_VERSION` | `1` | Message format version for cross-chain compatibility |
|
|
91
|
-
| `_maxMessagingDelay()` | `14 days` | Minimum deprecation lead time (virtual, can be overridden) |
|
|
92
|
-
|
|
93
|
-
## Permissions
|
|
94
|
-
|
|
95
|
-
| Permission ID | Used By | Required For |
|
|
96
|
-
|---------------|---------|-------------|
|
|
97
|
-
| `DEPLOY_SUCKERS` | `JBSuckerRegistry.deploySuckersFor` | Deploying suckers for a project |
|
|
98
|
-
| `MAP_SUCKER_TOKEN` | `JBSucker.mapToken`, `JBSucker.mapTokens` | Mapping/unmapping token pairs |
|
|
99
|
-
| `SUCKER_SAFETY` | `JBSucker.enableEmergencyHatchFor` | Opening the emergency hatch for tokens |
|
|
100
|
-
| `SET_SUCKER_DEPRECATION` | `JBSucker.setDeprecation` | Setting the deprecation timestamp |
|
|
101
|
-
| `MINT_TOKENS` | Needed on the sucker address | Sucker must have this to mint project tokens on claim |
|
|
3
|
+
## Use This File For
|
|
102
4
|
|
|
103
|
-
|
|
5
|
+
- Use this file when the task involves cross-chain project-token bridging, token mapping, Merkle claim flow, bridge-specific transport logic, or sucker registry behavior.
|
|
6
|
+
- Start here, then open the base sucker, registry, chain-specific implementation, or deployer depending on which leg of the bridge path is under review.
|
|
104
7
|
|
|
105
|
-
|
|
106
|
-
|-------|----------|------|
|
|
107
|
-
| `JBSucker_AmountExceedsUint128` | `JBSucker` | `projectTokenCount` or `terminalTokenAmount` exceeds `uint128` (SVM cap) |
|
|
108
|
-
| `JBSucker_BelowMinGas` | `JBSucker` | Token mapping `minGas` is below `MESSENGER_ERC20_MIN_GAS_LIMIT` |
|
|
109
|
-
| `JBSucker_Deprecated` | `JBSucker` | `prepare` or `toRemote` called when sucker is `SENDING_DISABLED` or `DEPRECATED` |
|
|
110
|
-
| `JBSucker_DeprecationTimestampTooSoon` | `JBSucker` | `setDeprecation` timestamp is less than `_maxMessagingDelay()` in the future |
|
|
111
|
-
| `JBSucker_ExpectedMsgValue` | `JBSucker` | `toRemote` called without required `msg.value` for transport payment |
|
|
112
|
-
| `JBSucker_IndexOutOfRange` | `JBSucker` | Leaf index exceeds tree depth (`>= 2^32`) in `_validate` or `_validateForEmergencyExit` |
|
|
113
|
-
| `JBSucker_InsufficientBalance` | `JBSucker` | Emergency hatch exit amount exceeds outbox balance |
|
|
114
|
-
| `JBSucker_InsufficientMsgValue` | `JBSucker` | `msg.value` is less than the `toRemoteFee` |
|
|
115
|
-
| `JBSucker_InvalidMessageVersion` | `JBSucker` | `fromRemote` receives a message with wrong `MESSAGE_VERSION` |
|
|
116
|
-
| `JBSucker_InvalidNativeRemoteAddress` | `JBSucker` | `NATIVE_TOKEN` mapped to a non-native, non-zero remote address (base validation) |
|
|
117
|
-
| `JBSucker_InvalidProof` | `JBSucker` | Merkle proof does not match the inbox root |
|
|
118
|
-
| `JBSucker_LeafAlreadyExecuted` | `JBSucker` | `claim` or emergency exit for a leaf that was already processed |
|
|
119
|
-
| `JBSucker_NoTerminalForToken` | `JBSucker` | Project has no terminal for the specified token |
|
|
120
|
-
| `JBSucker_NotPeer` | `JBSucker` | `fromRemote` called by an address that is not the recognized peer |
|
|
121
|
-
| `JBSucker_NothingToSend` | `JBSucker` | `toRemote` called when outbox has no pending entries |
|
|
122
|
-
| `JBSucker_TokenAlreadyMapped` | `JBSucker` | Attempting to remap a token to a different remote address when outbox has entries |
|
|
123
|
-
| `JBSucker_TokenHasInvalidEmergencyHatchState` | `JBSucker` | Operation incompatible with the token's emergency hatch state |
|
|
124
|
-
| `JBSucker_TokenNotMapped` | `JBSucker` | `prepare` called for a token that has no mapping |
|
|
125
|
-
| `JBSucker_UnexpectedMsgValue` | `JBSucker` | `msg.value` sent when not expected |
|
|
126
|
-
| `JBSucker_ZeroBeneficiary` | `JBSucker` | `prepare` called with `bytes32(0)` beneficiary |
|
|
127
|
-
| `JBSucker_ZeroERC20Token` | `JBSucker` | `prepare` called but project has no ERC-20 token deployed |
|
|
128
|
-
| `JBCCIPSucker_InvalidRouter` | `JBCCIPSucker` | Zero address passed as `CCIP_ROUTER` at construction time |
|
|
129
|
-
| `JBArbitrumSucker_NotEnoughGas` | `JBArbitrumSucker` | `msg.value` insufficient for Arbitrum retryable ticket cost |
|
|
130
|
-
| `JBSuckerRegistry_FeeExceedsMax` | `JBSuckerRegistry` | `setToRemoteFee` called with fee > `MAX_TO_REMOTE_FEE` |
|
|
131
|
-
| `JBSuckerRegistry_InvalidDeployer` | `JBSuckerRegistry` | Deployer not on the allowlist |
|
|
132
|
-
| `JBSuckerRegistry_SuckerDoesNotBelongToProject` | `JBSuckerRegistry` | Sucker is not registered for the given project |
|
|
133
|
-
| `JBSuckerRegistry_SuckerIsNotDeprecated` | `JBSuckerRegistry` | `removeDeprecatedSucker` called on a non-deprecated sucker |
|
|
134
|
-
| `JBSuckerDeployer_AlreadyConfigured` | `JBSuckerDeployer` | `configureSingleton` or `setChainSpecificConstants` called a second time |
|
|
135
|
-
| `JBSuckerDeployer_DeployerIsNotConfigured` | `JBSuckerDeployer` | `createForSender` called before singleton is configured |
|
|
136
|
-
| `JBSuckerDeployer_InvalidLayerSpecificConfiguration` | `JBSuckerDeployer` | Invalid bridge addresses passed to `setChainSpecificConstants` |
|
|
137
|
-
| `JBSuckerDeployer_LayerSpecificNotConfigured` | `JBSuckerDeployer` | `configureSingleton` called before `setChainSpecificConstants` |
|
|
138
|
-
| `JBSuckerDeployer_Unauthorized` | `JBSuckerDeployer` | Caller is not `LAYER_SPECIFIC_CONFIGURATOR` |
|
|
139
|
-
| `JBSuckerDeployer_ZeroConfiguratorAddress` | `JBSuckerDeployer` | Constructor passed `address(0)` for configurator |
|
|
140
|
-
| `JBCCIPSuckerDeployer_InvalidCCIPRouter` | `JBCCIPSuckerDeployer` | Zero address passed as CCIP router |
|
|
141
|
-
| `CCIPHelper_UnsupportedChain` | `CCIPHelper` | Chain ID has no known CCIP chain selector mapping |
|
|
142
|
-
| `MerkleLib_InsertTreeIsFull` | `MerkleLib` | Tree has reached max capacity (2^32 - 1 leaves) |
|
|
8
|
+
## Read This Next
|
|
143
9
|
|
|
144
|
-
|
|
10
|
+
| If you need... | Open this next |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Repo overview and bridge model | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
|
|
13
|
+
| Shared bridge logic | [`src/JBSucker.sol`](./src/JBSucker.sol), [`src/JBSuckerRegistry.sol`](./src/JBSuckerRegistry.sol) |
|
|
14
|
+
| Chain-specific transport behavior | [`src/`](./src/), [`src/deployers/`](./src/deployers/) |
|
|
15
|
+
| Merkle and helper logic | [`src/utils/`](./src/utils/), [`src/libraries/`](./src/libraries/) |
|
|
16
|
+
| Attack, interoperability, or regression coverage | [`test/`](./test/), [`test/regression/`](./test/regression/), [`test/fork/`](./test/fork/) |
|
|
145
17
|
|
|
146
|
-
|
|
147
|
-
- **`JBLeaf.beneficiary` is `bytes32`, not `address`.** Same cross-VM reasoning. The `prepare` function takes `bytes32 beneficiary`.
|
|
148
|
-
- `using MerkleLib for MerkleLib.Tree` is NOT inherited by derived contracts -- must redeclare in test harnesses.
|
|
149
|
-
- Suckers are deployed as minimal clones (`LibClone.cloneDeterministic`). The singleton's constructor calls `_disableInitializers()` so it cannot be initialized directly. Only clones can be initialized.
|
|
150
|
-
- For suckers to be peers, the same `salt` AND the same caller address must be used on both chains when calling `deploySuckersFor`. The registry hashes `keccak256(abi.encode(msg.sender, salt))`, and the deployer hashes `keccak256(abi.encodePacked(msg.sender, salt))` again.
|
|
151
|
-
- `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).
|
|
152
|
-
- `JBCCIPSucker` and `JBCeloSucker` override `_validateTokenMapping` to only enforce minimum gas, allowing `NATIVE_TOKEN` to map to any remote address (the remote chain may not have native ETH). Base `JBSucker` restricts `NATIVE_TOKEN` to mapping only to `NATIVE_TOKEN` or `bytes32(0)`.
|
|
153
|
-
- Emergency hatch is irreversible per-token. Once opened, the token can never be bridged again by that sucker. Invariant: `emergencyHatch == true` implies `enabled == false`.
|
|
154
|
-
- `MESSENGER_BASE_GAS_LIMIT` is 300,000. `MESSENGER_ERC20_MIN_GAS_LIMIT` is 200,000. Token mappings must specify `minGas >= 200,000` for ERC-20s. `JBCCIPSucker` requires `minGas >= 200,000` for ALL tokens (including native) because CCIP wraps native to WETH.
|
|
155
|
-
- `JBArbitrumSucker` uses `unsafeCreateRetryableTicket` (not `safeCreateRetryableTicket`) to avoid L2 address aliasing of the refund address.
|
|
156
|
-
- The outbox tree tracks `numberOfClaimsSent` separately from `tree.count`. Emergency hatch exit is only available for leaves whose index >= `numberOfClaimsSent` (not yet sent to remote). A `numberOfClaimsSent` of 0 means no root has ever been sent, so all leaves can be emergency-exited.
|
|
157
|
-
- Both `projectTokenCount` and `terminalTokenAmount` are capped at `uint128` in `_insertIntoTree` for SVM (Solana) compatibility.
|
|
158
|
-
- Nonce ordering is non-sequential: `fromRemote` accepts any nonce strictly greater than the current inbox nonce. Out-of-order nonces (from CCIP) cause earlier nonces to be silently skipped, making their claims permanently unclaimable on that chain. The sender must use the emergency hatch on the source chain to recover.
|
|
159
|
-
- `toRemote` fee payment is best-effort: if the fee project has no native token terminal or `terminal.pay()` reverts, `toRemote` proceeds without collecting the fee. The caller still receives project tokens from the fee payment when it succeeds.
|
|
160
|
-
- `JBCCIPSucker` transport payment refund uses a low-level `call` that does NOT revert on failure. If the refund fails (e.g., caller is a non-payable contract), the excess ETH is permanently stuck. The `TransportPaymentRefundFailed` event provides observability.
|
|
161
|
-
- The sucker has an unrestricted `receive()` function -- it must accept ETH from bridges, WETH unwrapping, and terminal cash-outs. Excess ETH increases `amountToAddToBalanceOf` for the project (not a double-spend risk).
|
|
162
|
-
- `fromRemote()` validates the peer using `msg.sender`, not `_msgSender()`. Using `_msgSender()` would allow a trusted ERC-2771 forwarder to spoof the bridge peer address. Never use a meta-tx forwarder as a relay for `fromRemote` calls.
|
|
163
|
-
- `ccipReceive()` validates the CCIP router using `msg.sender`, not `_msgSender()`, for the same reason. A trusted forwarder could append the router address via the ERC-2771 calldata suffix and fully control the `Any2EVMMessage` struct.
|
|
18
|
+
## Repo Map
|
|
164
19
|
|
|
165
|
-
|
|
20
|
+
| Area | Where to look |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Base contracts | [`src/JBSucker.sol`](./src/JBSucker.sol), [`src/JBSuckerRegistry.sol`](./src/JBSuckerRegistry.sol) |
|
|
23
|
+
| Chain-specific implementations and deployers | [`src/`](./src/), [`src/deployers/`](./src/deployers/) |
|
|
24
|
+
| Libraries, utils, and types | [`src/libraries/`](./src/libraries/), [`src/utils/`](./src/utils/), [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/), [`src/enums/`](./src/enums/) |
|
|
25
|
+
| Scripts | [`script/`](./script/) |
|
|
26
|
+
| Tests | [`test/`](./test/) |
|
|
166
27
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
**Mapping `NATIVE_TOKEN -> NATIVE_TOKEN` across chains with different native assets is dangerous:**
|
|
170
|
-
|
|
171
|
-
- The sucker bridges raw amounts without exchange rate conversion
|
|
172
|
-
- 1 CELO bridged as if it were 1 ETH massively overvalues the payment
|
|
173
|
-
- Project issuance (`baseCurrency=1`, i.e. ETH) treats CELO at 1:1 with ETH
|
|
174
|
-
|
|
175
|
-
**Safe chains** (ETH is native): Ethereum, Optimism, Base, Arbitrum -- `NATIVE_TOKEN -> NATIVE_TOKEN` works correctly.
|
|
176
|
-
|
|
177
|
-
**Unsafe chains** (non-ETH native): Celo, Polygon, Avalanche, BNB -- use ERC-20 WETH or USDC as the terminal accounting context, NOT `NATIVE_TOKEN`.
|
|
178
|
-
|
|
179
|
-
**Correct token mapping on Celo:**
|
|
180
|
-
```solidity
|
|
181
|
-
// Map WETH (ERC-20) on Ethereum to WETH (ERC-20) on Celo
|
|
182
|
-
JBTokenMapping({
|
|
183
|
-
localToken: WETH_ETHEREUM,
|
|
184
|
-
minGas: 200_000,
|
|
185
|
-
remoteToken: bytes32(uint256(uint160(WETH_CELO)))
|
|
186
|
-
})
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
**Wrong token mapping on Celo:**
|
|
190
|
-
```solidity
|
|
191
|
-
// NATIVE_TOKEN on Ethereum is ETH, but on Celo it's CELO!
|
|
192
|
-
JBTokenMapping({
|
|
193
|
-
localToken: JBConstants.NATIVE_TOKEN,
|
|
194
|
-
minGas: 200_000,
|
|
195
|
-
remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))) // WRONG
|
|
196
|
-
})
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
See also: `RISKS.md` in this repo.
|
|
200
|
-
|
|
201
|
-
## Deprecation Lifecycle
|
|
202
|
-
|
|
203
|
-
```
|
|
204
|
-
ENABLED --> DEPRECATION_PENDING --> SENDING_DISABLED --> DEPRECATED
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
| State | Condition | prepare | toRemote | fromRemote | claim | emergency exit |
|
|
208
|
-
|-------|-----------|---------|----------|------------|-------|----------------|
|
|
209
|
-
| `ENABLED` | `deprecatedAfter == 0` | yes | yes | yes | yes | per-token only |
|
|
210
|
-
| `DEPRECATION_PENDING` | `now < deprecatedAfter - 14 days` | yes | yes | yes | yes | per-token only |
|
|
211
|
-
| `SENDING_DISABLED` | `now < deprecatedAfter` | no | no | yes | yes | all tokens |
|
|
212
|
-
| `DEPRECATED` | `now >= deprecatedAfter` | no | no | yes | yes | all tokens |
|
|
213
|
-
|
|
214
|
-
## Example Integration
|
|
215
|
-
|
|
216
|
-
```solidity
|
|
217
|
-
// Deploy suckers for project 12 using CCIP to bridge ETH between Ethereum and Optimism
|
|
218
|
-
|
|
219
|
-
// 1. Grant MAP_SUCKER_TOKEN permission to the registry
|
|
220
|
-
uint256[] memory mapPermIds = new uint256[](1);
|
|
221
|
-
mapPermIds[0] = JBPermissionIds.MAP_SUCKER_TOKEN;
|
|
222
|
-
permissions.setPermissionsFor(
|
|
223
|
-
projectOwner,
|
|
224
|
-
JBPermissionsData({
|
|
225
|
-
operator: address(registry),
|
|
226
|
-
projectId: 12,
|
|
227
|
-
permissionIds: mapPermIds
|
|
228
|
-
})
|
|
229
|
-
);
|
|
28
|
+
## Purpose
|
|
230
29
|
|
|
231
|
-
|
|
232
|
-
JBTokenMapping[] memory mappings = new JBTokenMapping[](1);
|
|
233
|
-
mappings[0] = JBTokenMapping({
|
|
234
|
-
localToken: JBConstants.NATIVE_TOKEN,
|
|
235
|
-
minGas: 200_000,
|
|
236
|
-
remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN)))
|
|
237
|
-
});
|
|
30
|
+
Cross-chain bridge layer for Juicebox project tokens and the terminal assets that back them. Suckers package local burn or claim state into Merkle roots, relay those roots across bridge transports, and let users recreate the position on the remote chain.
|
|
238
31
|
|
|
239
|
-
|
|
240
|
-
JBSuckerDeployerConfig[] memory configs = new JBSuckerDeployerConfig[](1);
|
|
241
|
-
configs[0] = JBSuckerDeployerConfig({
|
|
242
|
-
deployer: IJBSuckerDeployer(ccipSuckerDeployerAddress),
|
|
243
|
-
mappings: mappings
|
|
244
|
-
});
|
|
32
|
+
## Reference Files
|
|
245
33
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
address[] memory suckers = registry.deploySuckersFor(12, salt, configs);
|
|
34
|
+
- Open [`references/runtime.md`](./references/runtime.md) when you need the base claim flow, registry role, token mapping model, or the main bridge invariants.
|
|
35
|
+
- Open [`references/operations.md`](./references/operations.md) when you need deployer and transport-selection guidance, deprecation and emergency behavior, or the common stale-data traps around bridge configuration.
|
|
249
36
|
|
|
250
|
-
|
|
251
|
-
uint256[] memory mintPermIds = new uint256[](1);
|
|
252
|
-
mintPermIds[0] = JBPermissionIds.MINT_TOKENS;
|
|
253
|
-
permissions.setPermissionsFor(
|
|
254
|
-
projectOwner,
|
|
255
|
-
JBPermissionsData({
|
|
256
|
-
operator: suckers[0],
|
|
257
|
-
projectId: 12,
|
|
258
|
-
permissionIds: mintPermIds
|
|
259
|
-
})
|
|
260
|
-
);
|
|
37
|
+
## Working Rules
|
|
261
38
|
|
|
262
|
-
|
|
263
|
-
|
|
39
|
+
- Start in [`src/JBSucker.sol`](./src/JBSucker.sol) for shared accounting and claim flow, then move to the chain-specific implementation only after you know the base path is correct.
|
|
40
|
+
- Treat token mapping, root progression, and emergency/deprecation controls as first-class runtime behavior, not admin-only side tooling.
|
|
41
|
+
- When debugging a bridge incident, separate accounting correctness from transport correctness before patching.
|
|
42
|
+
- If a task touches project deployment shape, check whether the real source is `nana-omnichain-deployers-v6` or `revnet-core-v6` instead of the sucker implementation itself.
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -26,8 +26,8 @@ pragma solidity 0.8.28;
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
|
28
28
|
|
|
29
|
-
// Libraries —
|
|
30
|
-
pragma solidity
|
|
29
|
+
// Libraries — pin to exact version like contracts
|
|
30
|
+
pragma solidity 0.8.28;
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Imports
|
|
@@ -86,12 +86,20 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
86
86
|
|
|
87
87
|
uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
88
88
|
|
|
89
|
+
//*********************************************************************//
|
|
90
|
+
// ------------------------ private constants ------------------------ //
|
|
91
|
+
//*********************************************************************//
|
|
92
|
+
|
|
89
93
|
//*********************************************************************//
|
|
90
94
|
// --------------- public immutable stored properties ---------------- //
|
|
91
95
|
//*********************************************************************//
|
|
92
96
|
|
|
93
97
|
IJBDirectory public immutable override DIRECTORY;
|
|
94
98
|
|
|
99
|
+
//*********************************************************************//
|
|
100
|
+
// -------------- internal immutable stored properties -------------- //
|
|
101
|
+
//*********************************************************************//
|
|
102
|
+
|
|
95
103
|
//*********************************************************************//
|
|
96
104
|
// --------------------- public stored properties -------------------- //
|
|
97
105
|
//*********************************************************************//
|
|
@@ -100,10 +108,26 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
100
108
|
// -------------------- internal stored properties ------------------- //
|
|
101
109
|
//*********************************************************************//
|
|
102
110
|
|
|
111
|
+
//*********************************************************************//
|
|
112
|
+
// -------------------- private stored properties -------------------- //
|
|
113
|
+
//*********************************************************************//
|
|
114
|
+
|
|
115
|
+
//*********************************************************************//
|
|
116
|
+
// ------------------- transient stored properties ------------------- //
|
|
117
|
+
//*********************************************************************//
|
|
118
|
+
|
|
103
119
|
//*********************************************************************//
|
|
104
120
|
// -------------------------- constructor ---------------------------- //
|
|
105
121
|
//*********************************************************************//
|
|
106
122
|
|
|
123
|
+
//*********************************************************************//
|
|
124
|
+
// ---------------------------- modifiers ---------------------------- //
|
|
125
|
+
//*********************************************************************//
|
|
126
|
+
|
|
127
|
+
//*********************************************************************//
|
|
128
|
+
// ------------------------- receive / fallback ---------------------- //
|
|
129
|
+
//*********************************************************************//
|
|
130
|
+
|
|
107
131
|
//*********************************************************************//
|
|
108
132
|
// ---------------------- external transactions ---------------------- //
|
|
109
133
|
//*********************************************************************//
|
|
@@ -112,10 +136,18 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
112
136
|
// ----------------------- external views ---------------------------- //
|
|
113
137
|
//*********************************************************************//
|
|
114
138
|
|
|
139
|
+
//*********************************************************************//
|
|
140
|
+
// -------------------------- public views --------------------------- //
|
|
141
|
+
//*********************************************************************//
|
|
142
|
+
|
|
115
143
|
//*********************************************************************//
|
|
116
144
|
// ----------------------- public transactions ----------------------- //
|
|
117
145
|
//*********************************************************************//
|
|
118
146
|
|
|
147
|
+
//*********************************************************************//
|
|
148
|
+
// ---------------------- internal transactions ---------------------- //
|
|
149
|
+
//*********************************************************************//
|
|
150
|
+
|
|
119
151
|
//*********************************************************************//
|
|
120
152
|
// ----------------------- internal helpers -------------------------- //
|
|
121
153
|
//*********************************************************************//
|
|
@@ -134,17 +166,28 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
134
166
|
1. Custom errors
|
|
135
167
|
2. Public constants
|
|
136
168
|
3. Internal constants
|
|
137
|
-
4.
|
|
138
|
-
5.
|
|
139
|
-
6.
|
|
140
|
-
7.
|
|
141
|
-
8.
|
|
142
|
-
9.
|
|
143
|
-
10.
|
|
144
|
-
11.
|
|
145
|
-
12.
|
|
146
|
-
13.
|
|
147
|
-
14.
|
|
169
|
+
4. Private constants
|
|
170
|
+
5. Public immutable stored properties
|
|
171
|
+
6. Internal immutable stored properties
|
|
172
|
+
7. Public stored properties
|
|
173
|
+
8. Internal stored properties
|
|
174
|
+
9. Private stored properties
|
|
175
|
+
10. Transient stored properties
|
|
176
|
+
11. Constructor
|
|
177
|
+
12. Modifiers
|
|
178
|
+
13. Receive / fallback
|
|
179
|
+
14. External transactions
|
|
180
|
+
15. External views
|
|
181
|
+
16. Public views
|
|
182
|
+
17. Public transactions
|
|
183
|
+
18. Internal transactions
|
|
184
|
+
19. Internal helpers
|
|
185
|
+
20. Internal views
|
|
186
|
+
21. Private helpers
|
|
187
|
+
|
|
188
|
+
Use these additional section labels where they better match the contents of the block:
|
|
189
|
+
- `internal functions` is accepted as equivalent to `internal helpers`
|
|
190
|
+
- `events` and `structs` are acceptable in specialized contracts that define them explicitly
|
|
148
191
|
|
|
149
192
|
Functions are alphabetized within each section.
|
|
150
193
|
|
|
@@ -197,7 +240,7 @@ interface IJBExample is IJBBase {
|
|
|
197
240
|
| Public/external function | `camelCase` | `cashOutTokensOf` |
|
|
198
241
|
| Internal/private function | `_camelCase` | `_processFee` |
|
|
199
242
|
| Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
|
|
200
|
-
| Function parameter | `camelCase` | `projectId`, `cashOutCount` |
|
|
243
|
+
| Function parameter | `camelCase` (no underscores) | `projectId`, `cashOutCount` |
|
|
201
244
|
|
|
202
245
|
## NatSpec
|
|
203
246
|
|
|
@@ -275,9 +318,7 @@ _transferOwnership(address(0), 0);
|
|
|
275
318
|
|
|
276
319
|
Single-argument calls use positional style: `_burn(amount)`.
|
|
277
320
|
|
|
278
|
-
This also applies to
|
|
279
|
-
|
|
280
|
-
**Exception:** Inherited constructor calls in the modifier position (`constructor(...) Parent(a, b)`) do not support named arguments in Solidity — positional style is required there.
|
|
321
|
+
This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
|
|
281
322
|
|
|
282
323
|
Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
|
|
283
324
|
|
|
@@ -567,7 +608,3 @@ CI checks formatting via `forge fmt --check`.
|
|
|
567
608
|
### Contract Size Checks
|
|
568
609
|
|
|
569
610
|
CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
|
|
570
|
-
|
|
571
|
-
## Repo-Specific Deviations
|
|
572
|
-
|
|
573
|
-
- **Multi-chain `[rpc_endpoints]`** — 7 endpoints vs standard 1
|