@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,676 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
+ import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
9
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
10
+ import {LibClone} from "solady/src/utils/LibClone.sol";
11
+
12
+ import "../src/JBSucker.sol";
13
+ import {JBAddToBalanceMode} from "../src/enums/JBAddToBalanceMode.sol";
14
+ import {JBInboxTreeRoot} from "../src/structs/JBInboxTreeRoot.sol";
15
+ import {JBMessageRoot} from "../src/structs/JBMessageRoot.sol";
16
+ import {JBRemoteToken} from "../src/structs/JBRemoteToken.sol";
17
+ import {MerkleLib} from "../src/utils/MerkleLib.sol";
18
+
19
+ /// @notice Test harness that exposes JBSucker internals for interop testing.
20
+ /// @dev Extends JBSucker to expose _buildTreeHash, _toBytes32, _toAddress, _insertIntoTree,
21
+ /// and merkle tree state without the _validateBranchRoot bypass from AttackTestSucker.
22
+ contract InteropTestSucker is JBSucker {
23
+ using MerkleLib for MerkleLib.Tree;
24
+
25
+ constructor(
26
+ IJBDirectory directory,
27
+ IJBPermissions permissions,
28
+ IJBTokens tokens,
29
+ JBAddToBalanceMode addToBalanceMode,
30
+ address forwarder
31
+ )
32
+ JBSucker(directory, permissions, tokens, addToBalanceMode, forwarder)
33
+ {}
34
+
35
+ function _sendRootOverAMB(
36
+ uint256,
37
+ uint256,
38
+ address,
39
+ uint256,
40
+ JBRemoteToken memory,
41
+ JBMessageRoot memory
42
+ )
43
+ internal
44
+ override
45
+ {}
46
+
47
+ function _isRemotePeer(address) internal pure override returns (bool) {
48
+ return false;
49
+ }
50
+
51
+ function peerChainId() external pure override returns (uint256) {
52
+ return 1;
53
+ }
54
+
55
+ // --- Exposed internals ---
56
+
57
+ function exposed_buildTreeHash(
58
+ uint256 projectTokenCount,
59
+ uint256 terminalTokenAmount,
60
+ bytes32 beneficiary
61
+ )
62
+ external
63
+ pure
64
+ returns (bytes32)
65
+ {
66
+ return _buildTreeHash(projectTokenCount, terminalTokenAmount, beneficiary);
67
+ }
68
+
69
+ function exposed_toBytes32(address addr) external pure returns (bytes32) {
70
+ return _toBytes32(addr);
71
+ }
72
+
73
+ function exposed_toAddress(bytes32 remote) external pure returns (address) {
74
+ return _toAddress(remote);
75
+ }
76
+
77
+ function exposed_insertIntoTree(
78
+ uint256 projectTokenCount,
79
+ address token,
80
+ uint256 terminalTokenAmount,
81
+ bytes32 beneficiary
82
+ )
83
+ external
84
+ {
85
+ _insertIntoTree(projectTokenCount, token, terminalTokenAmount, beneficiary);
86
+ }
87
+
88
+ function exposed_getOutboxRoot(address token) external view returns (bytes32) {
89
+ return _outboxOf[token].tree.root();
90
+ }
91
+
92
+ function exposed_getOutboxCount(address token) external view returns (uint256) {
93
+ return _outboxOf[token].tree.count;
94
+ }
95
+
96
+ function exposed_getOutboxBranch(address token) external view returns (bytes32[32] memory) {
97
+ bytes32[32] memory branch;
98
+ for (uint256 i; i < 32; i++) {
99
+ branch[i] = _outboxOf[token].tree.branch[i];
100
+ }
101
+ return branch;
102
+ }
103
+
104
+ function exposed_setInboxRoot(address token, uint64 nonce, bytes32 root) external {
105
+ _inboxOf[token] = JBInboxTreeRoot({nonce: nonce, root: root});
106
+ }
107
+
108
+ function exposed_validateBranchRoot(
109
+ bytes32 expectedRoot,
110
+ uint256 projectTokenCount,
111
+ uint256 terminalTokenAmount,
112
+ bytes32 beneficiary,
113
+ uint256 index,
114
+ bytes32[32] calldata leaves
115
+ )
116
+ external
117
+ {
118
+ _validateBranchRoot(expectedRoot, projectTokenCount, terminalTokenAmount, beneficiary, index, leaves);
119
+ }
120
+ }
121
+
122
+ /// @title InteropCompat
123
+ /// @notice Cross-VM interoperability tests proving that EVM and SVM suckers produce
124
+ /// identical leaf hashes, merkle trees, address encodings, and message formats.
125
+ /// These tests mock the SVM side in pure Solidity by replicating the exact byte
126
+ /// layout that the SVM Rust code uses.
127
+ contract InteropCompat is Test {
128
+ using MerkleLib for MerkleLib.Tree;
129
+
130
+ address constant DIRECTORY = address(600);
131
+ address constant PERMISSIONS = address(800);
132
+ address constant TOKENS = address(700);
133
+ address constant FORWARDER = address(1100);
134
+
135
+ InteropTestSucker sucker;
136
+
137
+ function setUp() public {
138
+ InteropTestSucker singleton = new InteropTestSucker(
139
+ IJBDirectory(DIRECTORY),
140
+ IJBPermissions(PERMISSIONS),
141
+ IJBTokens(TOKENS),
142
+ JBAddToBalanceMode.MANUAL,
143
+ FORWARDER
144
+ );
145
+
146
+ sucker = InteropTestSucker(payable(address(LibClone.cloneDeterministic(address(singleton), "interop"))));
147
+ sucker.initialize(1);
148
+ }
149
+
150
+ // =========================================================================
151
+ // Section 1: Leaf Hash Compatibility
152
+ // =========================================================================
153
+
154
+ /// @notice Replicate SVM build_tree_hash in Solidity and verify it matches _buildTreeHash.
155
+ /// @dev SVM constructs a 96-byte buffer:
156
+ /// [0..32] = projectTokenCount as uint256 big-endian (u128 zero-padded to 32 bytes)
157
+ /// [32..64] = terminalTokenAmount as uint256 big-endian (u128 zero-padded to 32 bytes)
158
+ /// [64..96] = beneficiary as bytes32
159
+ /// Then keccak256(buffer).
160
+ /// EVM's abi.encode(uint256, uint256, bytes32) produces the exact same layout.
161
+ function _svmBuildTreeHash(
162
+ uint256 projectTokenCount,
163
+ uint256 terminalTokenAmount,
164
+ bytes32 beneficiary
165
+ )
166
+ internal
167
+ pure
168
+ returns (bytes32)
169
+ {
170
+ // Manual 96-byte buffer matching SVM's build_tree_hash exactly
171
+ bytes memory data = new bytes(96);
172
+ assembly {
173
+ // data starts at data+32 (skip length prefix)
174
+ let ptr := add(data, 32)
175
+ // [0..32] = projectTokenCount as big-endian uint256
176
+ mstore(ptr, projectTokenCount)
177
+ // [32..64] = terminalTokenAmount as big-endian uint256
178
+ mstore(add(ptr, 32), terminalTokenAmount)
179
+ // [64..96] = beneficiary as bytes32
180
+ mstore(add(ptr, 64), beneficiary)
181
+ }
182
+ return keccak256(data);
183
+ }
184
+
185
+ function test_leafHash_evmAddress() public view {
186
+ address beneficiaryAddr = 0xdeAd000000000000000000000000000000001234;
187
+ bytes32 beneficiary = bytes32(uint256(uint160(beneficiaryAddr)));
188
+ uint256 projectTokens = 1000e18;
189
+ uint256 terminalTokens = 500e6;
190
+
191
+ bytes32 evmHash = sucker.exposed_buildTreeHash(projectTokens, terminalTokens, beneficiary);
192
+ bytes32 svmHash = _svmBuildTreeHash(projectTokens, terminalTokens, beneficiary);
193
+ assertEq(evmHash, svmHash, "EVM address leaf hash mismatch");
194
+ }
195
+
196
+ function test_leafHash_svmPubkey() public view {
197
+ // Full 32-byte SVM pubkey (all bytes used, high bits set)
198
+ bytes32 svmPubkey = 0xff01020304050607080910111213141516171819202122232425262728293031;
199
+ uint256 projectTokens = 42e18;
200
+ uint256 terminalTokens = 7e6;
201
+
202
+ bytes32 evmHash = sucker.exposed_buildTreeHash(projectTokens, terminalTokens, svmPubkey);
203
+ bytes32 svmHash = _svmBuildTreeHash(projectTokens, terminalTokens, svmPubkey);
204
+ assertEq(evmHash, svmHash, "SVM pubkey leaf hash mismatch");
205
+ }
206
+
207
+ function test_leafHash_uint128Max() public view {
208
+ uint256 maxU128 = type(uint128).max;
209
+ bytes32 beneficiary = bytes32(uint256(1));
210
+
211
+ bytes32 evmHash = sucker.exposed_buildTreeHash(maxU128, maxU128, beneficiary);
212
+ bytes32 svmHash = _svmBuildTreeHash(maxU128, maxU128, beneficiary);
213
+ assertEq(evmHash, svmHash, "u128 max leaf hash mismatch");
214
+ }
215
+
216
+ function test_leafHash_smallValues() public view {
217
+ bytes32 beneficiary = bytes32(uint256(uint160(address(1))));
218
+
219
+ bytes32 evmHash = sucker.exposed_buildTreeHash(1, 1, beneficiary);
220
+ bytes32 svmHash = _svmBuildTreeHash(1, 1, beneficiary);
221
+ assertEq(evmHash, svmHash, "Small value leaf hash mismatch");
222
+ }
223
+
224
+ function test_leafHash_zeroAmounts() public view {
225
+ bytes32 beneficiary = bytes32(uint256(uint160(address(0xBEEF))));
226
+
227
+ bytes32 evmHash = sucker.exposed_buildTreeHash(0, 0, beneficiary);
228
+ bytes32 svmHash = _svmBuildTreeHash(0, 0, beneficiary);
229
+ assertEq(evmHash, svmHash, "Zero amount leaf hash mismatch");
230
+ }
231
+
232
+ function testFuzz_leafHash_anyU128(uint128 projectTokens, uint128 terminalTokens, bytes32 beneficiary) public view {
233
+ bytes32 evmHash = sucker.exposed_buildTreeHash(uint256(projectTokens), uint256(terminalTokens), beneficiary);
234
+ bytes32 svmHash = _svmBuildTreeHash(uint256(projectTokens), uint256(terminalTokens), beneficiary);
235
+ assertEq(evmHash, svmHash, "Fuzz leaf hash mismatch");
236
+ }
237
+
238
+ // =========================================================================
239
+ // Section 2: Address Format Convention
240
+ // =========================================================================
241
+
242
+ function test_toBytes32_rightAligned() public view {
243
+ address addr = 0x1234567890AbcdEF1234567890aBcdef12345678;
244
+ bytes32 result = sucker.exposed_toBytes32(addr);
245
+
246
+ // Upper 12 bytes should be zero
247
+ for (uint256 i; i < 12; i++) {
248
+ assertEq(uint8(result[i]), 0, "Upper byte not zero");
249
+ }
250
+
251
+ // Lower 20 bytes should be the address
252
+ assertEq(address(uint160(uint256(result))), addr, "Lower 20 bytes mismatch");
253
+ }
254
+
255
+ function test_toAddress_extractsLower20() public view {
256
+ address addr = 0xdeAd000000000000000000000000000000001234;
257
+ bytes32 padded = bytes32(uint256(uint160(addr)));
258
+ address result = sucker.exposed_toAddress(padded);
259
+ assertEq(result, addr);
260
+ }
261
+
262
+ function test_roundTrip_addressToBytes32() public {
263
+ address original = makeAddr("roundTrip");
264
+ bytes32 asBytes32 = sucker.exposed_toBytes32(original);
265
+ address recovered = sucker.exposed_toAddress(asBytes32);
266
+ assertEq(recovered, original, "Round-trip failed");
267
+ }
268
+
269
+ function testFuzz_roundTrip(address addr) public view {
270
+ bytes32 asBytes32 = sucker.exposed_toBytes32(addr);
271
+ address recovered = sucker.exposed_toAddress(asBytes32);
272
+ assertEq(recovered, addr, "Fuzz round-trip failed");
273
+ }
274
+
275
+ function test_toAddress_svmPubkeyTruncates() public view {
276
+ // SVM pubkey with high bits set — _toAddress should truncate to lower 20 bytes
277
+ bytes32 svmPubkey = 0xFF01020304050607080910111213141516171819202122232425262728293031;
278
+ address result = sucker.exposed_toAddress(svmPubkey);
279
+ // Lower 20 bytes: 0x13141516171819202122232425262728293031XX
280
+ assertEq(result, address(uint160(uint256(svmPubkey))), "SVM pubkey truncation mismatch");
281
+ }
282
+
283
+ // =========================================================================
284
+ // Section 3: Merkle Tree Cross-Chain Proof
285
+ // =========================================================================
286
+
287
+ /// @notice Build a merkle tree with a single leaf, extract the proof, and verify it.
288
+ /// This simulates: SVM inserts leaf → SVM builds proof → EVM verifies proof.
289
+ function test_merkleProof_singleLeaf() public {
290
+ address token = address(0xAAAA);
291
+ uint256 projectTokens = 100e18;
292
+ uint256 terminalTokens = 50e6;
293
+ bytes32 beneficiary = bytes32(uint256(uint160(makeAddr("claimer"))));
294
+
295
+ // Insert a single leaf (simulates SVM prepare)
296
+ sucker.exposed_insertIntoTree(projectTokens, token, terminalTokens, beneficiary);
297
+
298
+ // Get the resulting root
299
+ bytes32 treeRoot = sucker.exposed_getOutboxRoot(token);
300
+ assertEq(sucker.exposed_getOutboxCount(token), 1, "Count should be 1");
301
+
302
+ // Build the proof for index 0 in a single-leaf tree.
303
+ // For a tree with 1 leaf at index 0, the proof is all Z_HASHES (sibling at each level is the zero hash).
304
+ bytes32[32] memory proof = _zeroProof();
305
+
306
+ // Compute the expected leaf hash
307
+ bytes32 leafHash = _svmBuildTreeHash(projectTokens, terminalTokens, beneficiary);
308
+
309
+ // Verify using MerkleLib.branchRoot
310
+ bytes32 computedRoot = MerkleLib.branchRoot(leafHash, proof, 0);
311
+ assertEq(computedRoot, treeRoot, "Merkle proof verification failed for single leaf");
312
+
313
+ // Also verify via the sucker's _validateBranchRoot (should not revert)
314
+ sucker.exposed_setInboxRoot(token, 1, treeRoot);
315
+ sucker.exposed_validateBranchRoot(treeRoot, projectTokens, terminalTokens, beneficiary, 0, proof);
316
+ }
317
+
318
+ /// @notice Build a tree with 4 leaves, verify proof for each leaf.
319
+ /// Uses a power-of-2 count to make proof construction straightforward.
320
+ function test_merkleProof_multipleLeaves() public {
321
+ address token = address(0xBBBB);
322
+
323
+ // 4 leaves (power of 2 for clean tree structure)
324
+ bytes32[4] memory leafHashes;
325
+ uint256[4] memory projectTokens = [uint256(100e18), uint256(200e18), uint256(300e18), uint256(400e18)];
326
+ uint256[4] memory terminalTokens = [uint256(50e6), uint256(100e6), uint256(150e6), uint256(200e6)];
327
+ bytes32[4] memory beneficiaries = [
328
+ bytes32(uint256(uint160(makeAddr("user1")))),
329
+ bytes32(uint256(uint160(makeAddr("user2")))),
330
+ bytes32(uint256(uint160(makeAddr("user3")))),
331
+ bytes32(uint256(uint160(makeAddr("user4"))))
332
+ ];
333
+
334
+ for (uint256 i; i < 4; i++) {
335
+ sucker.exposed_insertIntoTree(projectTokens[i], token, terminalTokens[i], beneficiaries[i]);
336
+ leafHashes[i] = _svmBuildTreeHash(projectTokens[i], terminalTokens[i], beneficiaries[i]);
337
+ }
338
+
339
+ bytes32 treeRoot = sucker.exposed_getOutboxRoot(token);
340
+ assertEq(sucker.exposed_getOutboxCount(token), 4, "Count should be 4");
341
+
342
+ // Build proofs manually for a 4-leaf tree:
343
+ // Level 0 pairs: (leaf0, leaf1), (leaf2, leaf3)
344
+ // Level 1 pairs: (hash01, hash23)
345
+ // Level 2+: (root, Z_i)
346
+ bytes32 hash01 = keccak256(abi.encodePacked(leafHashes[0], leafHashes[1]));
347
+ bytes32 hash23 = keccak256(abi.encodePacked(leafHashes[2], leafHashes[3]));
348
+
349
+ // Verify leaf 0 (index=0, binary=...00)
350
+ {
351
+ bytes32[32] memory proof = _zeroProofFrom(2);
352
+ proof[0] = leafHashes[1]; // sibling at level 0
353
+ proof[1] = hash23; // sibling at level 1
354
+ bytes32 computedRoot = MerkleLib.branchRoot(leafHashes[0], proof, 0);
355
+ assertEq(computedRoot, treeRoot, "Proof failed for leaf 0");
356
+ }
357
+
358
+ // Verify leaf 1 (index=1, binary=...01)
359
+ {
360
+ bytes32[32] memory proof = _zeroProofFrom(2);
361
+ proof[0] = leafHashes[0]; // sibling at level 0
362
+ proof[1] = hash23; // sibling at level 1
363
+ bytes32 computedRoot = MerkleLib.branchRoot(leafHashes[1], proof, 1);
364
+ assertEq(computedRoot, treeRoot, "Proof failed for leaf 1");
365
+ }
366
+
367
+ // Verify leaf 2 (index=2, binary=...10)
368
+ {
369
+ bytes32[32] memory proof = _zeroProofFrom(2);
370
+ proof[0] = leafHashes[3]; // sibling at level 0
371
+ proof[1] = hash01; // sibling at level 1
372
+ bytes32 computedRoot = MerkleLib.branchRoot(leafHashes[2], proof, 2);
373
+ assertEq(computedRoot, treeRoot, "Proof failed for leaf 2");
374
+ }
375
+
376
+ // Verify leaf 3 (index=3, binary=...11)
377
+ {
378
+ bytes32[32] memory proof = _zeroProofFrom(2);
379
+ proof[0] = leafHashes[2]; // sibling at level 0
380
+ proof[1] = hash01; // sibling at level 1
381
+ bytes32 computedRoot = MerkleLib.branchRoot(leafHashes[3], proof, 3);
382
+ assertEq(computedRoot, treeRoot, "Proof failed for leaf 3");
383
+ }
384
+ }
385
+
386
+ /// @notice SVM-style leaf hash inserted into EVM tree, proof verified via branchRoot.
387
+ /// This is the core cross-chain scenario: SVM prepares → EVM claims.
388
+ function test_merkleProof_svmBeneficiaryCrossChain() public {
389
+ address token = address(0xCCCC);
390
+ bytes32 svmPubkey = 0xABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789;
391
+ uint256 projectTokens = 500e18;
392
+ uint256 terminalTokens = 250e6;
393
+
394
+ // SVM inserts with full 32-byte pubkey beneficiary
395
+ sucker.exposed_insertIntoTree(projectTokens, token, terminalTokens, svmPubkey);
396
+
397
+ bytes32 treeRoot = sucker.exposed_getOutboxRoot(token);
398
+
399
+ // Build proof and verify using SVM-style hash
400
+ bytes32[32] memory proof = _zeroProof();
401
+ bytes32 leafHash = _svmBuildTreeHash(projectTokens, terminalTokens, svmPubkey);
402
+ bytes32 computedRoot = MerkleLib.branchRoot(leafHash, proof, 0);
403
+ assertEq(computedRoot, treeRoot, "Cross-chain SVM beneficiary proof failed");
404
+ }
405
+
406
+ // =========================================================================
407
+ // Section 4: MessageRoot Encoding
408
+ // =========================================================================
409
+
410
+ /// @notice Verify abi.encode(JBMessageRoot) layout matches SVM's expected field positions.
411
+ /// @dev SVM MessageRoot: { version: u8, token: [u8;32], amount: u128, nonce: u64, root: [u8;32] }
412
+ /// EVM abi.encode packs each field into 32-byte slots:
413
+ /// Slot 0: version (uint8, right-aligned in 32 bytes)
414
+ /// Slot 1: token (bytes32)
415
+ /// Slot 2: amount (uint256)
416
+ /// Slot 3: remoteRoot.nonce (uint64, right-aligned in 32 bytes)
417
+ /// Slot 4: remoteRoot.root (bytes32)
418
+ function test_messageRoot_encoding() public pure {
419
+ JBMessageRoot memory msg_ = JBMessageRoot({
420
+ version: 1,
421
+ token: bytes32(uint256(0xAABBCCDD)),
422
+ amount: 1000e18,
423
+ remoteRoot: JBInboxTreeRoot({nonce: 42, root: bytes32(uint256(0x1234))})
424
+ });
425
+
426
+ bytes memory encoded = abi.encode(msg_);
427
+
428
+ // Each field is 32 bytes in abi.encode
429
+ // Slot 0: version
430
+ uint8 decodedVersion;
431
+ assembly {
432
+ decodedVersion := mload(add(encoded, 32))
433
+ }
434
+ assertEq(decodedVersion, 1, "Version mismatch");
435
+
436
+ // Slot 1: token
437
+ bytes32 decodedToken;
438
+ assembly {
439
+ decodedToken := mload(add(encoded, 64))
440
+ }
441
+ assertEq(decodedToken, bytes32(uint256(0xAABBCCDD)), "Token mismatch");
442
+
443
+ // Slot 2: amount
444
+ uint256 decodedAmount;
445
+ assembly {
446
+ decodedAmount := mload(add(encoded, 96))
447
+ }
448
+ assertEq(decodedAmount, 1000e18, "Amount mismatch");
449
+
450
+ // Slot 3: nonce (part of JBInboxTreeRoot)
451
+ uint64 decodedNonce;
452
+ assembly {
453
+ decodedNonce := mload(add(encoded, 128))
454
+ }
455
+ assertEq(decodedNonce, 42, "Nonce mismatch");
456
+
457
+ // Slot 4: root (part of JBInboxTreeRoot)
458
+ bytes32 decodedRoot;
459
+ assembly {
460
+ decodedRoot := mload(add(encoded, 160))
461
+ }
462
+ assertEq(decodedRoot, bytes32(uint256(0x1234)), "Root mismatch");
463
+ }
464
+
465
+ function test_messageRoot_versionConstant() public view {
466
+ assertEq(sucker.MESSAGE_VERSION(), 1, "MESSAGE_VERSION should be 1");
467
+ }
468
+
469
+ function test_messageRoot_amountFitsU128() public pure {
470
+ // Verify that amounts up to uint128.max can be encoded
471
+ JBMessageRoot memory msg_ = JBMessageRoot({
472
+ version: 1,
473
+ token: bytes32(0),
474
+ amount: type(uint128).max,
475
+ remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(0)})
476
+ });
477
+
478
+ bytes memory encoded = abi.encode(msg_);
479
+ uint256 decodedAmount;
480
+ assembly {
481
+ decodedAmount := mload(add(encoded, 96))
482
+ }
483
+ assertEq(decodedAmount, type(uint128).max, "u128 max amount encoding mismatch");
484
+ // SVM reads this as u128 — the upper 128 bits must be zero
485
+ assertTrue(decodedAmount <= type(uint128).max, "Amount exceeds u128");
486
+ }
487
+
488
+ // =========================================================================
489
+ // Section 5: uint128 Boundary
490
+ // =========================================================================
491
+
492
+ function test_uint128_exactMax_works() public {
493
+ address token = address(0xDDDD);
494
+ uint256 maxU128 = type(uint128).max;
495
+ bytes32 beneficiary = bytes32(uint256(1));
496
+
497
+ // Should not revert
498
+ sucker.exposed_insertIntoTree(maxU128, token, maxU128, beneficiary);
499
+ assertEq(sucker.exposed_getOutboxCount(token), 1, "Insert at u128 max should succeed");
500
+ }
501
+
502
+ function test_uint128_overflow_projectTokens_reverts() public {
503
+ address token = address(0xEEEE);
504
+ uint256 overflow = uint256(type(uint128).max) + 1;
505
+ bytes32 beneficiary = bytes32(uint256(1));
506
+
507
+ vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_AmountExceedsUint128.selector, overflow));
508
+ sucker.exposed_insertIntoTree(overflow, token, 100, beneficiary);
509
+ }
510
+
511
+ function test_uint128_overflow_terminalTokens_reverts() public {
512
+ address token = address(0xFFFF);
513
+ uint256 overflow = uint256(type(uint128).max) + 1;
514
+ bytes32 beneficiary = bytes32(uint256(1));
515
+
516
+ vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_AmountExceedsUint128.selector, overflow));
517
+ sucker.exposed_insertIntoTree(100, token, overflow, beneficiary);
518
+ }
519
+
520
+ function testFuzz_uint128_validHashMatch(uint128 projectTokens, uint128 terminalTokens) public view {
521
+ bytes32 beneficiary = bytes32(uint256(uint160(address(0xBEEF))));
522
+ bytes32 evmHash = sucker.exposed_buildTreeHash(uint256(projectTokens), uint256(terminalTokens), beneficiary);
523
+ bytes32 svmHash = _svmBuildTreeHash(uint256(projectTokens), uint256(terminalTokens), beneficiary);
524
+ assertEq(evmHash, svmHash, "Fuzz u128 hash mismatch");
525
+ }
526
+
527
+ // =========================================================================
528
+ // Section 6: Z_HASH Verification
529
+ // =========================================================================
530
+
531
+ function test_zHash_0() public pure {
532
+ assertEq(MerkleLib.Z_0, bytes32(0), "Z_0 should be zero");
533
+ }
534
+
535
+ function test_zHash_1() public pure {
536
+ bytes32 expected = keccak256(abi.encodePacked(bytes32(0), bytes32(0)));
537
+ assertEq(MerkleLib.Z_1, expected, "Z_1 mismatch");
538
+ }
539
+
540
+ function test_zHash_2() public pure {
541
+ bytes32 z1 = keccak256(abi.encodePacked(bytes32(0), bytes32(0)));
542
+ bytes32 expected = keccak256(abi.encodePacked(z1, z1));
543
+ assertEq(MerkleLib.Z_2, expected, "Z_2 mismatch");
544
+ }
545
+
546
+ function test_zHash_chain_to_5() public pure {
547
+ bytes32 z = bytes32(0);
548
+ for (uint256 i; i < 5; i++) {
549
+ z = keccak256(abi.encodePacked(z, z));
550
+ }
551
+ assertEq(MerkleLib.Z_5, z, "Z_5 mismatch");
552
+ }
553
+
554
+ function test_zHash_32_emptyTreeRoot() public pure {
555
+ // Z_32 is the root of a tree where all 2^32 leaves are zero.
556
+ // Compute iteratively.
557
+ bytes32 z = bytes32(0);
558
+ for (uint256 i; i < 32; i++) {
559
+ z = keccak256(abi.encodePacked(z, z));
560
+ }
561
+ assertEq(MerkleLib.Z_32, z, "Z_32 (empty tree root) mismatch");
562
+ }
563
+
564
+ function test_zHash_emptyTreeRootMatchesMerkleLib() public {
565
+ // An empty tree's root() should return Z_32.
566
+ // We can test this by reading the root of a tree with 0 insertions.
567
+ address token = address(0x9999);
568
+ bytes32 root = sucker.exposed_getOutboxRoot(token);
569
+ assertEq(root, MerkleLib.Z_32, "Empty tree root should be Z_32");
570
+ }
571
+
572
+ function test_zHashes_matchSVM() public pure {
573
+ // Spot-check several Z_HASHES against the SVM constants (from lib.rs).
574
+ // Z_1
575
+ assertEq(
576
+ MerkleLib.Z_1, hex"ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", "Z_1 SVM mismatch"
577
+ );
578
+ // Z_8
579
+ assertEq(
580
+ MerkleLib.Z_8, hex"9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af", "Z_8 SVM mismatch"
581
+ );
582
+ // Z_16
583
+ assertEq(
584
+ MerkleLib.Z_16, hex"2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f", "Z_16 SVM mismatch"
585
+ );
586
+ // Z_32
587
+ assertEq(
588
+ MerkleLib.Z_32, hex"27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757", "Z_32 SVM mismatch"
589
+ );
590
+ }
591
+
592
+ // =========================================================================
593
+ // Helpers
594
+ // =========================================================================
595
+
596
+ /// @dev Returns a proof of all zero hashes (valid for a single-leaf tree at index 0).
597
+ function _zeroProof() internal pure returns (bytes32[32] memory proof) {
598
+ proof[0] = MerkleLib.Z_0;
599
+ proof[1] = MerkleLib.Z_1;
600
+ proof[2] = MerkleLib.Z_2;
601
+ proof[3] = MerkleLib.Z_3;
602
+ proof[4] = MerkleLib.Z_4;
603
+ proof[5] = MerkleLib.Z_5;
604
+ proof[6] = MerkleLib.Z_6;
605
+ proof[7] = MerkleLib.Z_7;
606
+ proof[8] = MerkleLib.Z_8;
607
+ proof[9] = MerkleLib.Z_9;
608
+ proof[10] = MerkleLib.Z_10;
609
+ proof[11] = MerkleLib.Z_11;
610
+ proof[12] = MerkleLib.Z_12;
611
+ proof[13] = MerkleLib.Z_13;
612
+ proof[14] = MerkleLib.Z_14;
613
+ proof[15] = MerkleLib.Z_15;
614
+ proof[16] = MerkleLib.Z_16;
615
+ proof[17] = MerkleLib.Z_17;
616
+ proof[18] = MerkleLib.Z_18;
617
+ proof[19] = MerkleLib.Z_19;
618
+ proof[20] = MerkleLib.Z_20;
619
+ proof[21] = MerkleLib.Z_21;
620
+ proof[22] = MerkleLib.Z_22;
621
+ proof[23] = MerkleLib.Z_23;
622
+ proof[24] = MerkleLib.Z_24;
623
+ proof[25] = MerkleLib.Z_25;
624
+ proof[26] = MerkleLib.Z_26;
625
+ proof[27] = MerkleLib.Z_27;
626
+ proof[28] = MerkleLib.Z_28;
627
+ proof[29] = MerkleLib.Z_29;
628
+ proof[30] = MerkleLib.Z_30;
629
+ proof[31] = MerkleLib.Z_31;
630
+ }
631
+
632
+ /// @dev Returns a proof where levels [0, startLevel) are bytes32(0) (to be filled by caller)
633
+ /// and levels [startLevel, 32) are the appropriate Z_HASHES.
634
+ function _zeroProofFrom(uint256 startLevel) internal pure returns (bytes32[32] memory proof) {
635
+ for (uint256 i = startLevel; i < 32; i++) {
636
+ proof[i] = _zHashAt(i);
637
+ }
638
+ }
639
+
640
+ /// @dev Returns Z_HASH at level i.
641
+ function _zHashAt(uint256 i) internal pure returns (bytes32) {
642
+ if (i == 0) return MerkleLib.Z_0;
643
+ if (i == 1) return MerkleLib.Z_1;
644
+ if (i == 2) return MerkleLib.Z_2;
645
+ if (i == 3) return MerkleLib.Z_3;
646
+ if (i == 4) return MerkleLib.Z_4;
647
+ if (i == 5) return MerkleLib.Z_5;
648
+ if (i == 6) return MerkleLib.Z_6;
649
+ if (i == 7) return MerkleLib.Z_7;
650
+ if (i == 8) return MerkleLib.Z_8;
651
+ if (i == 9) return MerkleLib.Z_9;
652
+ if (i == 10) return MerkleLib.Z_10;
653
+ if (i == 11) return MerkleLib.Z_11;
654
+ if (i == 12) return MerkleLib.Z_12;
655
+ if (i == 13) return MerkleLib.Z_13;
656
+ if (i == 14) return MerkleLib.Z_14;
657
+ if (i == 15) return MerkleLib.Z_15;
658
+ if (i == 16) return MerkleLib.Z_16;
659
+ if (i == 17) return MerkleLib.Z_17;
660
+ if (i == 18) return MerkleLib.Z_18;
661
+ if (i == 19) return MerkleLib.Z_19;
662
+ if (i == 20) return MerkleLib.Z_20;
663
+ if (i == 21) return MerkleLib.Z_21;
664
+ if (i == 22) return MerkleLib.Z_22;
665
+ if (i == 23) return MerkleLib.Z_23;
666
+ if (i == 24) return MerkleLib.Z_24;
667
+ if (i == 25) return MerkleLib.Z_25;
668
+ if (i == 26) return MerkleLib.Z_26;
669
+ if (i == 27) return MerkleLib.Z_27;
670
+ if (i == 28) return MerkleLib.Z_28;
671
+ if (i == 29) return MerkleLib.Z_29;
672
+ if (i == 30) return MerkleLib.Z_30;
673
+ if (i == 31) return MerkleLib.Z_31;
674
+ return MerkleLib.Z_32;
675
+ }
676
+ }