@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 +1 -1
- package/src/JBSwapCCIPSucker.sol +117 -15
package/package.json
CHANGED
package/src/JBSwapCCIPSucker.sol
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
678
|
-
|
|
679
|
-
|
|
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
|
}
|