@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,509 @@
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 {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
9
+ import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
10
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
11
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
12
+ import {LibClone} from "solady/src/utils/LibClone.sol";
13
+
14
+ import "../src/JBSucker.sol";
15
+ import {JBAddToBalanceMode} from "../src/enums/JBAddToBalanceMode.sol";
16
+ import {JBSuckerState} from "../src/enums/JBSuckerState.sol";
17
+ import {JBClaim} from "../src/structs/JBClaim.sol";
18
+ import {JBLeaf} from "../src/structs/JBLeaf.sol";
19
+ import {JBInboxTreeRoot} from "../src/structs/JBInboxTreeRoot.sol";
20
+ import {JBMessageRoot} from "../src/structs/JBMessageRoot.sol";
21
+ import {JBOutboxTree} from "../src/structs/JBOutboxTree.sol";
22
+ import {JBRemoteToken} from "../src/structs/JBRemoteToken.sol";
23
+ import {JBTokenMapping} from "../src/structs/JBTokenMapping.sol";
24
+ import {MerkleLib} from "../src/utils/MerkleLib.sol";
25
+
26
+ /// @notice A test sucker that exposes internals for attack testing.
27
+ contract AttackTestSucker is JBSucker {
28
+ using MerkleLib for MerkleLib.Tree;
29
+
30
+ bool nextCheckShouldPass;
31
+ bool public fromRemoteCalled;
32
+
33
+ constructor(
34
+ IJBDirectory directory,
35
+ IJBPermissions permissions,
36
+ IJBTokens tokens,
37
+ JBAddToBalanceMode addToBalanceMode,
38
+ address forwarder
39
+ )
40
+ JBSucker(directory, permissions, tokens, addToBalanceMode, forwarder)
41
+ {}
42
+
43
+ function _sendRootOverAMB(
44
+ uint256,
45
+ uint256,
46
+ address,
47
+ uint256,
48
+ JBRemoteToken memory,
49
+ JBMessageRoot memory
50
+ )
51
+ internal
52
+ override
53
+ {}
54
+
55
+ function _isRemotePeer(address sender) internal view override returns (bool) {
56
+ return sender == _toAddress(peer());
57
+ }
58
+
59
+ function peerChainId() external view virtual override returns (uint256) {
60
+ return block.chainid;
61
+ }
62
+
63
+ function _validateBranchRoot(
64
+ bytes32 expectedRoot,
65
+ uint256 projectTokenCount,
66
+ uint256 terminalTokenAmount,
67
+ bytes32 beneficiary,
68
+ uint256 index,
69
+ bytes32[_TREE_DEPTH] calldata leaves
70
+ )
71
+ internal
72
+ virtual
73
+ override
74
+ {
75
+ if (!nextCheckShouldPass) {
76
+ super._validateBranchRoot(expectedRoot, projectTokenCount, terminalTokenAmount, beneficiary, index, leaves);
77
+ }
78
+ nextCheckShouldPass = false;
79
+ }
80
+
81
+ // Test helpers
82
+ function test_setNextMerkleCheckToBe(bool _pass) external {
83
+ nextCheckShouldPass = _pass;
84
+ }
85
+
86
+ function test_setOutboxBalance(address token, uint256 amount) external {
87
+ _outboxOf[token].balance = amount;
88
+ }
89
+
90
+ function test_setInboxRoot(address token, uint64 nonce, bytes32 root) external {
91
+ _inboxOf[token] = JBInboxTreeRoot({nonce: nonce, root: root});
92
+ }
93
+
94
+ function test_insertIntoTree(
95
+ uint256 projectTokenCount,
96
+ address token,
97
+ uint256 terminalTokenAmount,
98
+ bytes32 beneficiary
99
+ )
100
+ external
101
+ {
102
+ _insertIntoTree(projectTokenCount, token, terminalTokenAmount, beneficiary);
103
+ }
104
+
105
+ function test_getOutboxRoot(address token) external view returns (bytes32) {
106
+ return _outboxOf[token].tree.root();
107
+ }
108
+
109
+ function test_getOutboxCount(address token) external view returns (uint256) {
110
+ return _outboxOf[token].tree.count;
111
+ }
112
+
113
+ function test_getOutboxNonce(address token) external view returns (uint64) {
114
+ return _outboxOf[token].nonce;
115
+ }
116
+
117
+ function test_getInboxRoot(address token) external view returns (bytes32) {
118
+ return _inboxOf[token].root;
119
+ }
120
+
121
+ function test_getInboxNonce(address token) external view returns (uint64) {
122
+ return _inboxOf[token].nonce;
123
+ }
124
+
125
+ function test_setRemoteToken(address localToken, JBRemoteToken memory remoteToken) external {
126
+ _remoteTokenFor[localToken] = remoteToken;
127
+ }
128
+ }
129
+
130
+ /// @title SuckerAttacks
131
+ /// @notice Attack tests for nana-suckers-v5 covering CCIP spoofing, double claims,
132
+ /// stale roots, token remapping, and merkle tree edge cases.
133
+ contract SuckerAttacks is Test {
134
+ using MerkleLib for MerkleLib.Tree;
135
+
136
+ address constant DIRECTORY = address(600);
137
+ address constant PERMISSIONS = address(800);
138
+ address constant TOKENS = address(700);
139
+ address constant CONTROLLER = address(900);
140
+ address constant PROJECT = address(1000);
141
+ address constant FORWARDER = address(1100);
142
+
143
+ uint256 constant PROJECT_ID = 1;
144
+
145
+ AttackTestSucker sucker;
146
+
147
+ function setUp() public {
148
+ vm.label(DIRECTORY, "MOCK_DIRECTORY");
149
+ vm.label(PERMISSIONS, "MOCK_PERMISSIONS");
150
+ vm.label(TOKENS, "MOCK_TOKENS");
151
+ vm.label(CONTROLLER, "MOCK_CONTROLLER");
152
+ vm.label(PROJECT, "MOCK_PROJECT");
153
+
154
+ sucker = _createTestSucker(PROJECT_ID, "attack_salt");
155
+
156
+ // Mock directory
157
+ vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.PROJECTS, ()), abi.encode(PROJECT));
158
+ vm.mockCall(PROJECT, abi.encodeCall(IERC721.ownerOf, (PROJECT_ID)), abi.encode(address(this)));
159
+ }
160
+
161
+ function _createTestSucker(uint256 projectId, bytes32 salt) internal returns (AttackTestSucker) {
162
+ AttackTestSucker singleton = new AttackTestSucker(
163
+ IJBDirectory(DIRECTORY),
164
+ IJBPermissions(PERMISSIONS),
165
+ IJBTokens(TOKENS),
166
+ JBAddToBalanceMode.MANUAL,
167
+ FORWARDER
168
+ );
169
+
170
+ AttackTestSucker clone =
171
+ AttackTestSucker(payable(address(LibClone.cloneDeterministic(address(singleton), salt))));
172
+ clone.initialize(projectId);
173
+ return clone;
174
+ }
175
+
176
+ // =========================================================================
177
+ // Test 1: ccipReceive — spoofed router (non-router calls ccipReceive)
178
+ // =========================================================================
179
+ /// @notice Non-router address calling fromRemote must revert with NotPeer.
180
+ /// @dev Verifies that only the initialized peer can call fromRemote.
181
+ function test_ccipReceive_spoofedRouter() public {
182
+ address spoofedRouter = makeAddr("spoofedRouter");
183
+
184
+ JBMessageRoot memory fakeRoot = JBMessageRoot({
185
+ version: 1,
186
+ token: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
187
+ amount: 1 ether,
188
+ remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(123))})
189
+ });
190
+
191
+ // Non-peer calling fromRemote should revert
192
+ vm.prank(spoofedRouter);
193
+ vm.expectRevert(
194
+ abi.encodeWithSelector(JBSucker.JBSucker_NotPeer.selector, bytes32(uint256(uint160(spoofedRouter))))
195
+ );
196
+ sucker.fromRemote(fakeRoot);
197
+ }
198
+
199
+ // =========================================================================
200
+ // Test 2: ccipReceive — wrong chain selector
201
+ // =========================================================================
202
+ /// @notice Valid peer but with wrong chain selector wouldn't be possible at fromRemote level,
203
+ /// but we verify that only the exact peer address can call fromRemote.
204
+ function test_ccipReceive_wrongChainSelector() public {
205
+ // Even if the message claims to be from the right chain, the caller must be the peer
206
+ address wrongSender = makeAddr("wrongPeer");
207
+
208
+ JBMessageRoot memory root = JBMessageRoot({
209
+ version: 1,
210
+ token: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
211
+ amount: 1 ether,
212
+ remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(456))})
213
+ });
214
+
215
+ vm.prank(wrongSender);
216
+ vm.expectRevert(
217
+ abi.encodeWithSelector(JBSucker.JBSucker_NotPeer.selector, bytes32(uint256(uint160(wrongSender))))
218
+ );
219
+ sucker.fromRemote(root);
220
+ }
221
+
222
+ // =========================================================================
223
+ // Test 3: ccipReceive — wrong peer address
224
+ // =========================================================================
225
+ /// @notice Verify that even a valid-looking address that isn't the peer gets rejected.
226
+ function test_ccipReceive_wrongPeerAddress() public {
227
+ // Create a second sucker that is NOT the peer
228
+ AttackTestSucker otherSucker = _createTestSucker(2, "other_salt");
229
+
230
+ JBMessageRoot memory root = JBMessageRoot({
231
+ version: 1,
232
+ token: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
233
+ amount: 0,
234
+ remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(789))})
235
+ });
236
+
237
+ // Other sucker calling fromRemote should be rejected
238
+ vm.prank(address(otherSucker));
239
+ vm.expectRevert(
240
+ abi.encodeWithSelector(JBSucker.JBSucker_NotPeer.selector, bytes32(uint256(uint160(address(otherSucker)))))
241
+ );
242
+ sucker.fromRemote(root);
243
+ }
244
+
245
+ // =========================================================================
246
+ // Test 4: ccipReceive — malformed message (garbage data)
247
+ // =========================================================================
248
+ /// @notice Valid source but garbage merkle root data. Verify no state corruption.
249
+ function test_ccipReceive_malformedMessage() public {
250
+ // Set the peer so we can call fromRemote from it
251
+ // First, initialize a peer
252
+ bytes32 peerAddr = sucker.peer();
253
+
254
+ // If no peer is set (bytes32(0)), fromRemote from any address will revert with NotPeer
255
+ // This is the expected behavior — only the peer can submit roots
256
+ if (peerAddr == bytes32(0)) {
257
+ JBMessageRoot memory root = JBMessageRoot({
258
+ version: 1,
259
+ token: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN))),
260
+ amount: 0,
261
+ remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(0)})
262
+ });
263
+
264
+ vm.expectRevert();
265
+ sucker.fromRemote(root);
266
+ }
267
+
268
+ // Verify inbox state was not corrupted
269
+ bytes32 inboxRoot = sucker.test_getInboxRoot(JBConstants.NATIVE_TOKEN);
270
+ assertEq(inboxRoot, bytes32(0), "Inbox root should remain empty after failed fromRemote");
271
+ }
272
+
273
+ // =========================================================================
274
+ // Test 5: claim — stale root (new root overwrites old, old proof fails)
275
+ // =========================================================================
276
+ /// @notice Root submitted, then new root overwrites it. Old proof should fail.
277
+ function test_claim_staleRoot() public {
278
+ address token = JBConstants.NATIVE_TOKEN;
279
+
280
+ // Insert items to create root 1
281
+ sucker.test_insertIntoTree(10 ether, token, 5 ether, bytes32(uint256(uint160(address(1000)))));
282
+
283
+ bytes32 root1 = sucker.test_getOutboxRoot(token);
284
+
285
+ // Set this as the inbox root (simulating receiving from remote)
286
+ sucker.test_setInboxRoot(token, 1, root1);
287
+
288
+ // Now insert more items to create root 2 (different root)
289
+ sucker.test_insertIntoTree(20 ether, token, 15 ether, bytes32(uint256(uint160(address(2000)))));
290
+ bytes32 root2 = sucker.test_getOutboxRoot(token);
291
+
292
+ // Overwrite inbox with root 2 (higher nonce)
293
+ sucker.test_setInboxRoot(token, 2, root2);
294
+
295
+ // Root 1 proof should no longer match the inbox root
296
+ // Any attempt to claim with root1's proof against root2 should fail
297
+ assertTrue(root1 != root2, "Roots should be different after new insertion");
298
+
299
+ // Verify inbox root is root2
300
+ bytes32 currentInbox = sucker.test_getInboxRoot(token);
301
+ assertEq(currentInbox, root2, "Inbox should have root2");
302
+ }
303
+
304
+ // =========================================================================
305
+ // Test 6: claim — double claim (same proof used twice)
306
+ // =========================================================================
307
+ /// @notice Same proof used twice. Second claim must revert.
308
+ function test_claim_doubleClaim() public {
309
+ address token = JBConstants.NATIVE_TOKEN;
310
+
311
+ // Insert a known item
312
+ sucker.test_insertIntoTree(5 ether, token, 5 ether, bytes32(uint256(uint160(address(120)))));
313
+
314
+ // Set the inbox root to match the outbox
315
+ bytes32 root = sucker.test_getOutboxRoot(token);
316
+ sucker.test_setInboxRoot(token, 1, root);
317
+ sucker.test_setOutboxBalance(token, 100 ether);
318
+
319
+ // Mock controller for minting
320
+ vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.controllerOf, (PROJECT_ID)), abi.encode(CONTROLLER));
321
+ vm.mockCall(
322
+ CONTROLLER,
323
+ abi.encodeCall(IJBController.mintTokensOf, (PROJECT_ID, 5 ether, address(120), "", false)),
324
+ abi.encode(5 ether)
325
+ );
326
+
327
+ // Make the first claim pass by overriding merkle validation
328
+ sucker.test_setNextMerkleCheckToBe(true);
329
+
330
+ bytes32[32] memory proof;
331
+ JBClaim memory claimData = JBClaim({
332
+ token: token,
333
+ leaf: JBLeaf({
334
+ index: 0,
335
+ beneficiary: bytes32(uint256(uint160(address(120)))),
336
+ projectTokenCount: 5 ether,
337
+ terminalTokenAmount: 5 ether
338
+ }),
339
+ proof: proof
340
+ });
341
+
342
+ // First claim succeeds
343
+ sucker.claim(claimData);
344
+
345
+ // Second claim with same index must revert (LeafAlreadyExecuted)
346
+ vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_LeafAlreadyExecuted.selector, token, 0));
347
+ sucker.claim(claimData);
348
+ }
349
+
350
+ // =========================================================================
351
+ // Test 7: claim after token remap (mapToken changes X→Y)
352
+ // =========================================================================
353
+ /// @notice Claim with token X proof after mapToken changes X→Y mapping.
354
+ function test_claim_afterTokenRemap() public {
355
+ address tokenX = makeAddr("tokenX");
356
+ address tokenY = makeAddr("tokenY");
357
+
358
+ // Set up a remote token mapping for tokenX
359
+ sucker.test_setRemoteToken(
360
+ tokenX,
361
+ JBRemoteToken({
362
+ enabled: true,
363
+ emergencyHatch: false,
364
+ minGas: 200_000,
365
+ addr: bytes32(uint256(uint160(makeAddr("remoteTokenX")))),
366
+ minBridgeAmount: 0
367
+ })
368
+ );
369
+
370
+ // Insert items for tokenX
371
+ sucker.test_insertIntoTree(10 ether, tokenX, 5 ether, bytes32(uint256(uint160(address(1000)))));
372
+ bytes32 rootX = sucker.test_getOutboxRoot(tokenX);
373
+
374
+ // Set inbox root for tokenX
375
+ sucker.test_setInboxRoot(tokenX, 1, rootX);
376
+
377
+ // Now remap tokenX → different remote token (simulating mapToken)
378
+ sucker.test_setRemoteToken(
379
+ tokenX,
380
+ JBRemoteToken({
381
+ enabled: true,
382
+ emergencyHatch: false,
383
+ minGas: 200_000,
384
+ addr: bytes32(uint256(uint160(makeAddr("remoteTokenY")))),
385
+ minBridgeAmount: 0
386
+ })
387
+ );
388
+
389
+ // The claim mechanism uses the inbox root which is keyed by token address,
390
+ // not by remote token mapping. So remapping the remote token doesn't invalidate
391
+ // existing inbox roots. Claims should still work for tokenX because the inbox
392
+ // root is stored per local token address.
393
+ assertEq(sucker.test_getInboxRoot(tokenX), rootX, "Inbox root for tokenX should persist after remap");
394
+ }
395
+
396
+ // =========================================================================
397
+ // Test 8: prepare then terminal changes
398
+ // =========================================================================
399
+ /// @notice Prepare claim, then project changes terminal. toRemote should handle gracefully.
400
+ function test_prepare_thenTerminalChanges() public {
401
+ // This tests the scenario where:
402
+ // 1. User prepares tokens for bridging (adds to outbox tree)
403
+ // 2. Project changes its terminal before toRemote is called
404
+ // The outbox tree items are already committed — terminal change doesn't affect them
405
+
406
+ address token = JBConstants.NATIVE_TOKEN;
407
+
408
+ // Insert items simulating prepare
409
+ sucker.test_insertIntoTree(10 ether, token, 5 ether, bytes32(uint256(uint160(address(1000)))));
410
+ sucker.test_insertIntoTree(20 ether, token, 10 ether, bytes32(uint256(uint160(address(2000)))));
411
+
412
+ uint256 count = sucker.test_getOutboxCount(token);
413
+ assertEq(count, 2, "Outbox should have 2 items");
414
+
415
+ // The outbox tree is independent of terminal configuration.
416
+ // Even if the terminal changes, the merkle root of committed items remains valid.
417
+ bytes32 rootBefore = sucker.test_getOutboxRoot(token);
418
+ assertTrue(rootBefore != bytes32(0), "Root should be non-zero after insertions");
419
+ }
420
+
421
+ // =========================================================================
422
+ // Test 9: emergency exit — balance manipulation
423
+ // =========================================================================
424
+ /// @notice Send tokens directly to sucker to inflate balanceOf, then try emergency exit.
425
+ function test_emergencyExit_balanceManipulation() public {
426
+ address token = JBConstants.NATIVE_TOKEN;
427
+
428
+ // Set the outbox balance to a known value
429
+ sucker.test_setOutboxBalance(token, 1 ether);
430
+
431
+ // Send extra ETH directly to the sucker (inflating its actual balance)
432
+ vm.deal(address(sucker), 10 ether);
433
+
434
+ // The outbox balance should still be 1 ether (tracked separately from actual balance)
435
+ // This means emergency exit should only allow withdrawal up to outbox balance,
436
+ // not the inflated actual balance.
437
+ // The sucker tracks outbox.balance independently of address(this).balance.
438
+
439
+ // Verify the outbox balance is the tracked amount, not the inflated amount
440
+ assertEq(address(sucker).balance, 10 ether, "Actual balance should be inflated");
441
+
442
+ // Try emergency exit with a claim that exceeds tracked outbox balance
443
+ // The sucker uses outbox.balance for validation, so large claims should fail
444
+
445
+ // Set up deprecated state for emergency exit
446
+ uint256 deprecationTimestamp = block.timestamp + 14 days;
447
+ sucker.setDeprecation(uint40(deprecationTimestamp));
448
+ vm.warp(deprecationTimestamp);
449
+
450
+ // Mock controller
451
+ vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.controllerOf, (PROJECT_ID)), abi.encode(CONTROLLER));
452
+ vm.mockCall(
453
+ CONTROLLER,
454
+ abi.encodeCall(IJBController.mintTokensOf, (PROJECT_ID, 1 ether, address(this), "", false)),
455
+ abi.encode(1 ether)
456
+ );
457
+
458
+ // Set up a valid claim with terminalTokenAmount == outbox balance
459
+ sucker.test_setNextMerkleCheckToBe(true);
460
+ bytes32[32] memory proof;
461
+ JBClaim memory claim = JBClaim({
462
+ token: token,
463
+ leaf: JBLeaf({
464
+ index: 0,
465
+ beneficiary: bytes32(uint256(uint160(address(this)))),
466
+ projectTokenCount: 1 ether,
467
+ terminalTokenAmount: 1 ether
468
+ }),
469
+ proof: proof
470
+ });
471
+
472
+ // Should work because claim amount <= tracked outbox balance
473
+ sucker.exitThroughEmergencyHatch(claim);
474
+ }
475
+
476
+ // =========================================================================
477
+ // Test 10: merkle tree — depth and count limits
478
+ // =========================================================================
479
+ /// @notice Verify the merkle tree handles many insertions correctly.
480
+ function test_merkleTree_depthOverflow() public {
481
+ address token = JBConstants.NATIVE_TOKEN;
482
+
483
+ // The tree depth is 32, supporting up to 2^32 - 1 leaves.
484
+ // We can't test 2^32 insertions (gas), but we can verify:
485
+ // 1. Multiple insertions produce valid roots
486
+ // 2. Each insertion changes the root
487
+
488
+ bytes32 prevRoot = sucker.test_getOutboxRoot(token);
489
+
490
+ // Insert several items and verify root changes each time
491
+ for (uint256 i = 0; i < 10; i++) {
492
+ sucker.test_insertIntoTree((i + 1) * 1 ether, token, (i + 1) * 0.5 ether, bytes32(uint256(1000 + i)));
493
+
494
+ bytes32 newRoot = sucker.test_getOutboxRoot(token);
495
+ assertTrue(newRoot != prevRoot, "Root should change after each insertion");
496
+ prevRoot = newRoot;
497
+ }
498
+
499
+ // Verify count
500
+ uint256 count = sucker.test_getOutboxCount(token);
501
+ assertEq(count, 10, "Count should be 10 after 10 insertions");
502
+
503
+ // Verify count is stored correctly as uint256
504
+ // (the MerkleLib.Tree.count field is uint256, not uint32)
505
+ assertTrue(count < type(uint256).max, "Count should be within uint256 range");
506
+ }
507
+
508
+ receive() external payable {}
509
+ }