@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,1159 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
5
+ import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
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 {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
10
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
11
+ import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
12
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
13
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
14
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
15
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
16
+ import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
17
+ import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
18
+ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
19
+ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
20
+ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
21
+
22
+ import {JBAddToBalanceMode} from "./enums/JBAddToBalanceMode.sol";
23
+ import {IJBSucker} from "./interfaces/IJBSucker.sol";
24
+ import {IJBSuckerExtended} from "./interfaces/IJBSuckerExtended.sol";
25
+ import {IJBSuckerDeployer} from "./interfaces/IJBSuckerDeployer.sol";
26
+ import {JBClaim} from "./structs/JBClaim.sol";
27
+ import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
28
+ import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
29
+ import {JBOutboxTree} from "./structs/JBOutboxTree.sol";
30
+ import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
31
+ import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
32
+ import {MerkleLib} from "./utils/MerkleLib.sol";
33
+ import {JBSuckerState} from "./enums/JBSuckerState.sol";
34
+
35
+ /// @notice An abstract contract for bridging a Juicebox project's tokens and the corresponding funds to and from a
36
+ /// remote chain.
37
+ /// @dev Beneficiaries and balances are tracked on two merkle trees: the outbox tree is used to send from the local
38
+ /// chain to the remote chain, and the inbox tree is used to receive from the remote chain to the local chain.
39
+ /// @dev Throughout this contract, "terminal token" refers to any token accepted by a project's terminal.
40
+ /// @dev This contract does *NOT* support tokens that have a fee on regular transfers and rebasing tokens.
41
+ /// @dev Cross-chain message authentication is delegated entirely to each bridge-specific subclass via the
42
+ /// `_isRemotePeer` virtual function. Each implementation authenticates differently: Optimism uses its native
43
+ /// `CrossDomainMessenger`, Arbitrum validates against the `Bridge` and `Outbox` contracts, and CCIP verifies
44
+ /// through the Chainlink `Router`. Deployers of new bridge integrations must implement `_isRemotePeer` to
45
+ /// guarantee that only messages from the legitimate remote peer are accepted.
46
+ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC165, IJBSuckerExtended {
47
+ using BitMaps for BitMaps.BitMap;
48
+ using MerkleLib for MerkleLib.Tree;
49
+ using SafeERC20 for IERC20;
50
+
51
+ //*********************************************************************//
52
+ // --------------------------- custom errors ------------------------- //
53
+ //*********************************************************************//
54
+
55
+ error JBSucker_BelowMinGas(uint256 minGas, uint256 minGasLimit);
56
+ error JBSucker_InsufficientBalance(uint256 amount, uint256 balance);
57
+ error JBSucker_InvalidNativeRemoteAddress(bytes32 remoteToken);
58
+ error JBSucker_InvalidMessageVersion(uint8 received, uint8 expected);
59
+ error JBSucker_InvalidProof(bytes32 root, bytes32 inboxRoot);
60
+ error JBSucker_LeafAlreadyExecuted(address token, uint256 index);
61
+ error JBSucker_ManualNotAllowed(JBAddToBalanceMode mode);
62
+ error JBSucker_DeprecationTimestampTooSoon(uint256 givenTime, uint256 minimumTime);
63
+ error JBSucker_NoTerminalForToken(uint256 projectId, address token);
64
+ error JBSucker_NotPeer(bytes32 caller);
65
+ error JBSucker_QueueInsufficientSize(uint256 amount, uint256 minimumAmount);
66
+ error JBSucker_TokenNotMapped(address token);
67
+ error JBSucker_TokenHasInvalidEmergencyHatchState(address token);
68
+ error JBSucker_TokenAlreadyMapped(address localToken, bytes32 mappedTo);
69
+ error JBSucker_AmountExceedsUint128(uint256 amount);
70
+ error JBSucker_UnexpectedMsgValue(uint256 value);
71
+ error JBSucker_ExpectedMsgValue();
72
+ error JBSucker_InsufficientMsgValue(uint256 received, uint256 expected);
73
+ error JBSucker_ZeroBeneficiary();
74
+ error JBSucker_ZeroERC20Token();
75
+ error JBSucker_Deprecated();
76
+
77
+ //*********************************************************************//
78
+ // ------------------------- public constants ------------------------ //
79
+ //*********************************************************************//
80
+
81
+ /// @notice A reasonable minimum gas limit for a basic cross-chain call. The minimum amount of gas required to call
82
+ /// the `fromRemote` (successfully/safely) on the remote chain.
83
+ uint32 public constant override MESSENGER_BASE_GAS_LIMIT = 300_000;
84
+
85
+ /// @notice A reasonable minimum gas limit used when bridging ERC-20s. The minimum amount of gas required to
86
+ /// (successfully/safely) perform a transfer on the remote chain.
87
+ uint32 public constant override MESSENGER_ERC20_MIN_GAS_LIMIT = 200_000;
88
+
89
+ //*********************************************************************//
90
+ // ------------------------- internal constants ----------------------- //
91
+ //*********************************************************************//
92
+
93
+ /// @notice The depth of the merkle tree used to store the outbox and inbox.
94
+ uint32 constant _TREE_DEPTH = 32;
95
+
96
+ /// @notice The message format version. Used to reject incompatible messages from remote chains.
97
+ uint8 public constant MESSAGE_VERSION = 1;
98
+
99
+ //*********************************************************************//
100
+ // --------------- public immutable stored properties ---------------- //
101
+ //*********************************************************************//
102
+
103
+ /// @notice Whether the `amountToAddToBalance` gets added to the project's balance automatically when `claim` is
104
+ /// called or manually by calling `addOutstandingAmountToBalance`.
105
+ JBAddToBalanceMode public immutable override ADD_TO_BALANCE_MODE;
106
+
107
+ /// @notice The directory of terminals and controllers for projects.
108
+ IJBDirectory public immutable override DIRECTORY;
109
+
110
+ /// @notice The contract that manages token minting and burning.
111
+ IJBTokens public immutable override TOKENS;
112
+
113
+ //*********************************************************************//
114
+ // --------------------- public stored properties -------------------- //
115
+ //*********************************************************************//
116
+
117
+ /// @notice The address of this contract's deployer.
118
+ address public override deployer;
119
+
120
+ //*********************************************************************//
121
+ // --------------------- private stored properties ------------------- //
122
+ //*********************************************************************//
123
+
124
+ /// @notice The timestamp after which the sucker is entirely deprecated.
125
+ uint256 internal deprecatedAfter;
126
+
127
+ /// @notice The ID of the project (on the local chain) that this sucker is associated with.
128
+ uint256 private _localProjectId;
129
+
130
+ //*********************************************************************//
131
+ // -------------------- internal stored properties ------------------- //
132
+ //*********************************************************************//
133
+
134
+ /// @notice Tracks whether individual leaves in a given token's merkle tree have been executed (to prevent
135
+ /// double-spending).
136
+ /// @dev A leaf is "executed" when the tokens it represents are minted for its beneficiary.
137
+ /// @custom:param token The token to get the executed bitmap of.
138
+ mapping(address token => BitMaps.BitMap) internal _executedFor;
139
+
140
+ /// @notice The inbox merkle tree root for a given token.
141
+ /// @custom:param token The local terminal token to get the inbox for.
142
+ mapping(address token => JBInboxTreeRoot root) internal _inboxOf;
143
+
144
+ /// @notice The outbox merkle tree for a given token.
145
+ /// @custom:param token The local terminal token to get the outbox for.
146
+ mapping(address token => JBOutboxTree) internal _outboxOf;
147
+
148
+ /// @notice Information about the token on the remote chain that the given token on the local chain is mapped to.
149
+ /// @custom:param token The local terminal token to get the remote token for.
150
+ mapping(address token => JBRemoteToken remoteToken) internal _remoteTokenFor;
151
+
152
+ //*********************************************************************//
153
+ // ---------------------------- constructor -------------------------- //
154
+ //*********************************************************************//
155
+
156
+ /// @param directory A contract storing directories of terminals and controllers for each project.
157
+ /// @param permissions A contract storing permissions.
158
+ /// @param tokens A contract that manages token minting and burning.
159
+ /// @param addToBalanceMode The mode of adding tokens to balance.
160
+ constructor(
161
+ IJBDirectory directory,
162
+ IJBPermissions permissions,
163
+ IJBTokens tokens,
164
+ JBAddToBalanceMode addToBalanceMode,
165
+ address trustedForwarder
166
+ )
167
+ ERC2771Context(trustedForwarder)
168
+ JBPermissioned(permissions)
169
+ {
170
+ DIRECTORY = directory;
171
+ TOKENS = tokens;
172
+ ADD_TO_BALANCE_MODE = addToBalanceMode;
173
+
174
+ // Make it so the singleton can't be initialized.
175
+ _disableInitializers();
176
+
177
+ // Sanity check: make sure the merkle lib uses the same tree depth.
178
+ assert(MerkleLib.TREE_DEPTH == _TREE_DEPTH);
179
+ }
180
+
181
+ //*********************************************************************//
182
+ // ------------------------ external views --------------------------- //
183
+ //*********************************************************************//
184
+
185
+ /// @notice The outstanding amount of tokens to be added to the project's balance by `claim` or
186
+ /// `addOutstandingAmountToBalance`.
187
+ /// @param token The local terminal token to get the amount to add to balance for.
188
+ function amountToAddToBalanceOf(address token) public view override returns (uint256) {
189
+ // Get the amount that is in this sucker to be bridged.
190
+ return _balanceOf({token: token, addr: address(this)}) - _outboxOf[token].balance;
191
+ }
192
+
193
+ /// @notice The inbox merkle tree root for a given token.
194
+ /// @param token The local terminal token to get the inbox for.
195
+ function inboxOf(address token) external view returns (JBInboxTreeRoot memory) {
196
+ return _inboxOf[token];
197
+ }
198
+
199
+ /// @notice Checks whether the specified token is mapped to a remote token.
200
+ /// @param token The terminal token to check.
201
+ /// @return A boolean which is `true` if the token is mapped to a remote token and `false` if it is not.
202
+ function isMapped(address token) external view override returns (bool) {
203
+ return _remoteTokenFor[token].addr != bytes32(0);
204
+ }
205
+
206
+ /// @notice Information about the token on the remote chain that the given token on the local chain is mapped to.
207
+ /// @param token The local terminal token to get the remote token for.
208
+ function outboxOf(address token) external view returns (JBOutboxTree memory) {
209
+ return _outboxOf[token];
210
+ }
211
+
212
+ /// @notice Returns the chain on which the peer is located.
213
+ /// @return chain ID of the peer.
214
+ function peerChainId() external view virtual returns (uint256);
215
+
216
+ /// @notice Information about the token on the remote chain that the given token on the local chain is mapped to.
217
+ /// @param token The local terminal token to get the remote token for.
218
+ function remoteTokenFor(address token) external view returns (JBRemoteToken memory) {
219
+ return _remoteTokenFor[token];
220
+ }
221
+
222
+ //*********************************************************************//
223
+ // ------------------------- public views ---------------------------- //
224
+ //*********************************************************************//
225
+
226
+ /// @notice The peer sucker on the remote chain, as a bytes32 for cross-VM compatibility.
227
+ /// @dev Defaults to `_toBytes32(address(this))`, assuming deterministic cross-chain deployment via CREATE2. The
228
+ /// deployer (`JBSuckerDeployer`) uses `salt = keccak256(abi.encode(_msgSender(), salt))` to ensure
229
+ /// sender-specific determinism. This assumption breaks if CREATE2 conditions differ across chains (e.g.,
230
+ /// different factory nonces, different init code, or different deployer addresses). In such cases, subclasses
231
+ /// must override this function to return the correct peer address (e.g., a Solana program/PDA address for
232
+ /// EVM-SVM deployments). Note that overriding `peer()` is fully supported by the sucker implementation and
233
+ /// off-chain infrastructure, but for revnets it breaks the assumption of matching configurations on both
234
+ /// chains -- for this reason the default same-address behavior is preferred.
235
+ function peer() public view virtual returns (bytes32) {
236
+ return _toBytes32(address(this));
237
+ }
238
+
239
+ /// @notice The ID of the project (on the local chain) that this sucker is associated with.
240
+ function projectId() public view returns (uint256) {
241
+ return _localProjectId;
242
+ }
243
+
244
+ /// @notice Reports the deprecation state of the sucker.
245
+ /// @return state The current deprecation state
246
+ function state() public view override returns (JBSuckerState) {
247
+ uint256 _deprecatedAfter = deprecatedAfter;
248
+
249
+ // The sucker is fully functional, no deprecation has been set yet.
250
+ if (_deprecatedAfter == 0) {
251
+ return JBSuckerState.ENABLED;
252
+ }
253
+
254
+ // The sucker will soon be considered deprecated, this functions only as a warning to users.
255
+ if (block.timestamp < _deprecatedAfter - _maxMessagingDelay()) {
256
+ return JBSuckerState.DEPRECATION_PENDING;
257
+ }
258
+
259
+ // The sucker will no longer send new roots to the pair, but it will accept new incoming roots.
260
+ // Additionally it will let users exit here now that we can no longer send roots/tokens.
261
+ if (block.timestamp < _deprecatedAfter) {
262
+ return JBSuckerState.SENDING_DISABLED;
263
+ }
264
+
265
+ // The sucker is now in the final state of deprecation. It will no longer allow new roots.
266
+ return JBSuckerState.DEPRECATED;
267
+ }
268
+
269
+ function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
270
+ return interfaceId == type(IJBSuckerExtended).interfaceId || interfaceId == type(IJBSucker).interfaceId
271
+ || interfaceId == type(IJBPermissioned).interfaceId || super.supportsInterface(interfaceId);
272
+ }
273
+
274
+ //*********************************************************************//
275
+ // ------------------------ internal views --------------------------- //
276
+ //*********************************************************************//
277
+
278
+ /// @notice Helper to get the `addr`'s balance for a given `token`.
279
+ /// @param token The token to get the balance for.
280
+ /// @param addr The address to get the `token` balance of.
281
+ /// @return balance The address' `token` balance.
282
+ function _balanceOf(address token, address addr) internal view returns (uint256 balance) {
283
+ if (token == JBConstants.NATIVE_TOKEN) {
284
+ return addr.balance;
285
+ }
286
+
287
+ // slither-disable-next-line calls-loop
288
+ return IERC20(token).balanceOf(addr);
289
+ }
290
+
291
+ /// @notice Builds a hash as they are stored in the merkle tree.
292
+ /// @param projectTokenCount The number of project tokens being cashed out.
293
+ /// @param terminalTokenAmount The amount of terminal tokens being reclaimed by the cash out.
294
+ /// @param beneficiary The beneficiary which will receive the project tokens (bytes32 for cross-VM compatibility).
295
+ function _buildTreeHash(
296
+ uint256 projectTokenCount,
297
+ uint256 terminalTokenAmount,
298
+ bytes32 beneficiary
299
+ )
300
+ internal
301
+ pure
302
+ returns (bytes32)
303
+ {
304
+ return keccak256(abi.encode(projectTokenCount, terminalTokenAmount, beneficiary));
305
+ }
306
+
307
+ /// @notice Allow sucker implementations to add/override mapping rules to suite their specific needs.
308
+ function _validateTokenMapping(JBTokenMapping calldata map) internal pure virtual {
309
+ bool isNative = map.localToken == JBConstants.NATIVE_TOKEN;
310
+
311
+ // If the token being mapped is the native token, the `remoteToken` must also be the native token.
312
+ // The native token can also be mapped to the 0 address, which is used to disable native token bridging.
313
+ if (isNative && map.remoteToken != _toBytes32(JBConstants.NATIVE_TOKEN) && map.remoteToken != bytes32(0)) {
314
+ revert JBSucker_InvalidNativeRemoteAddress(map.remoteToken);
315
+ }
316
+
317
+ // Enforce a reasonable minimum gas limit for bridging. A minimum which is too low could lead to the loss of
318
+ // funds.
319
+ if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT && !isNative) {
320
+ revert JBSucker_BelowMinGas(map.minGas, MESSENGER_ERC20_MIN_GAS_LIMIT);
321
+ }
322
+ }
323
+
324
+ /// @notice The calldata. Preferred to use over `msg.data`.
325
+ /// @return calldata The `msg.data` of this call.
326
+ function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
327
+ return ERC2771Context._msgData();
328
+ }
329
+
330
+ /// @notice The message's sender. Preferred to use over `msg.sender`.
331
+ /// @return sender The address which sent this call.
332
+ function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
333
+ return ERC2771Context._msgSender();
334
+ }
335
+
336
+ /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
337
+ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
338
+ return ERC2771Context._contextSuffixLength();
339
+ }
340
+
341
+ /// @notice Convert a bytes32 remote address to a local EVM address.
342
+ /// @param remote The bytes32 representation of the address.
343
+ /// @return The EVM address (lower 20 bytes).
344
+ function _toAddress(bytes32 remote) internal pure returns (address) {
345
+ return address(uint160(uint256(remote)));
346
+ }
347
+
348
+ /// @notice Convert an EVM address to a bytes32 remote address.
349
+ /// @param addr The EVM address.
350
+ /// @return The bytes32 representation (left-padded with zeros).
351
+ function _toBytes32(address addr) internal pure returns (bytes32) {
352
+ return bytes32(uint256(uint160(addr)));
353
+ }
354
+
355
+ //*********************************************************************//
356
+ // --------------------- external transactions ----------------------- //
357
+ //*********************************************************************//
358
+
359
+ /// @notice Initializes the sucker with the project ID and peer address.
360
+ /// @param _projectId The ID of the project (on the local chain) that this sucker is associated with.
361
+ function initialize(uint256 _projectId) public initializer {
362
+ // slither-disable-next-line missing-zero-check
363
+ _localProjectId = _projectId;
364
+ deployer = msg.sender;
365
+ }
366
+
367
+ /// @notice Adds the reclaimed `token` balance to the projects terminal. Can only be used if `ADD_TO_BALANCE_MODE`
368
+ /// is
369
+ /// `MANUAL`.
370
+ /// @param token The address of the terminal token to add to the project's balance.
371
+ function addOutstandingAmountToBalance(address token) external override {
372
+ if (ADD_TO_BALANCE_MODE != JBAddToBalanceMode.MANUAL) {
373
+ revert JBSucker_ManualNotAllowed(ADD_TO_BALANCE_MODE);
374
+ }
375
+
376
+ // Add entire outstanding amount to the project's balance.
377
+ _addToBalance({token: token, amount: amountToAddToBalanceOf(token)});
378
+ }
379
+
380
+ /// @notice Performs multiple claims.
381
+ /// @param claims A list of claims to perform (including the terminal token, merkle tree leaf, and proof for each
382
+ /// claim).
383
+ function claim(JBClaim[] calldata claims) external override {
384
+ // Claim each.
385
+ for (uint256 i; i < claims.length; i++) {
386
+ claim(claims[i]);
387
+ }
388
+ }
389
+
390
+ /// @notice `JBClaim` project tokens which have been bridged from the remote chain for their beneficiary.
391
+ /// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
392
+ function claim(JBClaim calldata claimData) public override {
393
+ // Attempt to validate the proof against the inbox tree for the terminal token.
394
+ _validate({
395
+ projectTokenCount: claimData.leaf.projectTokenCount,
396
+ terminalToken: claimData.token,
397
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
398
+ beneficiary: claimData.leaf.beneficiary,
399
+ index: claimData.leaf.index,
400
+ leaves: claimData.proof
401
+ });
402
+
403
+ emit Claimed({
404
+ beneficiary: claimData.leaf.beneficiary,
405
+ token: claimData.token,
406
+ projectTokenCount: claimData.leaf.projectTokenCount,
407
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
408
+ index: claimData.leaf.index,
409
+ autoAddedToBalance: ADD_TO_BALANCE_MODE == JBAddToBalanceMode.ON_CLAIM ? true : false,
410
+ caller: _msgSender()
411
+ });
412
+
413
+ // Give the user their project tokens, send the project its funds.
414
+ _handleClaim({
415
+ terminalToken: claimData.token,
416
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
417
+ projectTokenAmount: claimData.leaf.projectTokenCount,
418
+ beneficiary: claimData.leaf.beneficiary
419
+ });
420
+ }
421
+
422
+ /// @notice Receive a merkle root for a terminal token from the remote project.
423
+ /// @dev This can only be called by the messenger contract on the local chain, with a message from the remote peer.
424
+ /// @dev Nonce ordering: This function accepts any nonce strictly greater than the current inbox nonce, rather than
425
+ /// requiring sequential (nonce == inbox.nonce + 1) processing. This is intentional because some bridges (e.g.,
426
+ /// Chainlink CCIP) do not guarantee in-order message delivery. As a result, if nonces arrive out of order
427
+ /// (e.g., nonce 3 before nonce 2), the earlier nonce's root will be silently skipped. This means the claims
428
+ /// in the skipped root's merkle tree become permanently unclaimable on this chain. The sender would need to
429
+ /// use the emergency exit on the source chain to recover funds from skipped roots. This trade-off is accepted
430
+ /// because enforcing sequential nonces could permanently block a token's inbox if a single message is delayed
431
+ /// or lost by the bridge.
432
+ /// @param root The merkle root, token, and amount being received.
433
+ function fromRemote(JBMessageRoot calldata root) external payable {
434
+ // Make sure that the message came from our peer.
435
+ if (!_isRemotePeer(_msgSender())) {
436
+ revert JBSucker_NotPeer(_toBytes32(_msgSender()));
437
+ }
438
+
439
+ // Validate the message version to reject incompatible messages.
440
+ if (root.version != MESSAGE_VERSION) {
441
+ revert JBSucker_InvalidMessageVersion(root.version, MESSAGE_VERSION);
442
+ }
443
+
444
+ // Convert the remote token bytes32 to a local address for inbox lookup.
445
+ address localToken = _toAddress(root.token);
446
+
447
+ // Get the inbox in storage.
448
+ JBInboxTreeRoot storage inbox = _inboxOf[localToken];
449
+
450
+ // If the received tree's nonce is greater than the current inbox tree's nonce, update the inbox tree.
451
+ // We can't revert because this could be a native token transfer. If we reverted, we would lose the native
452
+ // tokens.
453
+ if (root.remoteRoot.nonce > inbox.nonce && state() != JBSuckerState.DEPRECATED) {
454
+ inbox.nonce = root.remoteRoot.nonce;
455
+ inbox.root = root.remoteRoot.root;
456
+ emit NewInboxTreeRoot({
457
+ token: localToken, nonce: root.remoteRoot.nonce, root: root.remoteRoot.root, caller: _msgSender()
458
+ });
459
+ } else {
460
+ // L-10: Emit an event when a root is rejected due to a stale (non-increasing) nonce.
461
+ // This aids off-chain monitoring in detecting out-of-order or duplicate deliveries.
462
+ emit StaleRootRejected({token: localToken, receivedNonce: root.remoteRoot.nonce, currentNonce: inbox.nonce});
463
+ }
464
+ }
465
+
466
+ /// @notice Map an ERC-20 token on the local chain to an ERC-20 token on the remote chain, allowing that token to be
467
+ /// bridged.
468
+ /// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
469
+ /// them.
470
+ function mapToken(JBTokenMapping calldata map) public payable override {
471
+ _mapToken({map: map, transportPaymentValue: msg.value});
472
+ }
473
+
474
+ /// @notice Map multiple ERC-20 tokens on the local chain to ERC-20 tokens on the remote chain, allowing those
475
+ /// tokens to be bridged.
476
+ /// @param maps A list of local and remote terminal token addresses to map, and minimum amount/gas limits for
477
+ /// bridging them.
478
+ function mapTokens(JBTokenMapping[] calldata maps) external payable override {
479
+ uint256 numberToDisable;
480
+
481
+ // Loop over the number of mappings and increase numberToDisable to correctly set transportPaymentValue.
482
+ for (uint256 h; h < maps.length; h++) {
483
+ JBOutboxTree storage _outbox = _outboxOf[maps[h].localToken];
484
+ if (maps[h].remoteToken == bytes32(0) && _outbox.numberOfClaimsSent != _outbox.tree.count) {
485
+ numberToDisable++;
486
+ }
487
+ }
488
+
489
+ // Perform each token mapping.
490
+ for (uint256 i; i < maps.length; i++) {
491
+ // slither-disable-next-line msg-value-loop
492
+ _mapToken({map: maps[i], transportPaymentValue: numberToDisable > 0 ? msg.value / numberToDisable : 0});
493
+ }
494
+ }
495
+
496
+ /// @notice Enables the emergency hatch for a list of tokens, allowing users to exit on the chain they deposited on.
497
+ /// @dev For use when a token or a few tokens are no longer compatible with a bridge.
498
+ /// @param tokens The terminal tokens to enable the emergency hatch for.
499
+ function enableEmergencyHatchFor(address[] calldata tokens) external override {
500
+ // The caller must be the project owner or have the `QUEUE_RULESETS` permission from them.
501
+ // slither-disable-next-line calls-loop
502
+ uint256 _projectId = projectId();
503
+
504
+ _requirePermissionFrom({
505
+ account: DIRECTORY.PROJECTS().ownerOf(_projectId),
506
+ projectId: _projectId,
507
+ permissionId: JBPermissionIds.SUCKER_SAFETY
508
+ });
509
+
510
+ // Enable the emergency hatch for each token.
511
+ for (uint256 i; i < tokens.length; i++) {
512
+ // We have an invariant where if emergencyHatch is true, enabled should be false.
513
+ _remoteTokenFor[tokens[i]].enabled = false;
514
+ _remoteTokenFor[tokens[i]].emergencyHatch = true;
515
+ }
516
+
517
+ emit EmergencyHatchOpened(tokens, _msgSender());
518
+ }
519
+
520
+ /// @notice Prepare project tokens and the cash out amount backing them to be bridged to the remote chain.
521
+ /// @dev This adds the tokens and funds to the outbox tree for the `token`. They will be bridged by the next call to
522
+ /// `toRemote` for the same `token`.
523
+ /// @param projectTokenCount The number of project tokens to prepare for bridging.
524
+ /// @param beneficiary The recipient on the remote chain (bytes32 for cross-VM compatibility).
525
+ /// For EVM peers: the EVM address left-padded to 32 bytes via `_toBytes32`.
526
+ /// For SVM peers: the full 32-byte Solana public key.
527
+ /// @param minTokensReclaimed The minimum amount of terminal tokens to cash out for. If the amount cashed out is
528
+ /// less
529
+ /// than this, the transaction will revert.
530
+ /// @param token The address of the terminal token to cash out for.
531
+ function prepare(
532
+ uint256 projectTokenCount,
533
+ bytes32 beneficiary,
534
+ uint256 minTokensReclaimed,
535
+ address token
536
+ )
537
+ external
538
+ override
539
+ {
540
+ // Make sure the beneficiary is not the zero address, as this would revert when minting on the remote chain.
541
+ if (beneficiary == bytes32(0)) {
542
+ revert JBSucker_ZeroBeneficiary();
543
+ }
544
+
545
+ // Get the project's token.
546
+ IERC20 projectToken = IERC20(address(TOKENS.tokenOf(projectId())));
547
+ if (address(projectToken) == address(0)) {
548
+ revert JBSucker_ZeroERC20Token();
549
+ }
550
+
551
+ // Make sure that the token is mapped to a remote token.
552
+ if (!_remoteTokenFor[token].enabled) {
553
+ revert JBSucker_TokenNotMapped(token);
554
+ }
555
+
556
+ // Make sure that the sucker still allows sending new messaged.
557
+ JBSuckerState deprecationState = state();
558
+ if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
559
+ revert JBSucker_Deprecated();
560
+ }
561
+
562
+ // Transfer the tokens to this contract.
563
+ // slither-disable-next-line reentrancy-events,reentrancy-benign
564
+ projectToken.safeTransferFrom({from: _msgSender(), to: address(this), value: projectTokenCount});
565
+
566
+ // Cash out the tokens.
567
+ // slither-disable-next-line reentrancy-events,reentrancy-benign
568
+ uint256 terminalTokenAmount = _pullBackingAssets({
569
+ projectToken: projectToken, count: projectTokenCount, token: token, minTokensReclaimed: minTokensReclaimed
570
+ });
571
+
572
+ // Insert the item into the outbox tree for the terminal `token`.
573
+ _insertIntoTree({
574
+ projectTokenCount: projectTokenCount,
575
+ token: token,
576
+ terminalTokenAmount: terminalTokenAmount,
577
+ beneficiary: beneficiary
578
+ });
579
+ }
580
+
581
+ /// @notice Bridge the project tokens, cashed out funds, and beneficiary information for a given `token` to the
582
+ /// remote
583
+ /// chain.
584
+ /// @dev This sends the outbox root for the specified `token` to the remote chain.
585
+ /// @param token The terminal token being bridged.
586
+ function toRemote(address token) external payable override {
587
+ JBRemoteToken memory remoteToken = _remoteTokenFor[token];
588
+
589
+ // Ensure that the token does not have an emergency hatch enabled.
590
+ if (remoteToken.emergencyHatch) {
591
+ revert JBSucker_TokenHasInvalidEmergencyHatchState(token);
592
+ }
593
+
594
+ // Ensure that the amount being bridged exceeds the minimum bridge amount.
595
+ if (_outboxOf[token].balance < remoteToken.minBridgeAmount) {
596
+ revert JBSucker_QueueInsufficientSize(_outboxOf[token].balance, remoteToken.minBridgeAmount);
597
+ }
598
+
599
+ // Send the merkle root to the remote chain.
600
+ _sendRoot({transportPayment: msg.value, token: token, remoteToken: remoteToken});
601
+ }
602
+
603
+ /// @notice Lets user exit on the chain they deposited in a scenario where the bridge is no longer functional.
604
+ /// @param claimData The terminal token, merkle tree leaf, and proof for the claim
605
+ function exitThroughEmergencyHatch(JBClaim calldata claimData) external override {
606
+ // Does all the needed validation to ensure that the claim is valid *and* that claiming through the emergency
607
+ // hatch is allowed.
608
+ _validateForEmergencyExit({
609
+ projectTokenCount: claimData.leaf.projectTokenCount,
610
+ terminalToken: claimData.token,
611
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
612
+ beneficiary: claimData.leaf.beneficiary,
613
+ index: claimData.leaf.index,
614
+ leaves: claimData.proof
615
+ });
616
+
617
+ // Decrease the outstanding balance for this token.
618
+ _outboxOf[claimData.token].balance -= claimData.leaf.terminalTokenAmount;
619
+
620
+ // Give the user their project tokens, send the project its funds.
621
+ _handleClaim({
622
+ terminalToken: claimData.token,
623
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
624
+ projectTokenAmount: claimData.leaf.projectTokenCount,
625
+ beneficiary: claimData.leaf.beneficiary
626
+ });
627
+ }
628
+
629
+ /// @notice Set or remove the time after which this sucker will be deprecated, once deprecated the sucker will no
630
+ /// longer be functional and it will let all users exit.
631
+ /// @param timestamp The time after which the sucker will be deprecated. Or `0` to remove the upcoming deprecation.
632
+ function setDeprecation(uint40 timestamp) external override {
633
+ // As long as the sucker has not started letting users withdrawal, its deprecation time can be
634
+ // extended/shortened.
635
+ JBSuckerState deprecationState = state();
636
+ if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
637
+ revert JBSucker_Deprecated();
638
+ }
639
+
640
+ // slither-disable-next-line calls-loop
641
+ uint256 _projectId = projectId();
642
+
643
+ // The caller must be the project owner or have the `SET_SUCKER_DEPRECATION` permission from them.
644
+ _requirePermissionFrom({
645
+ account: DIRECTORY.PROJECTS().ownerOf(_projectId),
646
+ projectId: _projectId,
647
+ permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
648
+ });
649
+
650
+ // This is the earliest time for when the sucker can be considered deprecated.
651
+ // There is a mandatory delay to allow for remaining messages to be received.
652
+ // This should be called on both sides of the suckers, preferably with a matching timestamp.
653
+ uint256 nextEarliestDeprecationTime = block.timestamp + _maxMessagingDelay();
654
+
655
+ // The deprecation can be entirely disabled *or* it has to be later than the earliest possible time.
656
+ if (timestamp != 0 && timestamp < nextEarliestDeprecationTime) {
657
+ revert JBSucker_DeprecationTimestampTooSoon(timestamp, nextEarliestDeprecationTime);
658
+ }
659
+
660
+ deprecatedAfter = timestamp;
661
+ emit DeprecationTimeUpdated(timestamp, _msgSender());
662
+ }
663
+
664
+ //*********************************************************************//
665
+ // ---------------------------- receive ----------------------------- //
666
+ //*********************************************************************//
667
+
668
+ /// @notice Accepts incoming native token (ETH) transfers.
669
+ /// @dev This receive function is intentionally unrestricted. It must accept ETH from multiple sources:
670
+ /// - Bridge contracts (e.g., Optimism's StandardBridge, Arbitrum's gateway) delivering bridged native tokens.
671
+ /// - WETH contracts during unwrapping (e.g., CCIP sucker unwraps WETH via `withdraw()` which sends ETH here).
672
+ /// - Terminals returning native tokens during `cashOutTokensOf` (backing asset pulls).
673
+ /// @dev Restricting this to known senders would risk breaking bridge integrations, as bridge contracts may change
674
+ /// addresses or use proxy patterns. The sucker's accounting (`_outboxOf[token].balance` and
675
+ /// `amountToAddToBalanceOf`) already tracks expected native token amounts, so excess ETH sent here does not
676
+ /// create a double-spend risk -- it would simply increase the `amountToAddToBalance` for the project.
677
+ receive() external payable {}
678
+
679
+ //*********************************************************************//
680
+ // --------------------- internal transactions ----------------------- //
681
+ //*********************************************************************//
682
+
683
+ /// @notice Adds funds to the projects balance.
684
+ /// @param token The terminal token to add to the project's balance.
685
+ /// @param amount The amount of terminal tokens to add to the project's balance.
686
+ function _addToBalance(address token, uint256 amount) internal {
687
+ // Make sure that the current `amountToAddToBalance` is greater than or equal to the amount being added.
688
+ uint256 addableAmount = amountToAddToBalanceOf(token);
689
+ if (amount > addableAmount) {
690
+ revert JBSucker_InsufficientBalance(amount, addableAmount);
691
+ }
692
+
693
+ uint256 _projectId = projectId();
694
+
695
+ // Get the project's primary terminal for the token.
696
+ // slither
697
+ // slither-disable-next-line calls-loop
698
+ IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId: _projectId, token: token});
699
+
700
+ // slither-disable-next-line incorrect-equality
701
+ if (address(terminal) == address(0)) revert JBSucker_NoTerminalForToken(_projectId, token);
702
+
703
+ // Perform the `addToBalance`.
704
+ if (token != JBConstants.NATIVE_TOKEN) {
705
+ // slither-disable-next-line calls-loop
706
+ uint256 balanceBefore = IERC20(token).balanceOf(address(this));
707
+
708
+ SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
709
+
710
+ // slither-disable-next-line calls-loop
711
+ terminal.addToBalanceOf({
712
+ projectId: _projectId, token: token, amount: amount, shouldReturnHeldFees: false, memo: "", metadata: ""
713
+ });
714
+
715
+ // Sanity check: make sure we transfer the full amount.
716
+ // slither-disable-next-line calls-loop,incorrect-equality
717
+ assert(IERC20(token).balanceOf(address(this)) == balanceBefore - amount);
718
+ } else {
719
+ // If the token is the native token, use `msg.value`.
720
+ // slither-disable-next-line arbitrary-send-eth,calls-loop
721
+ terminal.addToBalanceOf{value: amount}({
722
+ projectId: _projectId, token: token, amount: amount, shouldReturnHeldFees: false, memo: "", metadata: ""
723
+ });
724
+ }
725
+ }
726
+
727
+ /// @notice The action(s) to perform after a user has succesfully proven their claim.
728
+ /// @param terminalToken The terminal token being sucked.
729
+ /// @param terminalTokenAmount The amount of terminal tokens.
730
+ /// @param projectTokenAmount The amount of project tokens.
731
+ /// @param beneficiary The beneficiary of the project tokens (bytes32 for cross-VM compatibility).
732
+ function _handleClaim(
733
+ address terminalToken,
734
+ uint256 terminalTokenAmount,
735
+ uint256 projectTokenAmount,
736
+ bytes32 beneficiary
737
+ )
738
+ internal
739
+ {
740
+ // If this contract's add to balance mode is `ON_CLAIM`, add the cashed out funds to the project's balance.
741
+ if (ADD_TO_BALANCE_MODE == JBAddToBalanceMode.ON_CLAIM && terminalTokenAmount != 0) {
742
+ _addToBalance({token: terminalToken, amount: terminalTokenAmount});
743
+ }
744
+
745
+ uint256 _projectId = projectId();
746
+
747
+ // Cast the bytes32 beneficiary to an EVM address for the local mint.
748
+ address beneficiaryAddress = _toAddress(beneficiary);
749
+
750
+ // Mint the project tokens for the beneficiary.
751
+ // slither-disable-next-line calls-loop,unused-return
752
+ IJBController(address(DIRECTORY.controllerOf(_projectId)))
753
+ .mintTokensOf({
754
+ projectId: _projectId,
755
+ tokenCount: projectTokenAmount,
756
+ beneficiary: beneficiaryAddress,
757
+ memo: "",
758
+ useReservedPercent: false
759
+ });
760
+ }
761
+
762
+ /// @notice Inserts a new leaf into the outbox merkle tree for the specified `token`.
763
+ /// @param projectTokenCount The amount of project tokens being cashed out.
764
+ /// @param token The terminal token being cashed out for.
765
+ /// @param terminalTokenAmount The amount of terminal tokens reclaimed by cashing out.
766
+ /// @param beneficiary The beneficiary of the project tokens on the remote chain (bytes32 for cross-VM
767
+ /// compatibility).
768
+ function _insertIntoTree(
769
+ uint256 projectTokenCount,
770
+ address token,
771
+ uint256 terminalTokenAmount,
772
+ bytes32 beneficiary
773
+ )
774
+ internal
775
+ {
776
+ // Guard against amounts that would overflow uint128 on SVM (INTEROP-5).
777
+ if (terminalTokenAmount > type(uint128).max) revert JBSucker_AmountExceedsUint128(terminalTokenAmount);
778
+ if (projectTokenCount > type(uint128).max) revert JBSucker_AmountExceedsUint128(projectTokenCount);
779
+ // Build a hash based on the token amounts and the beneficiary.
780
+ bytes32 hashed = _buildTreeHash({
781
+ projectTokenCount: projectTokenCount, terminalTokenAmount: terminalTokenAmount, beneficiary: beneficiary
782
+ });
783
+
784
+ // Get the outbox in storage.
785
+ JBOutboxTree storage outbox = _outboxOf[token];
786
+
787
+ // Create a new tree based on the outbox tree for the terminal token with the hash inserted.
788
+ MerkleLib.Tree memory tree = outbox.tree.insert(hashed);
789
+
790
+ // Update the outbox tree and balance for the terminal token.
791
+ outbox.tree = tree;
792
+ outbox.balance += terminalTokenAmount;
793
+
794
+ emit InsertToOutboxTree({
795
+ beneficiary: beneficiary,
796
+ token: token,
797
+ hashed: hashed,
798
+ index: tree.count - 1, // Subtract 1 since we want the 0-based index.
799
+ root: outbox.tree.root(),
800
+ projectTokenCount: projectTokenCount,
801
+ terminalTokenAmount: terminalTokenAmount,
802
+ caller: _msgSender()
803
+ });
804
+ }
805
+
806
+ /// @notice Checks if the `sender` (`_msgSender`) is a valid representative of the remote peer.
807
+ /// @param sender The message's sender.
808
+ function _isRemotePeer(address sender) internal virtual returns (bool valid);
809
+
810
+ /// @notice Map an ERC-20 token on the local chain to an ERC-20 token on the remote chain, allowing that token to be
811
+ /// bridged or disabled.
812
+ /// @dev Once a token has outbox tree entries (`_outboxOf[token].tree.count != 0`), it cannot be remapped to a
813
+ /// different remote token -- it can only be disabled by mapping to `address(0)`, which triggers a final root
814
+ /// flush to settle outstanding claims. This permanence prevents double-spending: if a remapping were allowed
815
+ /// after outbox activity, the same local funds could be claimed against two different remote tokens. A
816
+ /// misconfigured mapping therefore requires deploying a new sucker. Re-enabling a previously disabled mapping
817
+ /// (back to the same remote token) is supported.
818
+ /// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
819
+ /// them.
820
+ /// @param transportPaymentValue The amount of `msg.value` to send for the token mapping.
821
+ function _mapToken(JBTokenMapping calldata map, uint256 transportPaymentValue) internal {
822
+ address token = map.localToken;
823
+ JBRemoteToken memory currentMapping = _remoteTokenFor[token];
824
+
825
+ // Once the emergency hatch for a token is enabled it can't be disabled.
826
+ if (currentMapping.emergencyHatch) {
827
+ revert JBSucker_TokenHasInvalidEmergencyHatchState(token);
828
+ }
829
+
830
+ // Validate the token mapping according to the rules of the sucker.
831
+ _validateTokenMapping(map);
832
+
833
+ // Reference the project id.
834
+ uint256 _projectId = projectId();
835
+
836
+ // slither-disable-next-line calls-loop
837
+ _requirePermissionFrom({
838
+ account: DIRECTORY.PROJECTS().ownerOf(_projectId),
839
+ projectId: _projectId,
840
+ permissionId: JBPermissionIds.MAP_SUCKER_TOKEN
841
+ });
842
+
843
+ // Make sure that the token does not get remapped to another remote token.
844
+ // As this would cause the funds for this token to be double spendable on the other side.
845
+ // It should not be possible to cause any issues even without this check
846
+ // a bridge *should* never accept such a request. This is mostly a sanity check.
847
+ if (
848
+ currentMapping.addr != bytes32(0) && currentMapping.addr != map.remoteToken && map.remoteToken != bytes32(0)
849
+ && _outboxOf[token].tree.count != 0
850
+ ) {
851
+ revert JBSucker_TokenAlreadyMapped(token, currentMapping.addr);
852
+ }
853
+
854
+ // Note (L-21): No inbox guard needed here. Token remapping only affects the outbound (sending) path —
855
+ // it changes where tokens get bridged TO. Existing inbox claims are resolved against the inbox merkle
856
+ // tree keyed by the local token address. Changing the remote token doesn't invalidate those claims
857
+ // since the tokens have already arrived and the merkle proofs remain valid.
858
+
859
+ // If the remote token is being set to the 0 address (which disables bridging), send any remaining outbox funds
860
+ // to the remote chain.
861
+ if (map.remoteToken == bytes32(0) && _outboxOf[token].numberOfClaimsSent != _outboxOf[token].tree.count) {
862
+ _sendRoot({transportPayment: transportPaymentValue, token: token, remoteToken: currentMapping});
863
+ }
864
+
865
+ // Update the token mapping.
866
+ _remoteTokenFor[token] = JBRemoteToken({
867
+ enabled: map.remoteToken != bytes32(0),
868
+ emergencyHatch: false,
869
+ minGas: map.minGas,
870
+ // This is done so that a token can be disabled and then enabled again
871
+ // while ensuring the remoteToken never changes (unless it hasn't been used yet)
872
+ addr: map.remoteToken == bytes32(0) ? currentMapping.addr : map.remoteToken,
873
+ minBridgeAmount: map.minBridgeAmount
874
+ });
875
+ }
876
+
877
+ /// @notice Cash out project tokens for terminal tokens.
878
+ /// @param projectToken The project token being cashed out.
879
+ /// @param count The number of project tokens to cash out.
880
+ /// @param token The terminal token to cash out for.
881
+ /// @param minTokensReclaimed The minimum amount of terminal tokens to reclaim. If the amount reclaimed is less than
882
+ /// this, the transaction will revert.
883
+ /// @return reclaimedAmount The amount of terminal tokens reclaimed by the cash out.
884
+ function _pullBackingAssets(
885
+ IERC20 projectToken,
886
+ uint256 count,
887
+ address token,
888
+ uint256 minTokensReclaimed
889
+ )
890
+ internal
891
+ virtual
892
+ returns (uint256 reclaimedAmount)
893
+ {
894
+ projectToken;
895
+
896
+ uint256 _projectId = projectId();
897
+
898
+ // Get the project's primary terminal for `token`. We will cash out from this terminal.
899
+ IJBCashOutTerminal terminal =
900
+ IJBCashOutTerminal(address(DIRECTORY.primaryTerminalOf({projectId: _projectId, token: token})));
901
+
902
+ // If the project doesn't have a primary terminal for `token`, revert.
903
+ if (address(terminal) == address(0)) {
904
+ revert JBSucker_NoTerminalForToken(_projectId, token);
905
+ }
906
+
907
+ // Cash out the tokens.
908
+ uint256 balanceBefore = _balanceOf({token: token, addr: address(this)});
909
+ reclaimedAmount = terminal.cashOutTokensOf({
910
+ holder: address(this),
911
+ projectId: _projectId,
912
+ cashOutCount: count,
913
+ tokenToReclaim: token,
914
+ minTokensReclaimed: minTokensReclaimed,
915
+ beneficiary: payable(address(this)),
916
+ metadata: bytes("")
917
+ });
918
+
919
+ // Sanity check to make sure we received the expected amount.
920
+ // This prevents malicious terminals from reporting amounts other than what they send.
921
+ // slither-disable-next-line incorrect-equality
922
+ assert(reclaimedAmount == _balanceOf({token: token, addr: address(this)}) - balanceBefore);
923
+ }
924
+
925
+ /// @notice Send the outbox root for the specified token to the remote peer.
926
+ /// @dev The call may have a `transportPayment` for bridging native tokens. Require it to be `0` if it is not
927
+ /// needed. Make sure if a value being paid to the bridge is expected to revert if the given value is `0`.
928
+ /// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message. (usually
929
+ /// derived from `msg.value`)
930
+ /// @param token The terminal token to bridge the merkle tree of.
931
+ /// @param remoteToken The remote token which the `token` is mapped to.
932
+ function _sendRoot(uint256 transportPayment, address token, JBRemoteToken memory remoteToken) internal virtual {
933
+ // Ensure the token is mapped to an address on the remote chain.
934
+ if (remoteToken.addr == bytes32(0)) revert JBSucker_TokenNotMapped(token);
935
+
936
+ // Make sure that the sucker still allows sending new messaged.
937
+ JBSuckerState deprecationState = state();
938
+ if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
939
+ revert JBSucker_Deprecated();
940
+ }
941
+
942
+ // Get the outbox in storage.
943
+ JBOutboxTree storage outbox = _outboxOf[token];
944
+
945
+ // Get the amount to send and then clear it from the outbox tree.
946
+ uint256 amount = outbox.balance;
947
+ delete outbox.balance;
948
+
949
+ // Increment the outbox tree's nonce.
950
+ uint64 nonce = ++outbox.nonce;
951
+ bytes32 root = outbox.tree.root();
952
+
953
+ uint256 count = outbox.tree.count;
954
+ // Update the numberOfClaimsSent to the current count of the tree.
955
+ // This is used as in the fallback to allow users to withdraw locally if the bridge is reverting.
956
+ outbox.numberOfClaimsSent = count;
957
+ uint256 index = count - 1;
958
+
959
+ // Emit an event for the relayers to watch for.
960
+ emit RootToRemote({root: root, token: token, index: index, nonce: nonce, caller: _msgSender()});
961
+
962
+ // Build the message to be send.
963
+ JBMessageRoot memory message = JBMessageRoot({
964
+ version: MESSAGE_VERSION,
965
+ token: remoteToken.addr,
966
+ amount: amount,
967
+ remoteRoot: JBInboxTreeRoot({nonce: nonce, root: root})
968
+ });
969
+
970
+ // Execute the chain/sucker specific logic for transferring the assets and communicating the root.
971
+ _sendRootOverAMB({
972
+ transportPayment: transportPayment,
973
+ index: index,
974
+ token: token,
975
+ amount: amount,
976
+ remoteToken: remoteToken,
977
+ message: message
978
+ });
979
+ }
980
+
981
+ /// @notice Performs the logic to send a message to the peer over the AMB.
982
+ /// @dev This is chain/sucker/bridge specific logic.
983
+ /// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
984
+ /// @param index The index of the most recent message that is part of the root.
985
+ /// @param token The terminal token being bridged.
986
+ /// @param amount The amount of terminal tokens being bridged.
987
+ /// @param remoteToken The remote token which the terminal token is mapped to.
988
+ /// @param message The message/root to send to the remote chain.
989
+ function _sendRootOverAMB(
990
+ uint256 transportPayment,
991
+ uint256 index,
992
+ address token,
993
+ uint256 amount,
994
+ JBRemoteToken memory remoteToken,
995
+ JBMessageRoot memory message
996
+ )
997
+ internal
998
+ virtual;
999
+
1000
+ /// @notice What is the maximum time it takes for a message to be received on the other side.
1001
+ /// @dev Be sure to keep in mind if a message fails having to retry and the time it takes to retry.
1002
+ /// @return The maximum time it takes for a message to be received on the other side.
1003
+ function _maxMessagingDelay() internal pure virtual returns (uint40) {
1004
+ return 14 days;
1005
+ }
1006
+
1007
+ /// @notice Validates a leaf as being in the inbox merkle tree and registers the leaf as executed (to prevent
1008
+ /// double-spending).
1009
+ /// @dev Reverts if the leaf is invalid.
1010
+ /// @param projectTokenCount The number of project tokens which were cashed out.
1011
+ /// @param terminalToken The terminal token that the project tokens were cashed out for.
1012
+ /// @param terminalTokenAmount The amount of terminal tokens reclaimed by the cash out.
1013
+ /// @param beneficiary The beneficiary of the project tokens (bytes32 for cross-VM compatibility).
1014
+ /// @param index The index of the leaf being proved in the terminal token's inbox tree.
1015
+ /// @param leaves The leaves that prove that the leaf at the `index` is in the tree (i.e. the merkle branch that the
1016
+ /// leaf is on).
1017
+ function _validate(
1018
+ uint256 projectTokenCount,
1019
+ address terminalToken,
1020
+ uint256 terminalTokenAmount,
1021
+ bytes32 beneficiary,
1022
+ uint256 index,
1023
+ bytes32[_TREE_DEPTH] calldata leaves
1024
+ )
1025
+ internal
1026
+ {
1027
+ // Make sure the leaf has not already been executed.
1028
+ if (_executedFor[terminalToken].get(index)) {
1029
+ revert JBSucker_LeafAlreadyExecuted(terminalToken, index);
1030
+ }
1031
+
1032
+ // Register the leaf as executed to prevent double-spending.
1033
+ _executedFor[terminalToken].set(index);
1034
+
1035
+ // Calculate the root based on the leaf, the branch, and the index.
1036
+ // Compare to the current root, Revert if they do not match.
1037
+ _validateBranchRoot({
1038
+ expectedRoot: _inboxOf[terminalToken].root,
1039
+ projectTokenCount: projectTokenCount,
1040
+ terminalTokenAmount: terminalTokenAmount,
1041
+ beneficiary: beneficiary,
1042
+ index: index,
1043
+ leaves: leaves
1044
+ });
1045
+ }
1046
+
1047
+ /// @notice Validates a leaf as being in the outbox merkle tree and not being send over the amb, and registers the
1048
+ /// leaf as executed (to prevent double-spending).
1049
+ /// @dev Reverts if the leaf is invalid.
1050
+ /// @dev IMPORTANT: Emergency exit safety depends on `numberOfClaimsSent` being accurately tracked.
1051
+ /// `numberOfClaimsSent` is updated in `_sendRoot` to equal `outbox.tree.count` at the time the root is sent
1052
+ /// over the bridge. This value determines which leaves have already been communicated to the remote peer and
1053
+ /// are therefore NOT safe to reclaim locally (as they could be claimed on the remote chain too, enabling
1054
+ /// double-spending).
1055
+ /// @dev Assumptions:
1056
+ /// 1. `numberOfClaimsSent` is only updated in `_sendRoot`, which is called from `toRemote` and `_mapToken`
1057
+ /// (when disabling a token). If `_sendRoot` fails or is never called, `numberOfClaimsSent` remains 0,
1058
+ /// allowing all leaves to be emergency-exited (which is correct -- nothing was sent).
1059
+ /// 2. If the bridge delivers the root but `numberOfClaimsSent` was set before additional leaves were added
1060
+ /// to the outbox tree, those additional leaves (with index >= numberOfClaimsSent) are safe to emergency-exit
1061
+ /// because they were never part of the sent root.
1062
+ /// 3. A compromised or buggy `_sendRootOverAMB` implementation that fails silently (does not revert but also
1063
+ /// does not deliver the message) could lead to `numberOfClaimsSent` being incremented without the remote
1064
+ /// peer receiving the root. In this scenario, leaves with index < numberOfClaimsSent would be blocked from
1065
+ /// emergency exit even though they were never claimable remotely. This is a conservative failure mode --
1066
+ /// funds are locked rather than double-spent. The emergency hatch or deprecation flow would need to be used.
1067
+ /// @param projectTokenCount The number of project tokens which were cashed out.
1068
+ /// @param terminalToken The terminal token that the project tokens were cashed out for.
1069
+ /// @param terminalTokenAmount The amount of terminal tokens reclaimed by the cash out.
1070
+ /// @param beneficiary The beneficiary of the project tokens (bytes32 for cross-VM compatibility).
1071
+ /// @param index The index of the leaf being proved in the terminal token's inbox tree.
1072
+ /// @param leaves The leaves that prove that the leaf at the `index` is in the tree (i.e. the merkle branch that the
1073
+ /// leaf is on).
1074
+ function _validateForEmergencyExit(
1075
+ uint256 projectTokenCount,
1076
+ address terminalToken,
1077
+ uint256 terminalTokenAmount,
1078
+ bytes32 beneficiary,
1079
+ uint256 index,
1080
+ bytes32[_TREE_DEPTH] calldata leaves
1081
+ )
1082
+ internal
1083
+ {
1084
+ // Make sure that the emergencyHatch is enabled for the token.
1085
+ JBSuckerState deprecationState = state();
1086
+ if (
1087
+ deprecationState != JBSuckerState.DEPRECATED && deprecationState != JBSuckerState.SENDING_DISABLED
1088
+ && !_remoteTokenFor[terminalToken].emergencyHatch
1089
+ ) {
1090
+ revert JBSucker_TokenHasInvalidEmergencyHatchState(terminalToken);
1091
+ }
1092
+
1093
+ // Check that this claim is within the bounds of who can claim.
1094
+ // If the root that this leaf is in was already send then we can not let the user claim here.
1095
+ // As it could have also been received by the peer sucker, which would then let the user claim on each side.
1096
+ // NOTE: We are comparing the *count* and the *index*, so `count - 1` is the last index that was sent.
1097
+ // A count of 0 means that no root has ever been send for this token, so everyone can claim.
1098
+ JBOutboxTree storage outboxOfToken = _outboxOf[terminalToken];
1099
+ if (outboxOfToken.numberOfClaimsSent != 0 && outboxOfToken.numberOfClaimsSent - 1 >= index) {
1100
+ revert JBSucker_LeafAlreadyExecuted(terminalToken, index);
1101
+ }
1102
+
1103
+ {
1104
+ // We re-use the same `_executedFor` mapping but we use a different slot.
1105
+ // We can not use the regular mapping, since this claim is done for tokens being send from here to the pair.
1106
+ // where the regular mapping is for tokens that were send on the pair to here. Even though these may seem
1107
+ // similar they are actually completely unrelated.
1108
+ address emergencyExitAddress = address(bytes20(keccak256(abi.encode(terminalToken))));
1109
+
1110
+ // Make sure the leaf has not already been executed.
1111
+ if (_executedFor[emergencyExitAddress].get(index)) {
1112
+ revert JBSucker_LeafAlreadyExecuted(terminalToken, index);
1113
+ }
1114
+
1115
+ // Register the leaf as executed to prevent double-spending.
1116
+ _executedFor[emergencyExitAddress].set(index);
1117
+ }
1118
+
1119
+ // Calculate the root based on the leaf, the branch, and the index.
1120
+ // Compare to the current root, Revert if they do not match.
1121
+ _validateBranchRoot({
1122
+ expectedRoot: _outboxOf[terminalToken].tree.root(),
1123
+ projectTokenCount: projectTokenCount,
1124
+ terminalTokenAmount: terminalTokenAmount,
1125
+ beneficiary: beneficiary,
1126
+ index: index,
1127
+ leaves: leaves
1128
+ });
1129
+ }
1130
+
1131
+ /// @notice Validates a branch root against the expected root.
1132
+ /// @dev This is a virtual function to allow a tests to override the behavior, it should never be overwritten
1133
+ /// otherwise.
1134
+ function _validateBranchRoot(
1135
+ bytes32 expectedRoot,
1136
+ uint256 projectTokenCount,
1137
+ uint256 terminalTokenAmount,
1138
+ bytes32 beneficiary,
1139
+ uint256 index,
1140
+ bytes32[_TREE_DEPTH] calldata leaves
1141
+ )
1142
+ internal
1143
+ virtual
1144
+ {
1145
+ // Calculate the root based on the leaf, the branch, and the index.
1146
+ bytes32 root = MerkleLib.branchRoot({
1147
+ _item: _buildTreeHash({
1148
+ projectTokenCount: projectTokenCount, terminalTokenAmount: terminalTokenAmount, beneficiary: beneficiary
1149
+ }),
1150
+ _branch: leaves,
1151
+ _index: index
1152
+ });
1153
+
1154
+ // Compare to the current root, Revert if they do not match.
1155
+ if (root != expectedRoot) {
1156
+ revert JBSucker_InvalidProof(root, expectedRoot);
1157
+ }
1158
+ }
1159
+ }