@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
@@ -0,0 +1,622 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "forge-std/Test.sol";
5
+ import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
7
+
8
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
9
+ import {IJBSucker} from "../../src/interfaces/IJBSucker.sol";
10
+ import {IJBSuckerDeployer} from "../../src/interfaces/IJBSuckerDeployer.sol";
11
+
12
+ import "../../src/JBSucker.sol";
13
+ import "../../src/JBOptimismSucker.sol";
14
+ import "../../src/JBCCIPSucker.sol";
15
+ import "../../src/deployers/JBOptimismSuckerDeployer.sol";
16
+ import "../../src/deployers/JBCCIPSuckerDeployer.sol";
17
+ import {JBSuckerRegistry} from "../../src/JBSuckerRegistry.sol";
18
+ import {JBSuckerDeployerConfig} from "../../src/structs/JBSuckerDeployerConfig.sol";
19
+ import {JBSuckersPair} from "../../src/structs/JBSuckersPair.sol";
20
+ import {JBSuckerState} from "../../src/enums/JBSuckerState.sol";
21
+ import {JBTokenMapping} from "../../src/structs/JBTokenMapping.sol";
22
+ import {JBRemoteToken} from "../../src/structs/JBRemoteToken.sol";
23
+
24
+ /// @title MultiChainEvolutionTest
25
+ /// @notice Tests that a project can start on one chain and incrementally expand to new chains over time.
26
+ ///
27
+ /// Lifecycle story:
28
+ /// Phase 1: Project launches on Ethereum mainnet (no cross-chain).
29
+ /// Phase 2: Expand to Optimism — deploy OP sucker, map ETH (NATIVE_TOKEN -> NATIVE_TOKEN).
30
+ /// Phase 3: Add USDC bridging to Optimism — map USDC on the existing OP sucker.
31
+ /// Phase 4: Expand to Celo via CCIP — deploy CCIP sucker, map ETH as NATIVE_TOKEN -> celoETH (ERC-20).
32
+ /// Phase 5: Add USDC bridging to Celo — map USDC -> celoUSDC on the existing CCIP sucker.
33
+ /// Phase 6: Deprecate the OP sucker — Celo sucker continues working.
34
+ contract MultiChainEvolutionTest is Test, TestBaseWorkflow, IERC721Receiver {
35
+ JBSuckerRegistry registry;
36
+ uint256 projectId;
37
+
38
+ // Mock bridge/messenger addresses for OP.
39
+ IOPMessenger constant MOCK_OP_MESSENGER = IOPMessenger(address(0xA001));
40
+ IOPStandardBridge constant MOCK_OP_BRIDGE = IOPStandardBridge(address(0xA002));
41
+
42
+ // Mock CCIP router.
43
+ address constant MOCK_CCIP_ROUTER_ADDR = address(0xA003);
44
+
45
+ // Celo chain constants.
46
+ uint256 constant CELO_CHAIN_ID = 42_220;
47
+ uint64 constant CELO_CHAIN_SELECTOR = 1_311_226;
48
+
49
+ // Token addresses representing tokens on remote chains.
50
+ // On Celo, ETH is an ERC-20 (not native).
51
+ address celoETH = address(0xCE10E001);
52
+ // USDC addresses (local and remote).
53
+ address localUSDC = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
54
+ address opUSDC = address(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85);
55
+ address celoUSDC = address(0xef4229c8c3250C675F21BCefa42f58EfbfF6002a);
56
+
57
+ // Deployers (set in setUp).
58
+ JBOptimismSuckerDeployer opDeployer;
59
+ JBCCIPSuckerDeployer ccipDeployer;
60
+
61
+ function setUp() public override {
62
+ super.setUp();
63
+
64
+ vm.label(address(MOCK_OP_MESSENGER), "OP_MESSENGER");
65
+ vm.label(address(MOCK_OP_BRIDGE), "OP_BRIDGE");
66
+ vm.label(MOCK_CCIP_ROUTER_ADDR, "CCIP_ROUTER");
67
+
68
+ // Etch code at mock addresses so Solidity's extcodesize checks pass.
69
+ vm.etch(address(MOCK_OP_MESSENGER), hex"01");
70
+ vm.etch(address(MOCK_OP_BRIDGE), hex"01");
71
+ vm.etch(MOCK_CCIP_ROUTER_ADDR, hex"01");
72
+
73
+ // Deploy the registry.
74
+ registry = new JBSuckerRegistry(jbDirectory(), jbPermissions(), address(this), address(0));
75
+
76
+ // --- Set up OP deployer ---
77
+ opDeployer = new JBOptimismSuckerDeployer({
78
+ directory: jbDirectory(),
79
+ permissions: jbPermissions(),
80
+ tokens: jbTokens(),
81
+ configurator: address(this),
82
+ trustedForwarder: address(0)
83
+ });
84
+ opDeployer.setChainSpecificConstants(MOCK_OP_MESSENGER, MOCK_OP_BRIDGE);
85
+
86
+ JBOptimismSucker opSingleton = new JBOptimismSucker({
87
+ deployer: opDeployer,
88
+ directory: jbDirectory(),
89
+ permissions: jbPermissions(),
90
+ tokens: jbTokens(),
91
+ addToBalanceMode: JBAddToBalanceMode.MANUAL,
92
+ trustedForwarder: address(0)
93
+ });
94
+ opDeployer.configureSingleton(opSingleton);
95
+
96
+ // --- Set up CCIP deployer ---
97
+ ccipDeployer = new JBCCIPSuckerDeployer({
98
+ directory: jbDirectory(),
99
+ permissions: jbPermissions(),
100
+ tokens: jbTokens(),
101
+ configurator: address(this),
102
+ trustedForwarder: address(0)
103
+ });
104
+ ccipDeployer.setChainSpecificConstants({
105
+ remoteChainId: CELO_CHAIN_ID,
106
+ remoteChainSelector: CELO_CHAIN_SELECTOR,
107
+ router: ICCIPRouter(MOCK_CCIP_ROUTER_ADDR)
108
+ });
109
+
110
+ JBCCIPSucker ccipSingleton = new JBCCIPSucker({
111
+ deployer: ccipDeployer,
112
+ directory: jbDirectory(),
113
+ permissions: jbPermissions(),
114
+ tokens: jbTokens(),
115
+ addToBalanceMode: JBAddToBalanceMode.MANUAL,
116
+ trustedForwarder: address(0)
117
+ });
118
+ ccipDeployer.configureSingleton(ccipSingleton);
119
+
120
+ // Allow both deployers in the registry.
121
+ registry.allowSuckerDeployer(address(opDeployer));
122
+ registry.allowSuckerDeployer(address(ccipDeployer));
123
+
124
+ // --- Launch project ---
125
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
126
+ reservedPercent: 0,
127
+ cashOutTaxRate: 0,
128
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
129
+ pausePay: false,
130
+ pauseCreditTransfers: false,
131
+ allowOwnerMinting: true,
132
+ allowSetCustomToken: false,
133
+ allowTerminalMigration: false,
134
+ allowSetTerminals: false,
135
+ allowSetController: false,
136
+ allowAddAccountingContext: true,
137
+ allowAddPriceFeed: true,
138
+ ownerMustSendPayouts: false,
139
+ holdFees: false,
140
+ useTotalSurplusForCashOuts: true,
141
+ useDataHookForPay: false,
142
+ useDataHookForCashOut: false,
143
+ dataHook: address(0),
144
+ metadata: 0
145
+ });
146
+
147
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
148
+ rulesetConfigs[0].mustStartAtOrAfter = 0;
149
+ rulesetConfigs[0].duration = 0;
150
+ rulesetConfigs[0].weight = 1000 * 10 ** 18;
151
+ rulesetConfigs[0].weightCutPercent = 0;
152
+ rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
153
+ rulesetConfigs[0].metadata = metadata;
154
+ rulesetConfigs[0].splitGroups = new JBSplitGroup[](0);
155
+ rulesetConfigs[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
156
+
157
+ JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
158
+ tokensToAccept[0] = JBAccountingContext({
159
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
160
+ });
161
+
162
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
163
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokensToAccept});
164
+
165
+ projectId = jbController()
166
+ .launchProjectFor({
167
+ owner: address(this),
168
+ projectUri: "myproject",
169
+ rulesetConfigurations: rulesetConfigs,
170
+ terminalConfigurations: terminalConfigs,
171
+ memo: ""
172
+ });
173
+ }
174
+
175
+ // =========================================================================
176
+ // Full lifecycle: project starts on one chain, evolves to many
177
+ // =========================================================================
178
+
179
+ // Storage vars for lifecycle test (avoids stack-too-deep).
180
+ IJBSucker _opSucker;
181
+ IJBSucker _celoSucker;
182
+
183
+ function test_lifecycle_projectExpandsAcrossChainsOverTime() public {
184
+ // ---------------------------------------------------------------
185
+ // Phase 1: Project exists only on Ethereum mainnet.
186
+ // No suckers deployed yet.
187
+ // ---------------------------------------------------------------
188
+
189
+ assertEq(registry.suckersOf(projectId).length, 0, "Phase 1: no suckers yet");
190
+
191
+ // Grant the registry MAP_SUCKER_TOKEN permission so deploySuckersFor can call mapTokens.
192
+ _grantMapPermission(address(registry));
193
+
194
+ // ---------------------------------------------------------------
195
+ // Phase 2: Expand to Optimism.
196
+ // Deploy OP sucker. Map NATIVE_TOKEN -> NATIVE_TOKEN
197
+ // (both chains have ETH as native).
198
+ // ---------------------------------------------------------------
199
+ {
200
+ JBTokenMapping[] memory opMappings = new JBTokenMapping[](1);
201
+ opMappings[0] = JBTokenMapping({
202
+ localToken: JBConstants.NATIVE_TOKEN,
203
+ minGas: 200_000,
204
+ remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
205
+ minBridgeAmount: 0.01 ether
206
+ });
207
+
208
+ JBSuckerDeployerConfig[] memory opConfig = new JBSuckerDeployerConfig[](1);
209
+ opConfig[0] =
210
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(opDeployer)), mappings: opMappings});
211
+
212
+ _opSucker = IJBSucker(registry.deploySuckersFor(projectId, bytes32("op_salt"), opConfig)[0]);
213
+ }
214
+
215
+ assertEq(registry.suckersOf(projectId).length, 1, "Phase 2: one sucker (OP)");
216
+ assertTrue(registry.isSuckerOf(projectId, address(_opSucker)), "Phase 2: OP sucker registered");
217
+
218
+ JBRemoteToken memory opNative = _opSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN);
219
+ assertTrue(opNative.enabled, "Phase 2: native mapping enabled on OP");
220
+ assertEq(
221
+ opNative.addr, bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))), "Phase 2: native maps to native on OP"
222
+ );
223
+
224
+ // ---------------------------------------------------------------
225
+ // Phase 3: Add USDC bridging to Optimism.
226
+ // Call mapToken on the existing OP sucker.
227
+ // ---------------------------------------------------------------
228
+
229
+ _opSucker.mapToken(
230
+ JBTokenMapping({
231
+ localToken: localUSDC,
232
+ minGas: 200_000,
233
+ remoteToken: bytes32(uint256(uint160(opUSDC))),
234
+ minBridgeAmount: 1e6
235
+ })
236
+ );
237
+
238
+ assertTrue(_opSucker.remoteTokenFor(localUSDC).enabled, "Phase 3: USDC enabled on OP");
239
+ assertEq(
240
+ _opSucker.remoteTokenFor(localUSDC).addr, bytes32(uint256(uint160(opUSDC))), "Phase 3: USDC maps to opUSDC"
241
+ );
242
+ assertTrue(_opSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).enabled, "Phase 3: native still works");
243
+
244
+ // ---------------------------------------------------------------
245
+ // Phase 4: Expand to Celo via CCIP.
246
+ // Deploy CCIP sucker. Map NATIVE_TOKEN -> celoETH (ERC-20).
247
+ //
248
+ // THIS IS THE KEY CROSS-CHAIN NATIVE TOKEN INTEROP CASE:
249
+ // On Ethereum, ETH is the native token (NATIVE_TOKEN).
250
+ // On Celo, ETH is an ERC-20 (celoETH address).
251
+ // The CCIP sucker allows this mapping because it wraps
252
+ // native ETH -> WETH for CCIP transport, and the receiving
253
+ // side processes celoETH as an ERC-20 (no unwrap).
254
+ // ---------------------------------------------------------------
255
+ {
256
+ JBTokenMapping[] memory celoMappings = new JBTokenMapping[](1);
257
+ celoMappings[0] = JBTokenMapping({
258
+ localToken: JBConstants.NATIVE_TOKEN,
259
+ minGas: 200_000,
260
+ remoteToken: bytes32(uint256(uint160(celoETH))), // ERC-20 on Celo, NOT NATIVE_TOKEN
261
+ minBridgeAmount: 0.01 ether
262
+ });
263
+
264
+ JBSuckerDeployerConfig[] memory celoConfig = new JBSuckerDeployerConfig[](1);
265
+ celoConfig[0] =
266
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(ccipDeployer)), mappings: celoMappings});
267
+
268
+ _celoSucker = IJBSucker(registry.deploySuckersFor(projectId, bytes32("celo_salt"), celoConfig)[0]);
269
+ }
270
+
271
+ assertEq(registry.suckersOf(projectId).length, 2, "Phase 4: two suckers (OP + Celo)");
272
+ assertTrue(registry.isSuckerOf(projectId, address(_celoSucker)), "Phase 4: Celo sucker registered");
273
+ assertTrue(registry.isSuckerOf(projectId, address(_opSucker)), "Phase 4: OP sucker still registered");
274
+
275
+ JBRemoteToken memory celoNative = _celoSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN);
276
+ assertTrue(celoNative.enabled, "Phase 4: native mapping enabled on Celo");
277
+ assertEq(
278
+ celoNative.addr, bytes32(uint256(uint160(celoETH))), "Phase 4: native maps to celoETH (ERC-20) on Celo"
279
+ );
280
+ assertEq(registry.suckerPairsOf(projectId).length, 2, "Phase 4: two sucker pairs");
281
+
282
+ // ---------------------------------------------------------------
283
+ // Phase 5: Add USDC bridging to Celo.
284
+ // Call mapToken on the existing CCIP sucker.
285
+ // ---------------------------------------------------------------
286
+
287
+ _celoSucker.mapToken(
288
+ JBTokenMapping({
289
+ localToken: localUSDC,
290
+ minGas: 200_000,
291
+ remoteToken: bytes32(uint256(uint160(celoUSDC))),
292
+ minBridgeAmount: 1e6
293
+ })
294
+ );
295
+
296
+ assertTrue(_celoSucker.remoteTokenFor(localUSDC).enabled, "Phase 5: USDC enabled on Celo");
297
+ assertEq(
298
+ _celoSucker.remoteTokenFor(localUSDC).addr,
299
+ bytes32(uint256(uint160(celoUSDC))),
300
+ "Phase 5: USDC maps to celoUSDC"
301
+ );
302
+
303
+ // OP sucker is completely independent — its mappings unchanged.
304
+ assertEq(
305
+ _opSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).addr,
306
+ bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
307
+ "Phase 5: OP native mapping unchanged"
308
+ );
309
+ assertEq(
310
+ _opSucker.remoteTokenFor(localUSDC).addr,
311
+ bytes32(uint256(uint160(opUSDC))),
312
+ "Phase 5: OP USDC mapping unchanged"
313
+ );
314
+
315
+ // ---------------------------------------------------------------
316
+ // Phase 6: Deprecate the OP sucker.
317
+ // The Celo sucker continues working.
318
+ // ---------------------------------------------------------------
319
+
320
+ uint40 deprecationTime = uint40(block.timestamp + 14 days);
321
+ JBOptimismSucker(payable(address(_opSucker))).setDeprecation(deprecationTime);
322
+ vm.warp(deprecationTime);
323
+
324
+ assertEq(uint8(_opSucker.state()), uint8(JBSuckerState.DEPRECATED), "Phase 6: OP sucker deprecated");
325
+
326
+ registry.removeDeprecatedSucker(projectId, address(_opSucker));
327
+ assertEq(registry.suckersOf(projectId).length, 1, "Phase 6: only Celo sucker remains");
328
+
329
+ assertEq(uint8(_celoSucker.state()), uint8(JBSuckerState.ENABLED), "Phase 6: Celo still enabled");
330
+ assertTrue(
331
+ _celoSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).enabled, "Phase 6: Celo native mapping still works"
332
+ );
333
+ assertEq(
334
+ _celoSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).addr,
335
+ bytes32(uint256(uint160(celoETH))),
336
+ "Phase 6: Celo native still maps to celoETH"
337
+ );
338
+ }
339
+
340
+ // =========================================================================
341
+ // Focused: deploy suckers to multiple chains in one transaction
342
+ // =========================================================================
343
+
344
+ function test_canDeployToMultipleChainsAtOnce() public {
345
+ _grantMapPermission(address(registry));
346
+
347
+ // Deploy to both OP and Celo in a single deploySuckersFor call.
348
+ JBSuckerDeployerConfig[] memory configs = new JBSuckerDeployerConfig[](2);
349
+
350
+ JBTokenMapping[] memory opMappings = new JBTokenMapping[](1);
351
+ opMappings[0] = JBTokenMapping({
352
+ localToken: JBConstants.NATIVE_TOKEN,
353
+ minGas: 200_000,
354
+ remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
355
+ minBridgeAmount: 0.01 ether
356
+ });
357
+
358
+ JBTokenMapping[] memory celoMappings = new JBTokenMapping[](1);
359
+ celoMappings[0] = JBTokenMapping({
360
+ localToken: JBConstants.NATIVE_TOKEN,
361
+ minGas: 200_000,
362
+ remoteToken: bytes32(uint256(uint160(celoETH))),
363
+ minBridgeAmount: 0.01 ether
364
+ });
365
+
366
+ configs[0] = JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(opDeployer)), mappings: opMappings});
367
+ configs[1] =
368
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(ccipDeployer)), mappings: celoMappings});
369
+
370
+ address[] memory suckers = registry.deploySuckersFor(projectId, bytes32("both"), configs);
371
+
372
+ assertEq(suckers.length, 2, "Should deploy 2 suckers");
373
+ assertEq(registry.suckersOf(projectId).length, 2, "Registry should track 2 suckers");
374
+
375
+ // Verify each sucker has its own independent mapping.
376
+ IJBSucker opSucker = IJBSucker(suckers[0]);
377
+ IJBSucker celoSucker = IJBSucker(suckers[1]);
378
+
379
+ assertEq(
380
+ opSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).addr,
381
+ bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
382
+ "OP: native -> native"
383
+ );
384
+ assertEq(
385
+ celoSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).addr,
386
+ bytes32(uint256(uint160(celoETH))),
387
+ "Celo: native -> celoETH (ERC-20)"
388
+ );
389
+ }
390
+
391
+ // =========================================================================
392
+ // Focused: add tokens incrementally to an existing sucker
393
+ // =========================================================================
394
+
395
+ function test_canMapTokensIncrementallyToExistingSucker() public {
396
+ _grantMapPermission(address(registry));
397
+
398
+ // Deploy CCIP sucker with just native mapping.
399
+ JBTokenMapping[] memory initialMappings = new JBTokenMapping[](1);
400
+ initialMappings[0] = JBTokenMapping({
401
+ localToken: JBConstants.NATIVE_TOKEN,
402
+ minGas: 200_000,
403
+ remoteToken: bytes32(uint256(uint160(celoETH))),
404
+ minBridgeAmount: 0.01 ether
405
+ });
406
+
407
+ JBSuckerDeployerConfig[] memory config = new JBSuckerDeployerConfig[](1);
408
+ config[0] =
409
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(ccipDeployer)), mappings: initialMappings});
410
+
411
+ address[] memory suckers = registry.deploySuckersFor(projectId, bytes32("incr"), config);
412
+ IJBSucker sucker = IJBSucker(suckers[0]);
413
+
414
+ // Initially: only native mapped.
415
+ assertTrue(sucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).enabled, "Native should be mapped");
416
+ assertFalse(sucker.remoteTokenFor(localUSDC).enabled, "USDC should NOT be mapped yet");
417
+
418
+ // Later: project owner adds USDC.
419
+ sucker.mapToken(
420
+ JBTokenMapping({
421
+ localToken: localUSDC,
422
+ minGas: 200_000,
423
+ remoteToken: bytes32(uint256(uint160(celoUSDC))),
424
+ minBridgeAmount: 1e6
425
+ })
426
+ );
427
+
428
+ // Now both are mapped.
429
+ assertTrue(sucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).enabled, "Native still mapped");
430
+ assertTrue(sucker.remoteTokenFor(localUSDC).enabled, "USDC now mapped");
431
+ assertEq(sucker.remoteTokenFor(localUSDC).addr, bytes32(uint256(uint160(celoUSDC))), "USDC maps to celoUSDC");
432
+ }
433
+
434
+ // =========================================================================
435
+ // Focused: modifications to one sucker don't affect another
436
+ // =========================================================================
437
+
438
+ function test_suckerMappingsAreIndependent() public {
439
+ _grantMapPermission(address(registry));
440
+
441
+ // Deploy two suckers.
442
+ JBTokenMapping[] memory nativeMappings = new JBTokenMapping[](1);
443
+ nativeMappings[0] = JBTokenMapping({
444
+ localToken: JBConstants.NATIVE_TOKEN,
445
+ minGas: 200_000,
446
+ remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
447
+ minBridgeAmount: 0.01 ether
448
+ });
449
+
450
+ JBSuckerDeployerConfig[] memory opConfig = new JBSuckerDeployerConfig[](1);
451
+ opConfig[0] =
452
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(opDeployer)), mappings: nativeMappings});
453
+
454
+ JBTokenMapping[] memory celoNativeMappings = new JBTokenMapping[](1);
455
+ celoNativeMappings[0] = JBTokenMapping({
456
+ localToken: JBConstants.NATIVE_TOKEN,
457
+ minGas: 200_000,
458
+ remoteToken: bytes32(uint256(uint160(celoETH))),
459
+ minBridgeAmount: 0.01 ether
460
+ });
461
+
462
+ JBSuckerDeployerConfig[] memory celoConfig = new JBSuckerDeployerConfig[](1);
463
+ celoConfig[0] =
464
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(ccipDeployer)), mappings: celoNativeMappings});
465
+
466
+ address[] memory opSuckers = registry.deploySuckersFor(projectId, bytes32("ind_op"), opConfig);
467
+ address[] memory celoSuckers = registry.deploySuckersFor(projectId, bytes32("ind_celo"), celoConfig);
468
+
469
+ IJBSucker opSucker = IJBSucker(opSuckers[0]);
470
+ IJBSucker celoSucker = IJBSucker(celoSuckers[0]);
471
+
472
+ // Map USDC only on the Celo sucker.
473
+ celoSucker.mapToken(
474
+ JBTokenMapping({
475
+ localToken: localUSDC,
476
+ minGas: 200_000,
477
+ remoteToken: bytes32(uint256(uint160(celoUSDC))),
478
+ minBridgeAmount: 1e6
479
+ })
480
+ );
481
+
482
+ // OP sucker should NOT have USDC mapped.
483
+ assertFalse(opSucker.remoteTokenFor(localUSDC).enabled, "OP sucker should NOT have USDC");
484
+
485
+ // Celo sucker should have USDC mapped.
486
+ assertTrue(celoSucker.remoteTokenFor(localUSDC).enabled, "Celo sucker should have USDC");
487
+
488
+ // Disable native on OP sucker.
489
+ opSucker.mapToken(
490
+ JBTokenMapping({
491
+ localToken: JBConstants.NATIVE_TOKEN, minGas: 200_000, remoteToken: bytes32(0), minBridgeAmount: 0
492
+ })
493
+ );
494
+
495
+ // OP native disabled, Celo native unaffected.
496
+ assertFalse(opSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).enabled, "OP native disabled");
497
+ assertTrue(celoSucker.remoteTokenFor(JBConstants.NATIVE_TOKEN).enabled, "Celo native still enabled");
498
+ }
499
+
500
+ // =========================================================================
501
+ // Focused: CCIP sucker accepts NATIVE -> ERC20, OP sucker rejects it
502
+ // =========================================================================
503
+
504
+ function test_nativeToERC20_acceptedOnCCIP_rejectedOnOP() public {
505
+ _grantMapPermission(address(registry));
506
+
507
+ // Deploy both suckers with just a placeholder native->native mapping.
508
+ JBTokenMapping[] memory nativeMappings = new JBTokenMapping[](1);
509
+ nativeMappings[0] = JBTokenMapping({
510
+ localToken: JBConstants.NATIVE_TOKEN,
511
+ minGas: 200_000,
512
+ remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
513
+ minBridgeAmount: 0.01 ether
514
+ });
515
+
516
+ JBSuckerDeployerConfig[] memory opConfig = new JBSuckerDeployerConfig[](1);
517
+ opConfig[0] =
518
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(opDeployer)), mappings: nativeMappings});
519
+ address[] memory opSuckers = registry.deploySuckersFor(projectId, bytes32("natv_op"), opConfig);
520
+
521
+ // CCIP sucker: deploy with NATIVE -> celoETH (ERC-20). Should succeed.
522
+ JBTokenMapping[] memory celoEthMappings = new JBTokenMapping[](1);
523
+ celoEthMappings[0] = JBTokenMapping({
524
+ localToken: JBConstants.NATIVE_TOKEN,
525
+ minGas: 200_000,
526
+ remoteToken: bytes32(uint256(uint160(celoETH))),
527
+ minBridgeAmount: 0.01 ether
528
+ });
529
+
530
+ JBSuckerDeployerConfig[] memory celoConfig = new JBSuckerDeployerConfig[](1);
531
+ celoConfig[0] =
532
+ JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(ccipDeployer)), mappings: celoEthMappings});
533
+
534
+ // This should succeed — CCIP allows NATIVE -> ERC-20.
535
+ address[] memory celoSuckers = registry.deploySuckersFor(projectId, bytes32("natv_celo"), celoConfig);
536
+ assertEq(
537
+ IJBSucker(celoSuckers[0]).remoteTokenFor(JBConstants.NATIVE_TOKEN).addr,
538
+ bytes32(uint256(uint160(celoETH))),
539
+ "CCIP: NATIVE -> celoETH accepted"
540
+ );
541
+
542
+ // OP sucker: try to map NATIVE -> celoETH (ERC-20). Should REVERT.
543
+ vm.expectRevert(
544
+ abi.encodeWithSelector(
545
+ JBSucker.JBSucker_InvalidNativeRemoteAddress.selector, bytes32(uint256(uint160(celoETH)))
546
+ )
547
+ );
548
+ IJBSucker(opSuckers[0])
549
+ .mapToken(
550
+ JBTokenMapping({
551
+ localToken: JBConstants.NATIVE_TOKEN,
552
+ minGas: 200_000,
553
+ remoteToken: bytes32(uint256(uint160(celoETH))),
554
+ minBridgeAmount: 0.01 ether
555
+ })
556
+ );
557
+ }
558
+
559
+ // =========================================================================
560
+ // Focused: can replace a deprecated sucker with a new one
561
+ // =========================================================================
562
+
563
+ function test_canReplaceSuckerAfterDeprecation() public {
564
+ _grantMapPermission(address(registry));
565
+
566
+ // Deploy initial OP sucker.
567
+ JBTokenMapping[] memory mappings = new JBTokenMapping[](1);
568
+ mappings[0] = JBTokenMapping({
569
+ localToken: JBConstants.NATIVE_TOKEN,
570
+ minGas: 200_000,
571
+ remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
572
+ minBridgeAmount: 0.01 ether
573
+ });
574
+
575
+ JBSuckerDeployerConfig[] memory config = new JBSuckerDeployerConfig[](1);
576
+ config[0] = JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(opDeployer)), mappings: mappings});
577
+
578
+ address[] memory firstDeploy = registry.deploySuckersFor(projectId, bytes32("v1"), config);
579
+ address oldSucker = firstDeploy[0];
580
+ assertEq(registry.suckersOf(projectId).length, 1);
581
+
582
+ // Deprecate it.
583
+ uint40 deprecationTime = uint40(block.timestamp + 14 days);
584
+ JBOptimismSucker(payable(oldSucker)).setDeprecation(deprecationTime);
585
+ vm.warp(deprecationTime);
586
+ assertEq(uint8(IJBSucker(oldSucker).state()), uint8(JBSuckerState.DEPRECATED));
587
+
588
+ // Remove from registry.
589
+ registry.removeDeprecatedSucker(projectId, oldSucker);
590
+ assertEq(registry.suckersOf(projectId).length, 0, "Old sucker removed");
591
+
592
+ // Deploy a replacement sucker with a new salt.
593
+ address[] memory secondDeploy = registry.deploySuckersFor(projectId, bytes32("v2"), config);
594
+ address newSucker = secondDeploy[0];
595
+
596
+ assertEq(registry.suckersOf(projectId).length, 1, "New sucker deployed");
597
+ assertTrue(registry.isSuckerOf(projectId, newSucker), "New sucker registered");
598
+ assertFalse(registry.isSuckerOf(projectId, oldSucker), "Old sucker no longer registered");
599
+ assertTrue(newSucker != oldSucker, "New sucker has different address");
600
+ }
601
+
602
+ // =========================================================================
603
+ // Helpers
604
+ // =========================================================================
605
+
606
+ function _grantMapPermission(address operator) internal {
607
+ uint8[] memory permissions = new uint8[](1);
608
+ permissions[0] = JBPermissionIds.MAP_SUCKER_TOKEN;
609
+
610
+ jbPermissions()
611
+ .setPermissionsFor(
612
+ address(this),
613
+ JBPermissionsData({operator: operator, projectId: uint56(projectId), permissionIds: permissions})
614
+ );
615
+ }
616
+
617
+ function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
618
+ return IERC721Receiver.onERC721Received.selector;
619
+ }
620
+
621
+ receive() external payable {}
622
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
7
+ import "../../src/JBOptimismSucker.sol";
8
+ import "../../src/deployers/JBOptimismSuckerDeployer.sol";
9
+
10
+ import {JBLeaf} from "../../src/structs/JBLeaf.sol";
11
+ import {JBClaim} from "../../src/structs/JBClaim.sol";
12
+
13
+ import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
14
+ import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
15
+ import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
16
+
17
+ import {JBSuckerRegistry} from "./../../src/JBSuckerRegistry.sol";
18
+
19
+ contract RegistryUnitTest is Test {
20
+ function testDeployNoProjectCheck() public {
21
+ JBProjects _projects = new JBProjects(msg.sender, address(0), address(0));
22
+ JBPermissions _permissions = new JBPermissions(address(0));
23
+ JBDirectory _directory = new JBDirectory(_permissions, _projects, address(100));
24
+ new JBSuckerRegistry(_directory, _permissions, address(100), address(0));
25
+ }
26
+ }