@bananapus/suckers-v6 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/SECURITY.md +55 -0
  4. package/SKILLS.md +163 -0
  5. package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSucker.json +1425 -0
  6. package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSuckerDeployer.json +391 -0
  7. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker.json +1479 -0
  8. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer.json +433 -0
  9. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_1.json +433 -0
  10. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_2.json +433 -0
  11. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_1.json +1479 -0
  12. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_2.json +1479 -0
  13. package/deployments/nana-suckers-v5/arbitrum/JBSuckerRegistry.json +690 -0
  14. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSucker.json +1425 -0
  15. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSuckerDeployer.json +391 -0
  16. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker.json +1479 -0
  17. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer.json +433 -0
  18. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_1.json +433 -0
  19. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_2.json +433 -0
  20. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_1.json +1479 -0
  21. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_2.json +1479 -0
  22. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBSuckerRegistry.json +690 -0
  23. package/deployments/nana-suckers-v5/base/JBBaseSucker.json +1389 -0
  24. package/deployments/nana-suckers-v5/base/JBBaseSuckerDeployer.json +376 -0
  25. package/deployments/nana-suckers-v5/base/JBCCIPSucker.json +1483 -0
  26. package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer.json +436 -0
  27. package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_1.json +436 -0
  28. package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_2.json +436 -0
  29. package/deployments/nana-suckers-v5/base/JBCCIPSucker_1.json +1483 -0
  30. package/deployments/nana-suckers-v5/base/JBCCIPSucker_2.json +1483 -0
  31. package/deployments/nana-suckers-v5/base/JBSuckerRegistry.json +694 -0
  32. package/deployments/nana-suckers-v5/base_sepolia/JBBaseSucker.json +1389 -0
  33. package/deployments/nana-suckers-v5/base_sepolia/JBBaseSuckerDeployer.json +376 -0
  34. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker.json +1483 -0
  35. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer.json +436 -0
  36. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
  37. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
  38. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_1.json +1483 -0
  39. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_2.json +1483 -0
  40. package/deployments/nana-suckers-v5/base_sepolia/JBSuckerRegistry.json +694 -0
  41. package/deployments/nana-suckers-v5/ethereum/JBArbitrumSucker.json +1429 -0
  42. package/deployments/nana-suckers-v5/ethereum/JBArbitrumSuckerDeployer.json +394 -0
  43. package/deployments/nana-suckers-v5/ethereum/JBBaseSucker.json +1389 -0
  44. package/deployments/nana-suckers-v5/ethereum/JBBaseSuckerDeployer.json +376 -0
  45. package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker.json +1483 -0
  46. package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer.json +436 -0
  47. package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_1.json +436 -0
  48. package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_2.json +436 -0
  49. package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_1.json +1483 -0
  50. package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_2.json +1483 -0
  51. package/deployments/nana-suckers-v5/ethereum/JBOptimismSucker.json +1389 -0
  52. package/deployments/nana-suckers-v5/ethereum/JBOptimismSuckerDeployer.json +376 -0
  53. package/deployments/nana-suckers-v5/ethereum/JBSuckerRegistry.json +694 -0
  54. package/deployments/nana-suckers-v5/optimism/JBCCIPSucker.json +1479 -0
  55. package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer.json +433 -0
  56. package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_1.json +433 -0
  57. package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_2.json +433 -0
  58. package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_1.json +1479 -0
  59. package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_2.json +1479 -0
  60. package/deployments/nana-suckers-v5/optimism/JBOptimismSucker.json +1385 -0
  61. package/deployments/nana-suckers-v5/optimism/JBOptimismSuckerDeployer.json +373 -0
  62. package/deployments/nana-suckers-v5/optimism/JBSuckerRegistry.json +690 -0
  63. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker.json +1483 -0
  64. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer.json +436 -0
  65. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
  66. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
  67. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_1.json +1483 -0
  68. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_2.json +1483 -0
  69. package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSucker.json +1389 -0
  70. package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSuckerDeployer.json +376 -0
  71. package/deployments/nana-suckers-v5/optimism_sepolia/JBSuckerRegistry.json +694 -0
  72. package/deployments/nana-suckers-v5/sepolia/JBArbitrumSucker.json +1429 -0
  73. package/deployments/nana-suckers-v5/sepolia/JBArbitrumSuckerDeployer.json +394 -0
  74. package/deployments/nana-suckers-v5/sepolia/JBBaseSucker.json +1389 -0
  75. package/deployments/nana-suckers-v5/sepolia/JBBaseSuckerDeployer.json +376 -0
  76. package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker.json +1483 -0
  77. package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer.json +436 -0
  78. package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_1.json +436 -0
  79. package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_2.json +436 -0
  80. package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_1.json +1483 -0
  81. package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_2.json +1483 -0
  82. package/deployments/nana-suckers-v5/sepolia/JBOptimismSucker.json +1389 -0
  83. package/deployments/nana-suckers-v5/sepolia/JBOptimismSuckerDeployer.json +376 -0
  84. package/deployments/nana-suckers-v5/sepolia/JBSuckerRegistry.json +694 -0
  85. package/foundry.lock +11 -0
  86. package/foundry.toml +22 -0
  87. package/package.json +33 -0
  88. package/remappings.txt +1 -0
  89. package/script/Deploy.s.sol +506 -0
  90. package/script/helpers/SuckerDeploymentLib.sol +97 -0
  91. package/slither-ci.config.json +10 -0
  92. package/sphinx.lock +476 -0
  93. package/src/JBArbitrumSucker.sol +311 -0
  94. package/src/JBBaseSucker.sol +41 -0
  95. package/src/JBCCIPSucker.sol +303 -0
  96. package/src/JBOptimismSucker.sol +143 -0
  97. package/src/JBSucker.sol +1159 -0
  98. package/src/JBSuckerRegistry.sol +262 -0
  99. package/src/deployers/JBArbitrumSuckerDeployer.sol +86 -0
  100. package/src/deployers/JBBaseSuckerDeployer.sol +26 -0
  101. package/src/deployers/JBCCIPSuckerDeployer.sol +88 -0
  102. package/src/deployers/JBOptimismSuckerDeployer.sol +82 -0
  103. package/src/deployers/JBSuckerDeployer.sol +147 -0
  104. package/src/enums/JBAddToBalanceMode.sol +11 -0
  105. package/src/enums/JBLayer.sol +8 -0
  106. package/src/enums/JBSuckerState.sol +14 -0
  107. package/src/interfaces/IArbGatewayRouter.sol +11 -0
  108. package/src/interfaces/IArbL1GatewayRouter.sol +17 -0
  109. package/src/interfaces/IArbL2GatewayRouter.sol +14 -0
  110. package/src/interfaces/ICCIPRouter.sol +11 -0
  111. package/src/interfaces/IJBArbitrumSucker.sol +13 -0
  112. package/src/interfaces/IJBArbitrumSuckerDeployer.sol +12 -0
  113. package/src/interfaces/IJBCCIPSuckerDeployer.sol +15 -0
  114. package/src/interfaces/IJBOpSuckerDeployer.sol +11 -0
  115. package/src/interfaces/IJBOptimismSucker.sol +10 -0
  116. package/src/interfaces/IJBSucker.sol +144 -0
  117. package/src/interfaces/IJBSuckerDeployer.sol +40 -0
  118. package/src/interfaces/IJBSuckerExtended.sol +22 -0
  119. package/src/interfaces/IJBSuckerRegistry.sol +75 -0
  120. package/src/interfaces/IOPMessenger.sol +18 -0
  121. package/src/interfaces/IOPStandardBridge.sol +29 -0
  122. package/src/interfaces/IWrappedNativeToken.sol +13 -0
  123. package/src/libraries/ARBAddresses.sol +17 -0
  124. package/src/libraries/ARBChains.sol +11 -0
  125. package/src/libraries/CCIPHelper.sol +136 -0
  126. package/src/structs/JBClaim.sol +13 -0
  127. package/src/structs/JBInboxTreeRoot.sol +12 -0
  128. package/src/structs/JBLeaf.sol +14 -0
  129. package/src/structs/JBMessageRoot.sol +16 -0
  130. package/src/structs/JBOutboxTree.sol +18 -0
  131. package/src/structs/JBRemoteToken.sol +17 -0
  132. package/src/structs/JBSuckerDeployerConfig.sol +12 -0
  133. package/src/structs/JBSuckersPair.sol +11 -0
  134. package/src/structs/JBTokenMapping.sol +13 -0
  135. package/src/utils/MerkleLib.sol +1020 -0
  136. package/test/Fork.t.sol +514 -0
  137. package/test/InteropCompat.t.sol +676 -0
  138. package/test/SuckerAttacks.t.sol +509 -0
  139. package/test/SuckerDeepAttacks.t.sol +1563 -0
  140. package/test/mocks/ERC20Mock.sol +36 -0
  141. package/test/mocks/MockMessenger.sol +42 -0
  142. package/test/unit/arb.t.sol +28 -0
  143. package/test/unit/ccip_native_interop.t.sol +719 -0
  144. package/test/unit/ccip_refund.t.sol +234 -0
  145. package/test/unit/deployer.t.sol +475 -0
  146. package/test/unit/emergency.t.sol +305 -0
  147. package/test/unit/merkle.t.sol +212 -0
  148. package/test/unit/multi_chain_evolution.t.sol +622 -0
  149. package/test/unit/registry.t.sol +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Bananapus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,422 @@
1
+ # nana-suckers-v5
2
+
3
+ Cross-chain bridging for Juicebox V5 projects. Suckers let users burn project tokens on one chain and receive the same amount on another, moving the backing funds across via merkle-tree-based claims and chain-specific bridges.
4
+
5
+ <details>
6
+ <summary>Table of Contents</summary>
7
+ <ol>
8
+ <li><a href="#what-are-suckers">What are Suckers?</a></li>
9
+ <li><a href="#architecture">Architecture</a></li>
10
+ <li><a href="#bridging-flow">Bridging Flow</a></li>
11
+ <li><a href="#bridging-tokens">Bridging Tokens</a></li>
12
+ <li><a href="#launching-suckers">Launching Suckers</a></li>
13
+ <li><a href="#managing-suckers">Managing Suckers</a></li>
14
+ <li><a href="#using-the-relayer">Using the Relayer</a></li>
15
+ <li><a href="#resources">Resources</a></li>
16
+ <li><a href="#repository-layout">Repository Layout</a></li>
17
+ <li><a href="#usage">Usage</a></li>
18
+ <ul>
19
+ <li><a href="#install">Install</a></li>
20
+ <li><a href="#develop">Develop</a></li>
21
+ <li><a href="#scripts">Scripts</a></li>
22
+ <li><a href="#tips">Tips</a></li>
23
+ </ul>
24
+ </ol>
25
+ </details>
26
+
27
+ _If you're having trouble understanding this contract, take a look at the [core protocol contracts](https://github.com/Bananapus/nana-core-v5) and the [documentation](https://docs.juicebox.money/) first. If you have questions, reach out on [Discord](https://discord.com/invite/ErQYmth4dS)._
28
+
29
+ ## What are Suckers?
30
+
31
+ `JBSucker` contracts are deployed in pairs, with one on each network being bridged to or from. The [`JBSucker`](src/JBSucker.sol) contract implements core logic, and is extended by network-specific implementations adapted to each bridge:
32
+
33
+ | Sucker | Networks | Description |
34
+ |--------|----------|-------------|
35
+ | [`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. |
36
+ | [`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). |
37
+ | [`JBBaseSucker`](src/JBBaseSucker.sol) | Ethereum and Base | A thin wrapper around `JBOptimismSucker` with Base chain IDs. |
38
+ | [`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. |
39
+
40
+ Suckers use two [merkle trees](https://en.wikipedia.org/wiki/Merkle_tree) to track project token claims associated with each terminal token they support:
41
+
42
+ - The **outbox tree** tracks tokens on the local chain — the network that the sucker is on.
43
+ - The **inbox tree** tracks tokens which have been bridged from the peer chain — the network that the sucker's peer is on.
44
+
45
+ For example, a sucker which supports bridging ETH and USDC would have four trees — an inbox and outbox tree for each token. These trees are append-only, and when they're bridged over to the other chain, they aren't deleted — they only update the remote inbox tree with the latest root.
46
+
47
+ To insert project tokens into the outbox tree, users call `JBSucker.prepare(...)` with the amount of project tokens to bridge and the terminal token to bridge with them. The sucker cashes out those project tokens to reclaim the chosen terminal token from the project's primary terminal. Then it inserts a claim with this information into the outbox tree.
48
+
49
+ Anyone can bridge an outbox tree to the peer chain by calling `JBSucker.toRemote(token)`. The outbox tree then _becomes_ the peer sucker's inbox tree for that token. Users can claim their tokens on the peer chain by providing a merkle proof which shows that their claim is in the inbox tree.
50
+
51
+ ## Architecture
52
+
53
+ On each network:
54
+
55
+ ```mermaid
56
+ graph TD;
57
+ A[JBSuckerRegistry] -->|exposes| B["deploySuckersFor(...)"]
58
+ B -->|calls| C[IJBSuckerDeployer]
59
+ C -->|deploys| D[JBSucker]
60
+ A -->|tracks| D
61
+ ```
62
+
63
+ For an example project deployed on mainnet and Optimism with a `JBOptimismSucker` on each network:
64
+
65
+ ```mermaid
66
+ graph TD;
67
+ subgraph Mainnet
68
+ A[Project] -->|cashed-out funds| B[JBOptimismSucker]
69
+ B -->|burns/mints tokens| A
70
+ end
71
+ subgraph Optimism
72
+ C[Project] -->|cashed-out funds| D[JBOptimismSucker]
73
+ D -->|burns/mints tokens| C
74
+ end
75
+ B <-->|merkle roots/funds| D
76
+ ```
77
+
78
+ ### Contracts
79
+
80
+ | Contract | Description |
81
+ |----------|-------------|
82
+ | `JBSucker` | Abstract base. Manages outbox/inbox merkle trees, `prepare`/`toRemote`/`claim` lifecycle, token mapping, deprecation, and emergency hatch. Deployed as clones via `Initializable`. |
83
+ | `JBCCIPSucker` | Extends `JBSucker`. Bridges via Chainlink CCIP (`ccipSend`/`ccipReceive`). Supports any CCIP-connected chain. Handles native token wrapping/unwrapping. |
84
+ | `JBOptimismSucker` | Extends `JBSucker`. Bridges via OP Standard Bridge + OP Messenger. |
85
+ | `JBBaseSucker` | Thin wrapper around `JBOptimismSucker` with Base chain IDs. |
86
+ | `JBArbitrumSucker` | Extends `JBSucker`. Bridges via Arbitrum Inbox + Gateway Router. Handles L1<->L2 retryable tickets. |
87
+ | `JBSuckerRegistry` | Tracks all suckers per project. Manages deployer allowlist. Entry point for `deploySuckersFor`. |
88
+ | `JBSuckerDeployer` | Abstract base deployer. Clones a singleton sucker via `LibClone.cloneDeterministic` and initializes it. |
89
+ | `JBCCIPSuckerDeployer` | Deployer for `JBCCIPSucker`. Stores CCIP router, remote chain ID, and chain selector. |
90
+ | `JBOptimismSuckerDeployer` | Deployer for `JBOptimismSucker`. Stores OP Messenger and OP Bridge addresses. |
91
+ | `JBBaseSuckerDeployer` | Thin wrapper around `JBOptimismSuckerDeployer` for Base. |
92
+ | `JBArbitrumSuckerDeployer` | Deployer for `JBArbitrumSucker`. Stores Arbitrum Inbox, Gateway Router, and layer (L1/L2). |
93
+ | `MerkleLib` | Incremental merkle tree (depth 32, modeled on eth2 deposit contract). Used for outbox/inbox trees. |
94
+ | `CCIPHelper` | CCIP router addresses, chain selectors, and WETH addresses per chain. |
95
+ | `ARBAddresses` | Arbitrum bridge contract addresses (Inbox, Gateway Router) for mainnet and Sepolia. |
96
+ | `ARBChains` | Arbitrum chain ID constants. |
97
+
98
+ ## Bridging Flow
99
+
100
+ ```
101
+ Chain A Chain B
102
+ | |
103
+ | 1. prepare(tokenCount, ...) |
104
+ | - transfers project tokens |
105
+ | - cashes out for terminal tkn |
106
+ | - inserts leaf into outbox |
107
+ | |
108
+ | 2. toRemote(token) |
109
+ | - sends merkle root + funds -->|
110
+ | |
111
+ | 3. fromRemote(root)
112
+ | - updates inbox tree
113
+ | |
114
+ | 4. claim(proof)
115
+ | - verifies merkle proof
116
+ | - mints project tokens
117
+ | - adds funds to balance
118
+ ```
119
+
120
+ ## Bridging Tokens
121
+
122
+ Imagine that "OhioDAO" is deployed on Ethereum mainnet and Optimism:
123
+
124
+ - It has the $OHIO ERC-20 project token and a `JBOptimismSucker` deployed on each network.
125
+ - Its suckers map mainnet ETH to Optimism ETH, and vice versa.
126
+
127
+ Each sucker has mappings from terminal tokens on the local chain to associated terminal tokens on the remote chain.
128
+
129
+ _Here's how Jimmy can bridge his $OHIO tokens (and the corresponding ETH) from mainnet to Optimism._
130
+
131
+ **1. Pay the project.** Jimmy pays OhioDAO 1 ETH on Ethereum mainnet:
132
+
133
+ ```solidity
134
+ JBMultiTerminal.pay{value: 1 ether}({
135
+ projectId: 12,
136
+ token: JBConstants.NATIVE_TOKEN,
137
+ amount: 1 ether,
138
+ beneficiary: jimmy,
139
+ minReturnedTokens: 0,
140
+ memo: "OhioDAO rules",
141
+ metadata: ""
142
+ });
143
+ ```
144
+
145
+ OhioDAO's ruleset has a `weight` of `1e18`, so Jimmy receives 1 $OHIO (`1e18` tokens).
146
+
147
+ **2. Approve the sucker.** Before bridging, Jimmy approves the `JBOptimismSucker` to transfer his $OHIO:
148
+
149
+ ```solidity
150
+ JBERC20.approve({
151
+ spender: address(optimismSucker),
152
+ value: 1e18
153
+ });
154
+ ```
155
+
156
+ **3. Prepare the bridge.** Jimmy calls `prepare(...)` on the mainnet sucker:
157
+
158
+ ```solidity
159
+ JBOptimismSucker.prepare({
160
+ projectTokenAmount: 1e18,
161
+ beneficiary: jimmy,
162
+ minTokensReclaimed: 0,
163
+ token: JBConstants.NATIVE_TOKEN
164
+ });
165
+ ```
166
+
167
+ 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's address, the project token count, and the terminal token amount reclaimed.
168
+
169
+ **4. Bridge to remote.** Jimmy (or anyone) calls `toRemote(...)`:
170
+
171
+ ```solidity
172
+ JBOptimismSucker.toRemote(JBConstants.NATIVE_TOKEN);
173
+ ```
174
+
175
+ This sends the outbox merkle root and the accumulated ETH to the peer sucker on Optimism. After the bridge completes, the Optimism sucker's ETH inbox tree is updated with the new root containing Jimmy's claim.
176
+
177
+ **5. Claim on the remote chain.** Jimmy claims his $OHIO on Optimism by calling `claim(...)` with a [`JBClaim`](src/structs/JBClaim.sol):
178
+
179
+ ```solidity
180
+ struct JBClaim {
181
+ address token;
182
+ JBLeaf leaf;
183
+ bytes32[32] proof; // Must be TREE_DEPTH (32) long.
184
+ }
185
+ ```
186
+
187
+ The [`JBLeaf`](src/structs/JBLeaf.sol):
188
+
189
+ ```solidity
190
+ struct JBLeaf {
191
+ uint256 index;
192
+ address beneficiary;
193
+ uint256 projectTokenCount;
194
+ uint256 terminalTokenAmount;
195
+ }
196
+ ```
197
+
198
+ 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`:
199
+
200
+ ```json
201
+ {
202
+ "chainId": 10,
203
+ "sucker": "0x5678...",
204
+ "token": "0x000000000000000000000000000000000000EEEe",
205
+ "beneficiary": "0x1234..."
206
+ }
207
+ ```
208
+
209
+ | Field | Type | Description |
210
+ |-------|------|-------------|
211
+ | `chainId` | `int` | Network ID for the sucker being claimed from. |
212
+ | `sucker` | `string` | Address of the sucker being claimed from. |
213
+ | `token` | `string` | Terminal token whose inbox tree is being claimed from. |
214
+ | `beneficiary` | `string` | Address to get available claims for. |
215
+
216
+ The service looks through the entire inbox tree and returns all available claims as `JBClaim` structs ready to pass to `claim(...)`.
217
+
218
+ If the sucker's `ADD_TO_BALANCE_MODE` is `ON_CLAIM`, the bridged ETH is immediately added to OhioDAO's Optimism balance. Otherwise, someone calls `addOutstandingAmountToBalance(token)` to add it manually.
219
+
220
+ ## Launching Suckers
221
+
222
+ Requirements for deploying a sucker pair:
223
+
224
+ 1. **Projects on both chains.** Project IDs don't have to match.
225
+ 2. **100% cash out rate.** Both projects must have `cashOutTaxRate` of `JBConstants.MAX_CASH_OUT_TAX_RATE` so suckers can fully cash out project tokens for terminal tokens.
226
+ 3. **Owner minting enabled.** Both projects must have `allowOwnerMinting` set to `true` so suckers can mint bridged project tokens.
227
+ 4. **ERC-20 project token.** Both projects must have a deployed ERC-20 token (via `JBController.deployERC20For(...)`).
228
+
229
+ 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:
230
+
231
+ ```solidity
232
+ // Give the registry MAP_SUCKER_TOKEN permission for project 12
233
+ uint256[] memory permissionIds = new uint256[](1);
234
+ permissionIds[0] = JBPermissionIds.MAP_SUCKER_TOKEN; // ID 28
235
+
236
+ permissions.setPermissionsFor(
237
+ projectOwner,
238
+ JBPermissionsData({
239
+ operator: address(registry),
240
+ projectId: 12,
241
+ permissionIds: permissionIds
242
+ })
243
+ );
244
+ ```
245
+
246
+ Now deploy the suckers with a token mapping:
247
+
248
+ ```solidity
249
+ // Map mainnet ETH to Optimism ETH
250
+ JBTokenMapping[] memory mappings = new JBTokenMapping[](1);
251
+ mappings[0] = JBTokenMapping({
252
+ localToken: JBConstants.NATIVE_TOKEN,
253
+ minGas: 200_000,
254
+ remoteToken: JBConstants.NATIVE_TOKEN,
255
+ minBridgeAmount: 0.025 ether
256
+ });
257
+
258
+ JBSuckerDeployerConfig[] memory configs = new JBSuckerDeployerConfig[](1);
259
+ configs[0] = JBSuckerDeployerConfig({
260
+ deployer: IJBSuckerDeployer(optimismSuckerDeployer),
261
+ mappings: mappings
262
+ });
263
+
264
+ // Must use the same salt and caller on both chains
265
+ bytes32 salt = keccak256("my-project-suckers-v1");
266
+ address[] memory suckers = registry.deploySuckersFor(12, salt, configs);
267
+ ```
268
+
269
+ - The [`JBTokenMapping`](src/structs/JBTokenMapping.sol) maps local mainnet ETH to remote Optimism ETH.
270
+ - `minBridgeAmount` prevents spam — ours blocks attempts to bridge less than 0.025 ETH.
271
+ - `minGas` requires a gas limit of at least 200,000. If your token has expensive transfer logic, you may need more.
272
+ - 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.
273
+ - **For the suckers to be peers, the `salt` has to match on both chains and the same address must call `deploySuckersFor(...)`.**
274
+
275
+ Finally, give the sucker permission to mint bridged project tokens:
276
+
277
+ ```solidity
278
+ uint256[] memory mintPermissionIds = new uint256[](1);
279
+ mintPermissionIds[0] = JBPermissionIds.MINT_TOKENS; // ID 9
280
+
281
+ permissions.setPermissionsFor(
282
+ projectOwner,
283
+ JBPermissionsData({
284
+ operator: suckers[0],
285
+ projectId: 12,
286
+ permissionIds: mintPermissionIds
287
+ })
288
+ );
289
+ ```
290
+
291
+ Repeat this process on the other chain to deploy the peer sucker, and the project is ready for bridging.
292
+
293
+ ## Managing Suckers
294
+
295
+ 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.
296
+
297
+ **Always perform these actions on BOTH sides of the sucker pair.**
298
+
299
+ ### Disable a token
300
+
301
+ If a bridge change affects only certain tokens, call `mapToken(...)` with `remoteToken` set to `address(0)` to disable that token. If the bridge won't allow a final transfer with the remaining funds, activate the `EmergencyHatch` for the affected tokens.
302
+
303
+ 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.
304
+
305
+ ### Deprecate the suckers
306
+
307
+ If the bridging infrastructure will no longer work, deprecate the sucker to begin shutdown. The deprecation timestamp must be at least `_maxMessagingDelay()` (14 days) in the future to ensure no funds or roots are lost in transit. After this duration, all tokens allow exit through the `EmergencyHatch` and no new messages are accepted. This protects against future fake or malicious bridge messages.
308
+
309
+ When deprecating, ensure no pending bridge messages need retrying — once deprecation completes, those messages will be rejected.
310
+
311
+ ## Using the Relayer
312
+
313
+ 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):
314
+
315
+ 1. The **withdrawal initiating transaction**, which the user submits on L2.
316
+ 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).
317
+ 3. The **withdrawal finalizing transaction**, which the user submits on L1 after the fault challenge period has passed.
318
+
319
+ 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.
320
+
321
+ ## Resources
322
+
323
+ - [`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.
324
+ - [`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.
325
+ - [`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.
326
+
327
+ ## Repository Layout
328
+
329
+ ```
330
+ nana-suckers-v5/
331
+ ├── script/
332
+ │ ├── Deploy.s.sol - Deployment script.
333
+ │ └── helpers/
334
+ │ └── SuckerDeploymentLib.sol - Internal helpers for deployment.
335
+ ├── src/
336
+ │ ├── JBSucker.sol - Abstract base sucker implementation.
337
+ │ ├── JBCCIPSucker.sol - Chainlink CCIP bridge implementation.
338
+ │ ├── JBOptimismSucker.sol - OP Stack bridge implementation.
339
+ │ ├── JBBaseSucker.sol - Base-specific wrapper around JBOptimismSucker.
340
+ │ ├── JBArbitrumSucker.sol - Arbitrum bridge implementation.
341
+ │ ├── JBSuckerRegistry.sol - Registry tracking suckers per project.
342
+ │ ├── deployers/ - Deployers for each kind of sucker.
343
+ │ ├── enums/ - JBAddToBalanceMode, JBLayer, JBSuckerState.
344
+ │ ├── interfaces/ - Contract interfaces.
345
+ │ ├── libraries/ - ARBAddresses, ARBChains, CCIPHelper.
346
+ │ ├── structs/ - JBClaim, JBLeaf, JBMessageRoot, JBOutboxTree, etc.
347
+ │ └── utils/
348
+ │ └── MerkleLib.sol - Incremental merkle tree (depth 32).
349
+ └── test/
350
+ ├── Fork.t.sol - Fork tests.
351
+ ├── SuckerAttacks.t.sol - Security-focused attack tests.
352
+ ├── SuckerDeepAttacks.t.sol - Deep attack scenario tests.
353
+ ├── mocks/ - Mock contracts for testing.
354
+ └── unit/ - Unit tests (merkle, registry, deployer, emergency, arb).
355
+ ```
356
+
357
+ ## Usage
358
+
359
+ ### Install
360
+
361
+ For projects using `npm` to manage dependencies (recommended):
362
+
363
+ ```bash
364
+ npm install @bananapus/suckers-v5
365
+ ```
366
+
367
+ For projects using `forge` to manage dependencies:
368
+
369
+ ```bash
370
+ forge install Bananapus/nana-suckers-v5
371
+ ```
372
+
373
+ If you're using `forge`, add `@bananapus/suckers-v5/=lib/nana-suckers-v5/` to `remappings.txt`. You'll also need to install `nana-suckers-v5`'s dependencies and add similar remappings for them.
374
+
375
+ ### Develop
376
+
377
+ `nana-suckers-v5` 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):
378
+
379
+ ```bash
380
+ curl -L https://foundry.paradigm.xyz | sh
381
+ ```
382
+
383
+ Download and install dependencies with:
384
+
385
+ ```bash
386
+ npm ci && forge install
387
+ ```
388
+
389
+ If you run into trouble with `forge install`, try using `git submodule update --init --recursive` to ensure that nested submodules have been properly initialized.
390
+
391
+ | Command | Description |
392
+ |---------|-------------|
393
+ | `forge build` | Compile the contracts and write artifacts to `out`. |
394
+ | `forge test` | Run the tests. |
395
+ | `forge test -vvvv` | Run tests with full traces. |
396
+ | `forge fmt` | Lint. |
397
+ | `forge coverage` | Generate a test coverage report. |
398
+ | `forge build --sizes` | Get contract sizes. |
399
+ | `forge clean` | Remove build artifacts and cache. |
400
+ | `foundryup` | Update Foundry. Run this periodically. |
401
+
402
+ To learn more, visit the [Foundry Book](https://book.getfoundry.sh/) docs.
403
+
404
+ ### Scripts
405
+
406
+ | Command | Description |
407
+ |---------|-------------|
408
+ | `npm test` | Run local tests. |
409
+ | `npm run coverage` | Generate an LCOV test coverage report. |
410
+ | `npm run artifacts` | Fetch Sphinx artifacts and write them to `deployments/`. |
411
+
412
+ ### Tips
413
+
414
+ 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.
415
+
416
+ 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:
417
+
418
+ ```bash
419
+ forge remappings >> remappings.txt
420
+ ```
421
+
422
+ This makes the extension aware of default remappings.
package/SECURITY.md ADDED
@@ -0,0 +1,55 @@
1
+ # Security Considerations
2
+
3
+ ## [INTEROP-6] Cross-Chain Accounting Mismatch: NATIVE_TOKEN Semantic Divergence
4
+
5
+ **Severity:** Medium
6
+ **Status:** Acknowledged — by design, not fixable without oracle dependencies
7
+
8
+ ### Description
9
+
10
+ `JBConstants.NATIVE_TOKEN` represents different real-world assets on different chains:
11
+
12
+ | Chain | Native Token | Value |
13
+ |-------|-------------|-------|
14
+ | Ethereum, Optimism, Base, Arbitrum | ETH | ~$X |
15
+ | Celo | CELO | ~$Y |
16
+ | Polygon | MATIC | ~$Z |
17
+
18
+ When a project deploys suckers that map `NATIVE_TOKEN → NATIVE_TOKEN` across chains where the native token differs, the protocol treats different assets as equivalent. There is no on-chain mechanism that distinguishes ETH from CELO at the sucker level.
19
+
20
+ ### How Suckers Bridge Tokens
21
+
22
+ 1. Source chain: sucker wraps `NATIVE_TOKEN` → WETH (via `CCIPHelper.wethOfChain()`)
23
+ 2. CCIP bridges the WETH to the destination chain
24
+ 3. Destination chain: sucker unwraps WETH → `NATIVE_TOKEN`
25
+
26
+ On ETH-native chains (Ethereum, OP, Base, Arbitrum), this works correctly because WETH wraps/unwraps ETH.
27
+
28
+ On Celo, `NATIVE_TOKEN` is CELO. The sucker would attempt to wrap CELO into WETH, which is a different operation — WETH on Celo (`0xD221812de1BD094f35587EE8E174B07B6167D9Af`) wraps ETH bridged to Celo, not CELO itself.
29
+
30
+ ### Impact
31
+
32
+ 1. **Issuance mispricing** — Payments in CELO through a `NATIVE_TOKEN` terminal are priced as ETH-equivalent without a CELO/ETH price feed.
33
+ 2. **Sucker bridging failure** — `NATIVE_TOKEN → NATIVE_TOKEN` mapping causes the CCIP sucker to attempt incompatible token operations.
34
+ 3. **Surplus fragmentation** — Cash-out bonding curve on each chain only sees that chain's surplus. Users must bridge project tokens to the chain with more surplus to get fair cash-out values.
35
+
36
+ ### Why the Matching Hash Doesn't Catch This
37
+
38
+ The REVDeployer matching hash (used to verify both sides of a sucker deployment match) includes economic parameters (baseCurrency, stages, issuance, cash-out tax) but does NOT include terminal configurations, accounting contexts, or token mappings. Two deployments can produce identical hashes with incompatible asset configurations.
39
+
40
+ ### Recommended Mitigation (Operational)
41
+
42
+ For non-ETH-native chains (Celo, Polygon, Avalanche, BNB Chain):
43
+
44
+ 1. **Use WETH (ERC20) as the accounting context** — not `NATIVE_TOKEN`. This avoids the semantic ambiguity about what "native" means.
45
+ 2. **Set sucker token mappings as `WETH → WETH`** (ERC20 to ERC20) — not `NATIVE_TOKEN → NATIVE_TOKEN`.
46
+ 3. **Use USDC as a second accounting context** — USDC is the same asset cross-chain, no conversion needed.
47
+ 4. **Ensure `JBPrices` has working price feeds** for accepted tokens on each chain.
48
+
49
+ ### Safe Chains (ETH-native)
50
+
51
+ OP Stack L2s where native token IS ETH are unaffected: Ethereum, Optimism, Base, Arbitrum.
52
+
53
+ ### Affected Chains
54
+
55
+ Any chain where the native token is not ETH: Celo (CELO), Polygon (MATIC), Avalanche (AVAX), BNB Chain (BNB).
package/SKILLS.md ADDED
@@ -0,0 +1,163 @@
1
+ # nana-suckers-v5
2
+
3
+ ## Purpose
4
+
5
+ Cross-chain token and fund bridging for Juicebox V5 projects, using merkle trees to batch claims and chain-specific bridges (Chainlink CCIP, OP Stack, Arbitrum) to move assets.
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. |
12
+ | `JBCCIPSucker` | CCIP bridge implementation. Implements `IAny2EVMMessageReceiver.ccipReceive`. Wraps/unwraps native tokens for CCIP transport. |
13
+ | `JBOptimismSucker` | OP Stack bridge implementation. Uses `IOPMessenger.sendMessage` and `IOPStandardBridge.bridgeERC20To`. |
14
+ | `JBBaseSucker` | Extends `JBOptimismSucker` with Base<->Ethereum chain ID mapping. |
15
+ | `JBArbitrumSucker` | Arbitrum bridge implementation. Uses retryable tickets (`unsafeCreateRetryableTicket`) for L1->L2, `ArbSys.sendTxToL1` for L2->L1. Handles L1/L2 address aliasing. |
16
+ | `JBSuckerRegistry` | Entry point for deploying and tracking suckers. Manages deployer allowlist. Requires `DEPLOY_SUCKERS` permission. |
17
+ | `JBSuckerDeployer` | Abstract deployer base. Uses Solady `LibClone.cloneDeterministic` to deploy suckers as minimal proxies. |
18
+ | `JBCCIPSuckerDeployer` | CCIP-specific deployer. Stores `ccipRouter`, `ccipRemoteChainId`, `ccipRemoteChainSelector`. |
19
+ | `JBOptimismSuckerDeployer` | OP-specific deployer. Stores `opMessenger`, `opBridge`. |
20
+ | `JBBaseSuckerDeployer` | Thin wrapper around `JBOptimismSuckerDeployer` for separate Base artifact. |
21
+ | `JBArbitrumSuckerDeployer` | Arbitrum-specific deployer. Stores `arbInbox`, `arbGatewayRouter`, `arbLayer`. |
22
+ | `MerkleLib` | Incremental merkle tree (depth 32). `insert` appends leaves, `root` computes current root, `branchRoot` verifies proofs. |
23
+
24
+ ## Key Functions
25
+
26
+ | Function | Contract | What it does |
27
+ |----------|----------|--------------|
28
+ | `prepare(projectTokenCount, beneficiary, minTokensReclaimed, token)` | `JBSucker` | Transfers project tokens from caller, cashes them out for terminal tokens, inserts a leaf into the outbox merkle tree. |
29
+ | `toRemote(token)` | `JBSucker` | Sends the outbox merkle root and accumulated funds for `token` to the peer sucker on the remote chain via the bridge. |
30
+ | `fromRemote(root)` | `JBSucker` | Receives a merkle root from the remote peer. Updates the inbox tree. Only callable by the verified remote peer. |
31
+ | `claim(claimData)` | `JBSucker` | Verifies a merkle proof against the inbox tree, mints project tokens for the beneficiary, and optionally adds terminal tokens to the project's balance. |
32
+ | `claim(claims[])` | `JBSucker` | Batch version of `claim`. |
33
+ | `mapToken(map)` | `JBSucker` | Maps a local terminal token to a remote token. Requires `MAP_SUCKER_TOKEN` permission. Setting `remoteToken` to `address(0)` disables bridging and sends remaining outbox. |
34
+ | `mapTokens(maps[])` | `JBSucker` | Batch version of `mapToken`. |
35
+ | `addOutstandingAmountToBalance(token)` | `JBSucker` | Manually adds received terminal tokens to the project's balance (only when `ADD_TO_BALANCE_MODE == MANUAL`). |
36
+ | `enableEmergencyHatchFor(tokens)` | `JBSucker` | Opens emergency hatch for specified tokens (irreversible). Requires `SUCKER_SAFETY` permission. |
37
+ | `exitThroughEmergencyHatch(claimData)` | `JBSucker` | Lets users reclaim tokens on the chain they deposited, using their outbox proof. Only works when emergency hatch is open or sucker is deprecated. |
38
+ | `setDeprecation(timestamp)` | `JBSucker` | Sets when the sucker becomes fully deprecated. Must be at least `_maxMessagingDelay()` (14 days) in the future. Requires `SUCKER_SAFETY` permission. |
39
+ | `ccipReceive(any2EvmMessage)` | `JBCCIPSucker` | CCIP entry point. Validates router + peer + chain selector, unwraps native tokens if needed, calls `fromRemote`. |
40
+ | `deploySuckersFor(projectId, salt, configurations)` | `JBSuckerRegistry` | Deploys one or more suckers for a project. Requires `DEPLOY_SUCKERS` permission. Salt must match on both chains for peer pairing. |
41
+ | `allowSuckerDeployer(deployer)` | `JBSuckerRegistry` | Adds a deployer to the allowlist. Owner-only. |
42
+ | `removeDeprecatedSucker(projectId, sucker)` | `JBSuckerRegistry` | Removes a deprecated sucker from the registry. Callable by anyone. |
43
+ | `createForSender(localProjectId, salt)` | `JBSuckerDeployer` | Clones the singleton sucker deterministically and initializes it with the project ID. |
44
+ | `setChainSpecificConstants(...)` | Deployers | One-time configuration of bridge-specific addresses. Callable only by `LAYER_SPECIFIC_CONFIGURATOR`. |
45
+
46
+ ## Integration Points
47
+
48
+ | Dependency | Import | Used For |
49
+ |------------|--------|----------|
50
+ | `@bananapus/core-v5` | `IJBDirectory`, `IJBController`, `IJBTokens`, `IJBTerminal`, `IJBCashOutTerminal`, `IJBPayoutTerminal` | Project lookup, token minting/burning, cash-outs, `addToBalanceOf` |
51
+ | `@bananapus/core-v5` | `JBConstants` | `NATIVE_TOKEN` sentinel address |
52
+ | `@bananapus/permission-ids-v5` | `JBPermissionIds` | `MAP_SUCKER_TOKEN`, `DEPLOY_SUCKERS`, `SUCKER_SAFETY`, `MINT_TOKENS` permission IDs |
53
+ | `@chainlink/contracts-ccip` | `Client`, `IAny2EVMMessageReceiver` | CCIP message encoding/decoding, receiver interface |
54
+ | `@arbitrum/nitro-contracts` | `IInbox`, `IOutbox`, `IBridge`, `ArbSys`, `AddressAliasHelper` | Arbitrum retryable tickets, L2->L1 messages, address aliasing |
55
+ | `@openzeppelin/contracts` | `SafeERC20`, `BitMaps`, `ERC165`, `Initializable`, `Ownable`, `ERC2771Context`, `EnumerableMap` | Token safety, leaf tracking, clone init, registry ownership, meta-transactions |
56
+ | `solady` | `LibClone` | Deterministic minimal proxy deployment |
57
+ | `@prb/math` | `mulDiv` | Safe fixed-point multiplication |
58
+
59
+ ## Key Types
60
+
61
+ | Struct/Enum | Key Fields | Used In |
62
+ |-------------|------------|---------|
63
+ | `JBClaim` | `token`, `leaf` (`JBLeaf`), `proof` (`bytes32[32]`) | `claim`, `exitThroughEmergencyHatch` |
64
+ | `JBLeaf` | `index`, `beneficiary`, `projectTokenCount`, `terminalTokenAmount` | Merkle tree leaves (outbox/inbox) |
65
+ | `JBOutboxTree` | `nonce` (uint64), `balance`, `tree` (MerkleLib.Tree), `numberOfClaimsSent` | Per-token outbox state in `JBSucker` |
66
+ | `JBInboxTreeRoot` | `nonce` (uint64), `root` (bytes32) | Per-token inbox state in `JBSucker` |
67
+ | `JBMessageRoot` | `token`, `amount`, `remoteRoot` (`JBInboxTreeRoot`) | Cross-chain message payload |
68
+ | `JBRemoteToken` | `enabled`, `emergencyHatch`, `minGas` (uint32), `addr`, `minBridgeAmount` | Token mapping config |
69
+ | `JBTokenMapping` | `localToken`, `minGas` (uint32), `remoteToken`, `minBridgeAmount` | Input for `mapToken`/`mapTokens` |
70
+ | `JBSuckerDeployerConfig` | `deployer` (`IJBSuckerDeployer`), `mappings` (`JBTokenMapping[]`) | Input for `deploySuckersFor` |
71
+ | `JBSuckersPair` | `local`, `remote`, `remoteChainId` | Return type for `suckerPairsOf` |
72
+ | `JBAddToBalanceMode` | `MANUAL`, `ON_CLAIM` | Controls when received funds are added to project balance |
73
+ | `JBSuckerState` | `ENABLED`, `DEPRECATION_PENDING`, `SENDING_DISABLED`, `DEPRECATED` | Deprecation lifecycle |
74
+ | `JBLayer` | `L1`, `L2` | Arbitrum sucker layer identification |
75
+
76
+ ## Gotchas
77
+
78
+ - `using MerkleLib for MerkleLib.Tree` is NOT inherited by derived contracts -- must redeclare in test harnesses.
79
+ - 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.
80
+ - 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))`.
81
+ - `peer()` defaults to `address(this)` -- suckers expect to be deployed at matching addresses on both chains via deterministic deployment.
82
+ - `fromRemote` is `external payable` and does NOT revert on stale nonce or deprecated state -- it silently ignores the update to avoid losing native tokens sent along with the message.
83
+ - `JBCCIPSucker.ccipReceive` calls `this.fromRemote(root)` (external self-call) so that `_isRemotePeer` sees `msg.sender == address(this)` rather than the CCIP router.
84
+ - `_validateTokenMapping` in `JBSucker` enforces that native token (`NATIVE_TOKEN`) can only map to `NATIVE_TOKEN` or `address(0)`. `JBCCIPSucker` overrides this to relax the native token constraint (remote chain may not have native ETH).
85
+ - Emergency hatch is irreversible per-token. Once opened, the token can never be bridged again by that sucker.
86
+ - `setDeprecation` requires the timestamp to be at least `_maxMessagingDelay()` (14 days) in the future. This ensures in-flight messages arrive before the sucker stops accepting them.
87
+ - Deployer `setChainSpecificConstants` and `configureSingleton` are both one-shot functions -- they revert if called twice.
88
+ - `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.
89
+ - `JBArbitrumSucker` uses `unsafeCreateRetryableTicket` (not `safeCreateRetryableTicket`) to avoid L2 address aliasing of the refund address.
90
+ - The outbox tree tracks `numberOfClaimsSent` separately from `tree.count`. Emergency hatch exit is only available for leaves whose index is `>= numberOfClaimsSent` (not yet sent to remote).
91
+
92
+ ### CRITICAL: NATIVE_TOKEN Mismatch on Non-ETH Chains
93
+
94
+ `JBConstants.NATIVE_TOKEN` (`0x000...EEEe`) represents whatever is native on the current chain -- ETH on Ethereum/Optimism/Base/Arbitrum, but CELO on Celo, MATIC on Polygon, etc.
95
+
96
+ **Mapping `NATIVE_TOKEN -> NATIVE_TOKEN` across chains with different native assets is dangerous:**
97
+
98
+ - The sucker bridges raw amounts without exchange rate conversion
99
+ - 1 CELO bridged as if it were 1 ETH massively overvalues the payment
100
+ - Project issuance (`baseCurrency=1`, i.e. ETH) treats CELO at 1:1 with ETH
101
+
102
+ **Safe chains** (ETH is native): Ethereum, Optimism, Base, Arbitrum -- `NATIVE_TOKEN -> NATIVE_TOKEN` works correctly.
103
+
104
+ **Unsafe chains** (non-ETH native): Celo, Polygon, Avalanche, BNB -- use ERC-20 WETH or USDC as the terminal accounting context, NOT `NATIVE_TOKEN`.
105
+
106
+ **Correct token mapping on Celo:**
107
+ ```solidity
108
+ // Map WETH (ERC-20) on Ethereum to WETH (ERC-20) on Celo
109
+ JBTokenMapping({
110
+ localToken: WETH_ETHEREUM,
111
+ remoteToken: WETH_CELO, // 0xD221812de1BD094f35587EE8E174B07B6167D9Af
112
+ minGas: 200_000,
113
+ minBridgeAmount: 0.01 ether
114
+ })
115
+ ```
116
+
117
+ **Wrong token mapping on Celo:**
118
+ ```solidity
119
+ // NATIVE_TOKEN on Ethereum is ETH, but on Celo it's CELO!
120
+ JBTokenMapping({
121
+ localToken: JBConstants.NATIVE_TOKEN,
122
+ remoteToken: JBConstants.NATIVE_TOKEN, // WRONG on non-ETH chains
123
+ minGas: 200_000,
124
+ minBridgeAmount: 0.01 ether
125
+ })
126
+ ```
127
+
128
+ See also: `SECURITY.md` in this repo and INTEROP-6 in `AUDIT_FINDINGS.md`.
129
+
130
+ ## Example Integration
131
+
132
+ ```solidity
133
+ // Deploy suckers for project 12 using CCIP to bridge ETH between Ethereum and Optimism
134
+ JBTokenMapping[] memory mappings = new JBTokenMapping[](1);
135
+ mappings[0] = JBTokenMapping({
136
+ localToken: JBConstants.NATIVE_TOKEN,
137
+ minGas: 200_000,
138
+ remoteToken: JBConstants.NATIVE_TOKEN,
139
+ minBridgeAmount: 0.025 ether
140
+ });
141
+
142
+ JBSuckerDeployerConfig[] memory configs = new JBSuckerDeployerConfig[](1);
143
+ configs[0] = JBSuckerDeployerConfig({
144
+ deployer: IJBSuckerDeployer(ccipSuckerDeployerAddress),
145
+ mappings: mappings
146
+ });
147
+
148
+ // Must use the same salt and caller on both chains
149
+ bytes32 salt = keccak256("my-project-suckers-v1");
150
+ address[] memory suckers = registry.deploySuckersFor(12, salt, configs);
151
+
152
+ // Grant the sucker MINT_TOKENS permission so it can mint on claim
153
+ uint256[] memory permissionIds = new uint256[](1);
154
+ permissionIds[0] = JBPermissionIds.MINT_TOKENS;
155
+ permissions.setPermissionsFor(
156
+ projectOwner,
157
+ JBPermissionsData({
158
+ operator: suckers[0],
159
+ projectId: 12,
160
+ permissionIds: permissionIds
161
+ })
162
+ );
163
+ ```