@bananapus/suckers-v6 0.0.43 → 0.0.46

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/CHANGELOG.md CHANGED
@@ -15,6 +15,15 @@ This file describes the verified change from `nana-suckers-v5` to the current `n
15
15
  - `JBCeloSucker`
16
16
  - the deployers, structs, and interfaces under `src/`
17
17
 
18
+ ## 0.0.46 — Bump nana-core-v6 to 0.0.53
19
+
20
+ `@bananapus/core-v6@0.0.53` ([nana-core-v6 PR #145](https://github.com/Bananapus/nana-core-v6/pull/145)) drops the `via_ir` requirement on `JBCashOutHookSpecsLib`, which lets this package consume the cross-project cashout work (`payAfterCashOutTokensOf` / `addToBalanceAfterCashOutTokensOf`) without needing `via_ir = true` in its own foundry profile.
21
+
22
+ - No src changes — suckers doesn't reference `IJBFeeTerminal.FEE()` or any of the touched core surfaces.
23
+ - All `JBRulesetMetadata` test literals patched to include `pauseCrossProjectFeeFreeInflows: false`.
24
+
25
+ `package.json`: version `0.0.44 → 0.0.46` (skipping 0.0.45 because nothing shipped at that intermediate revision), core dep `^0.0.48 → ^0.0.53`.
26
+
18
27
  ## Summary
19
28
 
20
29
  - Cross-chain identifiers are now modeled for a wider address space. The v6 repo uses `bytes32` where the v5 repo used EVM `address` assumptions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.43",
3
+ "version": "0.0.46",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@arbitrum/nitro-contracts": "3.2.0",
33
- "@bananapus/core-v6": "^0.0.48",
33
+ "@bananapus/core-v6": "^0.0.53",
34
34
  "@bananapus/permission-ids-v6": "^0.0.25",
35
35
  "@chainlink/contracts-ccip": "1.6.4",
36
36
  "@chainlink/local": "0.2.7",
@@ -179,6 +179,19 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
179
179
  /// @custom:param token The local token address.
180
180
  mapping(address token => uint64) internal _highestReceivedNonce;
181
181
 
182
+ /// @notice Count of populated batch nonces per token. Appended exactly once per batch in
183
+ /// `ccipReceive`, so it equals the number of received batches independent of CCIP ordering.
184
+ /// @custom:param token The local token address.
185
+ mapping(address token => uint64) internal _populatedNonceCount;
186
+
187
+ /// @notice Populated batch nonces per token, indexed by insertion order. Enables the
188
+ /// empty-midpoint fallback in `_findNonceForLeafIndex` to walk only the K populated nonces
189
+ /// (O(K) worst case) instead of the full [lo, hi] nonce span (O(N) worst case under sparse
190
+ /// adversarial patterns).
191
+ /// @custom:param token The local token address.
192
+ /// @custom:param index The insertion index in [0, _populatedNonceCount[token]).
193
+ mapping(address token => mapping(uint64 index => uint64 nonce)) internal _populatedNonceByIndex;
194
+
182
195
  /// @notice Cumulative leaf count at the last `_sendRootOverAMB` call, per token.
183
196
  /// @dev Used on the sender side to derive the batch start index for the next send.
184
197
  /// @custom:param token The local token address.
@@ -366,8 +379,33 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
366
379
  // Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
367
380
  // independently of nonce ordering. Each nonce is self-describing: [start, end).
368
381
  if (batchEnd > 0) {
382
+ // Record this batch's half-open leaf range `[batchStart, batchEnd)`. Self-
383
+ // describing per-nonce — no implicit chain across nonces — so out-of-order
384
+ // delivery can still resolve a leaf to its batch.
369
385
  _batchStartOf[localToken][nonce] = batchStart;
370
386
  _batchEndOf[localToken][nonce] = batchEnd;
387
+
388
+ // Append `nonce` to the populated-nonce list for this token. The outer
389
+ // `_batchEndOf == 0 && _conversionRateOf == 0 && pendingSwapOf == 0` guard
390
+ // fires at most once per (token, nonce), so each populated nonce is appended
391
+ // exactly once — the array stays duplicate-free without extra checks.
392
+ //
393
+ // Reading `_populatedNonceCount[localToken]` first into a local lets us write
394
+ // the new slot and the new count in a single read-modify-write pair (one
395
+ // SLOAD, two SSTOREs to distinct slots). The `unchecked` increment is safe:
396
+ // `priorCount` is bounded by the total number of populated nonces, which is
397
+ // upper-bounded by the CCIP nonce space (`uint64`) — overflow requires more
398
+ // batches than `uint64.max`, which the inbox can never produce.
399
+ uint64 priorCount = _populatedNonceCount[localToken];
400
+ _populatedNonceByIndex[localToken][priorCount] = nonce;
401
+ unchecked {
402
+ _populatedNonceCount[localToken] = priorCount + 1;
403
+ }
404
+
405
+ // Track the highest nonce ever observed for this token. Read by
406
+ // `_findNonceForLeafIndex` as the binary-search upper bound. Out-of-order
407
+ // delivery keeps this monotonic — we only advance it when the new nonce is
408
+ // strictly higher than the prior maximum.
371
409
  if (nonce > _highestReceivedNonce[localToken]) {
372
410
  _highestReceivedNonce[localToken] = nonce;
373
411
  }
@@ -672,10 +710,10 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
672
710
  /// monotonic even when CCIP delivery is out of order and leaves intermediate slots empty.
673
711
  /// Binary search exploits that monotonicity for O(log N) lookup.
674
712
  /// @dev Gap handling: when the midpoint slot is empty (CCIP out-of-order delivery or sparse
675
- /// attacker writes), the search falls back to a single linear scan over the remaining window.
676
- /// This matches the previous linear-scan cost in the worst case, so the change is never worse
677
- /// than the prior implementation. The fallback also keeps bytecode small enough that
678
- /// JBSwapCCIPSucker stays under EIP-170.
713
+ /// attacker writes), the fallback walks `_populatedNonceByIndex` the list of every nonce
714
+ /// actually populated. That bounds the worst case at `O(K)` SLOADs, where `K` is the number
715
+ /// of received batches, regardless of how sparse the populated set is inside `[1, maxNonce]`.
716
+ /// The empty-slot scans that drove `O(N)` under the prior linear fallback are eliminated.
679
717
  /// @param token The local token address.
680
718
  /// @param leafIndex The leaf index from the claim.
681
719
  /// @return The nonce of the batch containing this leaf, or 0 if no batches have been recorded.
@@ -700,16 +738,40 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
700
738
  // write guard in `ccipReceive`); a real batch always has `batchEnd > batchStart`.
701
739
  uint256 end = _batchEndOf[token][mid];
702
740
 
703
- // Empty midpoint from out-of-order CCIP delivery. Fall back to a tight linear
704
- // scan over the remaining window same complexity as the pre-fix path, but only
705
- // on the gap branch (the audit's dense-nonce attack never trips this).
741
+ // Empty midpoint from out-of-order CCIP delivery (the sender minted a higher nonce
742
+ // than the inbox has yet received, leaving holes in `[1, maxNonce]`) or a sparse
743
+ // pattern an attacker assembled by inflating `_highestReceivedNonce` without
744
+ // populating intermediate slots. The earlier linear `[lo, hi]` scan walked every
745
+ // empty slot, so worst-case cost was `O(maxNonce)` SLOADs — that's the residual
746
+ // the audit flagged. Walk `_populatedNonceByIndex` instead: it's `K` entries (one
747
+ // per received batch) and contains exactly the slots a real batch can cover.
706
748
  if (end == 0) {
707
- for (uint64 n = lo; n <= hi; n++) {
749
+ // Number of populated batch slots for this token equal to the array length.
750
+ // Maintained by `ccipReceive`'s append (one SSTORE per first-time receive).
751
+ uint64 count = _populatedNonceCount[token];
752
+
753
+ // Linear walk over the populated set. Bounded by `count`, not `maxNonce`, so
754
+ // sparse adversarial patterns can't inflate this loop with empty slots.
755
+ for (uint64 i; i < count; i++) {
756
+ // Insertion-ordered nonce at index `i`. May be any value in `[1, maxNonce]`
757
+ // because CCIP delivery is out-of-order; the array is not sorted by nonce.
758
+ uint64 n = _populatedNonceByIndex[token][i];
759
+
760
+ // Read end of this batch's leaf range. Always `> 0` here — we only push to
761
+ // `_populatedNonceByIndex` after the corresponding `_batchEndOf` write.
708
762
  uint256 nEnd = _batchEndOf[token][n];
709
- if (nEnd == 0) continue;
710
- uint256 nStart = _batchStartOf[token][n];
711
- if (leafIndex >= nStart && leafIndex < nEnd) return n;
763
+
764
+ // Coverage test: each batch's `[batchStart, batchEnd)` is non-overlapping
765
+ // across populated nonces, so a hit is unique. The `[lo, hi]` window from
766
+ // the binary search isn't applied — the binary-search invariant guarantees
767
+ // the leaf can only live in a populated nonce, so an out-of-window match
768
+ // would mean the binary search was already wrong; falling through to the
769
+ // next iteration costs less than the extra two compares per iteration.
770
+ if (leafIndex >= _batchStartOf[token][n] && leafIndex < nEnd) return n;
712
771
  }
772
+
773
+ // No populated nonce contains `leafIndex` for this token. Break out of the
774
+ // outer binary-search loop and fall through to the shared revert below.
713
775
  break;
714
776
  }
715
777