@bananapus/suckers-v6 0.0.42 → 0.0.44

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.42",
3
+ "version": "0.0.44",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
  }
@@ -666,29 +704,93 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
666
704
  //*********************************************************************//
667
705
 
668
706
  /// @notice Find the nonce whose batch contains the given leaf index.
669
- /// @dev Scans from the newest nonce backwards so recent batches resolve first without storing extra cache state.
707
+ /// @dev Binary search by nonce. Source-side `prepare()` appends batches in nonce order with each
708
+ /// new batch's `batchStart` equal to the previous batch's `batchEnd`, so across populated
709
+ /// destination slots `_batchStartOf` is strictly increasing in nonce — the populated subset is
710
+ /// monotonic even when CCIP delivery is out of order and leaves intermediate slots empty.
711
+ /// Binary search exploits that monotonicity for O(log N) lookup.
712
+ /// @dev Gap handling: when the midpoint slot is empty (CCIP out-of-order delivery or sparse
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.
670
717
  /// @param token The local token address.
671
718
  /// @param leafIndex The leaf index from the claim.
672
- /// @return The nonce of the batch containing this leaf, or 0 if no conversion rates exist.
719
+ /// @return The nonce of the batch containing this leaf, or 0 if no batches have been recorded.
673
720
  function _findNonceForLeafIndex(address token, uint256 leafIndex) internal view returns (uint64) {
721
+ // `_highestReceivedNonce` upper-bounds any populated slot for this token; zero means
722
+ // nothing has been received yet, so the leaf belongs to no batch.
674
723
  uint64 maxNonce = _highestReceivedNonce[token];
675
724
  if (maxNonce == 0) return 0;
676
725
 
677
- // Scan from most recent nonce backwards so the normal just-bridged claim path exits quickly.
678
- for (uint64 n = maxNonce; n >= 1; n--) {
679
- if (_nonceContainsLeaf({token: token, nonce: n, leafIndex: leafIndex})) return n;
726
+ // Nonce 0 is reserved by inbox initialization and never holds a batch, so search [1, max].
727
+ uint64 lo = 1;
728
+ uint64 hi = maxNonce;
729
+
730
+ // Wrap arithmetic in `unchecked` — `lo` and `hi` are always in `[1, maxNonce]` and the
731
+ // edge-guards (`mid == lo` / `mid == hi` breaks) prevent the only ways `mid - 1` or
732
+ // `mid + 1` could over/underflow. Skipping the compiler checks shaves enough bytecode to
733
+ // stay under EIP-170 without changing semantics.
734
+ unchecked {
735
+ while (lo <= hi) {
736
+ uint64 mid = lo + (hi - lo) / 2;
737
+ // `batchEnd == 0` is the established sentinel for "no batch recorded" (see the
738
+ // write guard in `ccipReceive`); a real batch always has `batchEnd > batchStart`.
739
+ uint256 end = _batchEndOf[token][mid];
740
+
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.
748
+ if (end == 0) {
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.
762
+ uint256 nEnd = _batchEndOf[token][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;
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.
775
+ break;
776
+ }
777
+
778
+ // Each batch covers [batchStart, batchEnd). Across populated nonces these ranges
779
+ // are non-overlapping and strictly increasing, so the standard comparison applies.
780
+ uint256 start = _batchStartOf[token][mid];
781
+ if (leafIndex < start) {
782
+ if (mid == lo) break; // Guard against `mid - 1` underflow at the lower edge.
783
+ hi = mid - 1;
784
+ } else if (leafIndex >= end) {
785
+ if (mid == hi) break; // Mirror guard at the upper edge.
786
+ lo = mid + 1;
787
+ } else {
788
+ return mid; // `start <= leafIndex < end`: leaf is inside this batch.
789
+ }
790
+ }
680
791
  }
681
792
 
793
+ // Window collapsed without a hit — surface the same error the legacy linear scan used.
682
794
  revert JBSwapCCIPSucker_BatchNotReceived({nonce: 0});
683
795
  }
684
-
685
- /// @notice Check whether the given nonce's batch range contains the leaf index.
686
- /// @param token The local token address.
687
- /// @param nonce The nonce to check.
688
- /// @param leafIndex The leaf index to look for.
689
- /// @return True if the nonce's [start, end) range contains `leafIndex`.
690
- function _nonceContainsLeaf(address token, uint64 nonce, uint256 leafIndex) internal view returns (bool) {
691
- uint256 end = _batchEndOf[token][nonce];
692
- return end != 0 && leafIndex >= _batchStartOf[token][nonce] && leafIndex < end;
693
- }
694
796
  }