@bananapus/suckers-v6 0.0.58 → 0.0.59

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.58",
3
+ "version": "0.0.59",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/JBSucker.sol CHANGED
@@ -140,6 +140,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
140
140
  /// @notice The address of this contract's deployer.
141
141
  address public override deployer;
142
142
 
143
+ /// @notice The keccak256 hash of the leaf data committed at execution time, keyed by `(terminalToken,
144
+ /// leafIndex)`. Beneficiary contracts (e.g. `JBReferralSplitHook`) use this to authenticate post-hoc
145
+ /// settlement when their `claim()` call was front-run by a direct external caller — they re-derive the
146
+ /// hash from the claim data they hold and compare. Returns `bytes32(0)` for unexecuted indices —
147
+ /// `_buildTreeHash` is pre-image-resistant so zero unambiguously means "not executed".
148
+ /// @custom:param token The token whose inbox tree contains the leaf.
149
+ /// @custom:param index The leaf's index in the inbox tree.
150
+ mapping(address token => mapping(uint256 index => bytes32)) public override executedLeafHashOf;
151
+
143
152
  /// @notice The last known total token supply on the peer chain, updated each time a bridge message is received.
144
153
  /// @dev Used by data hooks to compute `effectiveTotalSupply = localSupply + sum(peerChainTotalSupply)` across all
145
154
  /// suckers, preventing cash out tax bypass on chains where a holder dominates the local supply.
@@ -925,36 +934,31 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
925
934
  revert JBSucker_NoTerminalForToken({projectId: cachedProjectId, token: token});
926
935
  }
927
936
 
928
- // Perform the `addToBalance` for ERC-20 tokens.
929
- if (token != JBConstants.NATIVE_TOKEN) {
930
- // Record the balance before the transfer for the sanity check.
931
- uint256 balanceBefore = IERC20(token).balanceOf(address(this));
932
-
933
- // Approve the terminal to spend the ERC-20 tokens.
937
+ // Native and ERC-20 differ only in (a) value attachment to the call, and (b) ERC-20 requires an
938
+ // allowance grant + post-transfer balance assertion to catch fee-on-transfer / non-conforming tokens.
939
+ // The terminal call itself is identical for both, so it lives outside the branch.
940
+ uint256 nativeValue;
941
+ uint256 balanceBefore;
942
+ bool isErc20 = token != JBConstants.NATIVE_TOKEN;
943
+ if (isErc20) {
944
+ balanceBefore = IERC20(token).balanceOf(address(this));
934
945
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
946
+ } else {
947
+ nativeValue = amount;
948
+ }
935
949
 
936
- // Add the tokens to the project's balance.
937
- terminal.addToBalanceOf({
938
- projectId: cachedProjectId,
939
- token: token,
940
- amount: amount,
941
- shouldReturnHeldFees: false,
942
- memo: "",
943
- metadata: ""
944
- });
950
+ terminal.addToBalanceOf{value: nativeValue}({
951
+ projectId: cachedProjectId,
952
+ token: token,
953
+ amount: amount,
954
+ shouldReturnHeldFees: false,
955
+ memo: "",
956
+ metadata: ""
957
+ });
945
958
 
946
- // Sanity check: make sure we transferred the full amount.
959
+ if (isErc20) {
960
+ // Sanity check: catches fee-on-transfer / non-conforming ERC-20s that move less than `amount`.
947
961
  assert(IERC20(token).balanceOf(address(this)) == balanceBefore - amount);
948
- } else {
949
- // If the token is the native token, send ETH with the call.
950
- terminal.addToBalanceOf{value: amount}({
951
- projectId: cachedProjectId,
952
- token: token,
953
- amount: amount,
954
- shouldReturnHeldFees: false,
955
- memo: "",
956
- metadata: ""
957
- });
958
962
  }
959
963
  }
960
964
 
@@ -1320,15 +1324,21 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1320
1324
  // Register the leaf as executed to prevent double-spending.
1321
1325
  _executedFor[terminalToken].set(index);
1322
1326
 
1323
- // Calculate the root and compare it to the current inbox root.
1324
- _validateBranchRoot({
1325
- expectedRoot: _inboxOf[terminalToken].root,
1327
+ // Compute the leaf hash once. It's used twice: stored in `executedLeafHashOf` (so beneficiary contracts
1328
+ // can authenticate post-hoc settlement when their `claim()` was front-run) and passed to
1329
+ // `_validateBranchRoot` for merkle verification. The bare executed bitmap proves "some leaf at index I
1330
+ // was executed" but not "which leaf"; storing the hash binds the index to the actual leaf content.
1331
+ bytes32 leafHash = _buildTreeHash({
1326
1332
  projectTokenCount: projectTokenCount,
1327
1333
  terminalTokenAmount: terminalTokenAmount,
1328
1334
  beneficiary: beneficiary,
1329
- metadata: metadata,
1330
- index: index,
1331
- leaves: leaves
1335
+ metadata: metadata
1336
+ });
1337
+ executedLeafHashOf[terminalToken][index] = leafHash;
1338
+
1339
+ // Calculate the root and compare it to the current inbox root.
1340
+ _validateBranchRoot({
1341
+ expectedRoot: _inboxOf[terminalToken].root, leafHash: leafHash, index: index, leaves: leaves
1332
1342
  });
1333
1343
  }
1334
1344
 
@@ -1336,17 +1346,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1336
1346
  /// @dev This is a virtual function to allow a tests to override the behavior, it should never be overwritten
1337
1347
  /// otherwise.
1338
1348
  /// @param expectedRoot The expected merkle root to validate against.
1339
- /// @param projectTokenCount The number of project tokens in the leaf.
1340
- /// @param terminalTokenAmount The amount of terminal tokens in the leaf.
1341
- /// @param beneficiary The beneficiary address in the leaf (bytes32 for cross-VM compatibility).
1349
+ /// @param leafHash The precomputed leaf hash (`_buildTreeHash` output) for the leaf being validated.
1342
1350
  /// @param index The index of the leaf in the merkle tree.
1343
1351
  /// @param leaves The merkle branch proving the leaf's inclusion.
1344
1352
  function _validateBranchRoot(
1345
1353
  bytes32 expectedRoot,
1346
- uint256 projectTokenCount,
1347
- uint256 terminalTokenAmount,
1348
- bytes32 beneficiary,
1349
- bytes32 metadata,
1354
+ bytes32 leafHash,
1350
1355
  uint256 index,
1351
1356
  bytes32[_TREE_DEPTH] calldata leaves
1352
1357
  )
@@ -1355,16 +1360,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1355
1360
  {
1356
1361
  // Calculate the root based on the leaf, the branch, and the index.
1357
1362
  // Delegates to JBSuckerLib (via DELEGATECALL) to keep MerkleLib.branchRoot bytecode out of each sucker.
1358
- bytes32 root = JBSuckerLib.computeBranchRoot({
1359
- item: _buildTreeHash({
1360
- projectTokenCount: projectTokenCount,
1361
- terminalTokenAmount: terminalTokenAmount,
1362
- beneficiary: beneficiary,
1363
- metadata: metadata
1364
- }),
1365
- branch: leaves,
1366
- index: index
1367
- });
1363
+ bytes32 root = JBSuckerLib.computeBranchRoot({item: leafHash, branch: leaves, index: index});
1368
1364
 
1369
1365
  // Revert if the computed root does not match the expected inbox root.
1370
1366
  if (root != expectedRoot) {
@@ -1451,10 +1447,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1451
1447
  // Calculate the root and compare it to the current outbox root.
1452
1448
  _validateBranchRoot({
1453
1449
  expectedRoot: _computeOutboxRoot(_outboxOf[terminalToken].tree),
1454
- projectTokenCount: projectTokenCount,
1455
- terminalTokenAmount: terminalTokenAmount,
1456
- beneficiary: beneficiary,
1457
- metadata: metadata,
1450
+ leafHash: _buildTreeHash({
1451
+ projectTokenCount: projectTokenCount,
1452
+ terminalTokenAmount: terminalTokenAmount,
1453
+ beneficiary: beneficiary,
1454
+ metadata: metadata
1455
+ }),
1458
1456
  index: index,
1459
1457
  leaves: leaves
1460
1458
  });
@@ -109,6 +109,18 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
109
109
  // ------------------------- external views -------------------------- //
110
110
  //*********************************************************************//
111
111
 
112
+ /// @notice All suckers for a project, INCLUDING deprecated entries that are no longer listed in `suckersOf`.
113
+ /// @dev Used by consumers that need to detect "has any sucker ever peered to chain X?" — e.g. to prevent
114
+ /// premature burn of bridgeable credit by `JBReferralSplitHook.burnUnbridgeableCreditFor`. Returns every key
115
+ /// from `_suckersOf[projectId]` regardless of active/deprecated state. Order matches the underlying
116
+ /// `EnumerableMap` iteration order (insertion order, with swap-and-pop semantics on removal — which this
117
+ /// registry doesn't trigger, since deprecation transitions to `_SUCKER_DEPRECATED` rather than deleting).
118
+ /// @param projectId The ID of the project to get the suckers of.
119
+ /// @return suckers The addresses of every sucker ever registered for `projectId`.
120
+ function allSuckersOf(uint256 projectId) external view override returns (address[] memory suckers) {
121
+ return _suckersOf[projectId].keys();
122
+ }
123
+
112
124
  /// @notice Whether the given address is a sucker (active or deprecated) that was deployed through this registry for
113
125
  /// the specified project. Used by controllers to authorize mint calls from suckers.
114
126
  /// @param projectId The ID of the project to check for.
@@ -39,7 +39,9 @@ import {JBSwapPoolLib} from "./libraries/JBSwapPoolLib.sol";
39
39
 
40
40
  // Local: structs (alphabetized).
41
41
  import {JBClaim} from "./structs/JBClaim.sol";
42
+ import {JBConversionRate} from "./structs/JBConversionRate.sol";
42
43
  import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
44
+ import {JBPendingSwap} from "./structs/JBPendingSwap.sol";
43
45
  import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
44
46
 
45
47
  /// @notice A `JBCCIPSucker` extension that swaps between local and bridge tokens using the best
@@ -71,7 +73,7 @@ import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
71
73
  ///
72
74
  /// **Inbound swap resilience**: If a swap reverts during `ccipReceive` (due to insufficient
73
75
  /// liquidity, stale TWAP observations, or extreme price impact), the CCIP message still succeeds.
74
- /// The unswapped bridge tokens are stored in a `PendingSwap` and the merkle root is recorded
76
+ /// The unswapped bridge tokens are stored in a `JBPendingSwap` and the merkle root is recorded
75
77
  /// normally. Claims for the affected batch are gated until `retrySwap` is called successfully.
76
78
  /// Anyone can call `retrySwap` once swap conditions improve.
77
79
  ///
@@ -136,31 +138,11 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
136
138
  // ------------------- internal stored properties -------------------- //
137
139
  //*********************************************************************//
138
140
 
139
- /// @notice Bridge tokens from a failed inbound swap, stored for later retry via `retrySwap`.
140
- /// @custom:member bridgeToken The bridge token received from CCIP.
141
- /// @custom:member bridgeAmount Amount of bridge tokens to swap.
142
- /// @custom:member leafTotal Original leaf-denomination total (for conversion rate).
143
- struct PendingSwap {
144
- address bridgeToken;
145
- uint256 bridgeAmount;
146
- uint256 leafTotal;
147
- }
148
-
149
141
  /// @notice Pending (failed) inbound swaps, keyed by local token and batch nonce.
150
142
  /// @dev Populated when `ccipReceive` swap fails; cleared when `retrySwap` succeeds.
151
143
  /// @custom:param localToken The local token the swap targets.
152
144
  /// @custom:param nonce The CCIP nonce identifying the batch.
153
- mapping(address localToken => mapping(uint64 nonce => PendingSwap)) public pendingSwapOf;
154
-
155
- /// @notice Immutable conversion rate for one received root batch, keyed by nonce.
156
- /// @dev Each batch stores its total leaf and local amounts. Individual claims compute their
157
- /// scaled amount as `claimLeafAmount * localTotal / leafTotal` — no mutable state changes.
158
- /// @custom:member leafTotal Total leaf-denomination (source chain) amount for this batch.
159
- /// @custom:member localTotal Total local-denomination (after swap) amount for this batch.
160
- struct ConversionRate {
161
- uint256 leafTotal;
162
- uint256 localTotal;
163
- }
145
+ mapping(address localToken => mapping(uint64 nonce => JBPendingSwap)) public pendingSwapOf;
164
146
 
165
147
  /// @notice End leaf index (exclusive) for each received root batch, keyed by token and nonce.
166
148
  /// @custom:param token The local token address.
@@ -177,7 +159,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
177
159
  /// @notice Conversion rate for each received root batch, keyed by token and nonce.
178
160
  /// @custom:param token The local token address.
179
161
  /// @custom:param nonce The CCIP nonce identifying the batch.
180
- mapping(address token => mapping(uint64 nonce => ConversionRate)) internal _conversionRateOf;
162
+ mapping(address token => mapping(uint64 nonce => JBConversionRate)) internal _conversionRateOf;
181
163
 
182
164
  /// @notice Count of populated batch nonces per token. Appended exactly once per batch in
183
165
  /// `ccipReceive`, so it equals the number of received batches independent of CCIP ordering.
@@ -421,7 +403,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
421
403
  // Store pendingSwapOf for failed swaps now that nonce is validated.
422
404
  if (swapFailed) {
423
405
  pendingSwapOf[localToken][nonce] =
424
- PendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
406
+ JBPendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
425
407
  }
426
408
 
427
409
  // Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
@@ -435,11 +417,12 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
435
417
  // the swap produced a positive local amount.
436
418
  if (leafTotal > 0 && !swapFailed) {
437
419
  if (localAmount == 0 && deliveredAmount > 0) {
438
- pendingSwapOf[localToken][nonce] =
439
- PendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
420
+ pendingSwapOf[localToken][nonce] = JBPendingSwap({
421
+ bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal
422
+ });
440
423
  } else {
441
424
  _conversionRateOf[localToken][nonce] =
442
- ConversionRate({leafTotal: leafTotal, localTotal: localAmount});
425
+ JBConversionRate({leafTotal: leafTotal, localTotal: localAmount});
443
426
  }
444
427
  }
445
428
  } else {
@@ -504,7 +487,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
504
487
  }
505
488
  _retrySwapLocked = true;
506
489
 
507
- PendingSwap memory pending = pendingSwapOf[localToken][nonce];
490
+ JBPendingSwap memory pending = pendingSwapOf[localToken][nonce];
508
491
  if (pending.bridgeAmount == 0) {
509
492
  revert JBSwapCCIPSucker_NoPendingSwap({
510
493
  localToken: localToken, nonce: nonce, retrySwapLocked: _retrySwapLocked
@@ -515,7 +498,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
515
498
  _executeSwapOrRevert({tokenIn: pending.bridgeToken, tokenOut: localToken, amount: pending.bridgeAmount});
516
499
 
517
500
  // Update the conversion rate so claims can proceed, then clear the pending swap.
518
- _conversionRateOf[localToken][nonce] = ConversionRate({leafTotal: pending.leafTotal, localTotal: localAmount});
501
+ _conversionRateOf[localToken][nonce] = JBConversionRate({leafTotal: pending.leafTotal, localTotal: localAmount});
519
502
  delete pendingSwapOf[localToken][nonce];
520
503
 
521
504
  _retrySwapLocked = false;
@@ -571,7 +554,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
571
554
  if (pendingSwapOf[token][nonce].bridgeAmount > 0) {
572
555
  revert JBSwapCCIPSucker_SwapPending({nonce: nonce});
573
556
  }
574
- ConversionRate storage rate = _conversionRateOf[token][nonce];
557
+ JBConversionRate storage rate = _conversionRateOf[token][nonce];
575
558
  if (rate.leafTotal > 0) {
576
559
  amount = amount * rate.localTotal / rate.leafTotal;
577
560
  }
@@ -108,6 +108,18 @@ interface IJBSucker is IERC165 {
108
108
  /// @return The deployer address.
109
109
  function deployer() external view returns (address);
110
110
 
111
+ /// @notice The keccak256 hash of the leaf data committed at execution time, for the leaf at the given
112
+ /// `(terminalToken, index)`. Returns `bytes32(0)` for unexecuted indices.
113
+ /// @dev Beneficiary contracts (e.g. `JBReferralSplitHook`) use this to authenticate post-hoc settlement when
114
+ /// their `claim()` call was front-run by a direct external caller — they re-derive the hash from the claim
115
+ /// data they hold and compare. The hash is computed via `_buildTreeHash(projectTokenCount,
116
+ /// terminalTokenAmount, beneficiary, metadata)` and is pre-image-resistant, so zero unambiguously means
117
+ /// "not executed".
118
+ /// @param token The terminal token whose tree contains the leaf.
119
+ /// @param index The index of the leaf in the inbox tree.
120
+ /// @return hash The committed leaf hash (or `bytes32(0)` if unexecuted).
121
+ function executedLeafHashOf(address token, uint256 index) external view returns (bytes32 hash);
122
+
111
123
  /// @notice The inbox merkle tree root for a given token.
112
124
  /// @param token The local terminal token.
113
125
  /// @return The inbox tree root.
@@ -54,6 +54,14 @@ interface IJBSuckerRegistry {
54
54
  /// @return The projects contract.
55
55
  function PROJECTS() external view returns (IJBProjects);
56
56
 
57
+ /// @notice All suckers for a project, INCLUDING deprecated entries that are no longer listed in `suckersOf`.
58
+ /// @dev Used by consumers that need to detect "has any sucker ever peered to chain X?" — e.g. to prevent
59
+ /// premature burn of bridgeable credit by `JBReferralSplitHook.burnUnbridgeableCreditFor`. Returns every key
60
+ /// from `_suckersOf[projectId]` regardless of active/deprecated state.
61
+ /// @param projectId The ID of the project.
62
+ /// @return The addresses of every sucker ever registered for `projectId`.
63
+ function allSuckersOf(uint256 projectId) external view returns (address[] memory);
64
+
57
65
  /// @notice Returns true if the specified sucker belongs to the specified project and was deployed through this
58
66
  /// registry.
59
67
  /// @param projectId The ID of the project to check for.
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @notice Immutable conversion rate for one received root batch, keyed by nonce.
5
+ /// @dev Each batch stores its total leaf and local amounts. Individual claims compute their scaled amount as
6
+ /// `claimLeafAmount * localTotal / leafTotal` — no mutable state changes.
7
+ /// @custom:member leafTotal Total leaf-denomination (source chain) amount for this batch.
8
+ /// @custom:member localTotal Total local-denomination (after swap) amount for this batch.
9
+ struct JBConversionRate {
10
+ uint256 leafTotal;
11
+ uint256 localTotal;
12
+ }
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @notice Bridge tokens from a failed inbound swap, stored for later retry via `retrySwap`.
5
+ /// @custom:member bridgeToken The bridge token received from CCIP.
6
+ /// @custom:member bridgeAmount Amount of bridge tokens to swap.
7
+ /// @custom:member leafTotal Original leaf-denomination total (for conversion rate).
8
+ struct JBPendingSwap {
9
+ address bridgeToken;
10
+ uint256 bridgeAmount;
11
+ uint256 leafTotal;
12
+ }