@bananapus/suckers-v6 0.0.63 → 0.0.64

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/foundry.toml CHANGED
@@ -5,6 +5,9 @@ evm_version = 'cancun'
5
5
  optimizer_runs = 200
6
6
  libs = ["node_modules", "lib"]
7
7
  fs_permissions = [{ access = "read-write", path = "./"}]
8
+ # Archived, unused contracts (e.g. JBSwapCCIPSucker) and their tests live under `archive/` folders and are
9
+ # excluded from compilation, sizing, and test runs. Kept for reference only.
10
+ skip = ["src/archive/**", "test/archive/**"]
8
11
 
9
12
  [fuzz]
10
13
  runs = 4096
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.63",
3
+ "version": "0.0.64",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/JBSucker.sol CHANGED
@@ -109,6 +109,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
109
109
  // ------------------------- internal constants ----------------------- //
110
110
  //*********************************************************************//
111
111
 
112
+ /// @notice The number of recently-accepted inbox roots retained per token so that a proof generated against a
113
+ /// slightly older root still validates after a later `toRemote`/`fromRemote` advances the inbox.
114
+ /// @dev The inbox is append-only and a leaf's `(hash, index)` is stable across roots, so honoring a small window
115
+ /// of recent roots is safe: the `_executedFor` double-spend guard is keyed by `(token, leafIndex)` — independent
116
+ /// of which retained root validated the proof — so an executed leaf stays blocked no matter which retained root a
117
+ /// later proof matches. The window only widens which still-valid proofs are accepted; it never relaxes the
118
+ /// double-spend guard.
119
+ uint256 internal constant _INBOX_ROOT_RING_SIZE = 4;
120
+
112
121
  /// @notice The depth of the merkle tree used to store the outbox and inbox.
113
122
  uint32 internal constant _TREE_DEPTH = 32;
114
123
 
@@ -184,6 +193,22 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
184
193
  /// @custom:param token The local terminal token to get the inbox for.
185
194
  mapping(address token => JBInboxTreeRoot root) internal _inboxOf;
186
195
 
196
+ /// @notice The index of the most recently-written slot in `_inboxRootRingOf[token]`.
197
+ /// @dev Advances modulo `_INBOX_ROOT_RING_SIZE` each time `fromRemote` accepts a newer-nonce root, overwriting the
198
+ /// oldest retained root. Defaults to `0`; the first accepted root is written to slot `1` after the pre-increment.
199
+ /// @custom:param token The local terminal token to get the ring cursor for.
200
+ mapping(address token => uint256 cursor) internal _inboxRootRingCursorOf;
201
+
202
+ /// @notice A small ring buffer of the most recently-accepted inbox roots for a given token.
203
+ /// @dev Holds the last `_INBOX_ROOT_RING_SIZE` distinct roots accepted by `fromRemote` (the newest is also mirrored
204
+ /// in `_inboxOf[token].root`). `_validate` accepts a proof matching ANY retained, not-yet-executed leaf's root, so
205
+ /// proofs generated against a recent-but-superseded root keep validating without regenerated branches. The window
206
+ /// is intentionally small: it bounds storage/gas and keeps the set of accepted roots tightly recent. Unused slots
207
+ /// are `bytes32(0)`, which `_validate` skips (a real root is never `bytes32(0)` — the empty-tree root is
208
+ /// `MerkleLib.Z_32`, and roots only enter the ring once a non-empty tree has been bridged).
209
+ /// @custom:param token The local terminal token to get the retained inbox roots for.
210
+ mapping(address token => bytes32[_INBOX_ROOT_RING_SIZE] roots) internal _inboxRootRingOf;
211
+
187
212
  /// @notice The local token that has reserved each remote token address in this sucker.
188
213
  /// @dev Inbound roots are keyed by `root.token` on the destination chain. Within a single sucker, allowing two
189
214
  /// local tokens to send roots to the same remote token would give them independent source nonces but one shared
@@ -286,12 +311,27 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
286
311
 
287
312
  /// @notice Claim multiple bridged entries in a single transaction. Each claim mints project tokens for the
288
313
  /// beneficiary and deposits the corresponding terminal tokens into the project's local balance.
314
+ /// @dev Per-leaf resilience: each leaf is claimed through an external `this.claim(JBClaim)` sub-call wrapped in
315
+ /// try/catch, so a single failing or stale leaf (e.g. its inbox root has not arrived yet, it was already executed,
316
+ /// or a transient mint/add-to-balance dependency reverts) only skips that one leaf — the rest of the batch still
317
+ /// settles. Because the sub-call is a separate message frame, a caught revert rolls back every state change that
318
+ /// leaf attempted (its `_executedFor` bit, its `executedLeafHashOf` entry, and any `_addToBalance`/`mintTokensOf`
319
+ /// effects), so the skipped leaf stays fully claimable later. Routing through `this.claim` is safe because the
320
+ /// single-leaf `claim` mints to `claimData.leaf.beneficiary` and adds funds to the project balance — it never
321
+ /// depends on `msg.sender` being the original batch caller, so the self-call does not change who is credited.
289
322
  /// @param claims A list of claims to perform (including the terminal token, merkle tree leaf, and proof for each
290
323
  /// claim).
291
324
  function claim(JBClaim[] calldata claims) external override {
292
- // Claim each.
325
+ // Claim each. Isolate each leaf in its own external sub-call so one bad/stale leaf cannot revert the batch.
293
326
  for (uint256 i; i < claims.length;) {
294
- claim(claims[i]);
327
+ try this.claim(claims[i]) {
328
+ // Leaf settled successfully.
329
+ }
330
+ catch {
331
+ // The leaf failed: its sub-call reverted atomically, leaving no persisted state for it. Surface the
332
+ // skip for off-chain monitoring; the leaf remains claimable in a future call.
333
+ emit ClaimFailed({token: claims[i].token, index: claims[i].leaf.index, caller: _msgSender()});
334
+ }
295
335
  unchecked {
296
336
  ++i;
297
337
  }
@@ -448,6 +488,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
448
488
  inbox.nonce = root.remoteRoot.nonce;
449
489
  inbox.root = root.remoteRoot.root;
450
490
 
491
+ // Retain the newly-accepted root in the per-token ring so proofs generated against a recent-but-superseded
492
+ // root still validate. Advance the cursor and overwrite the oldest slot. Skipping the empty-tree root keeps
493
+ // the ring populated only with roots that can actually back a claim.
494
+ if (root.remoteRoot.root != MerkleLib.Z_32) {
495
+ uint256 nextCursor = (_inboxRootRingCursorOf[localToken] + 1) % _INBOX_ROOT_RING_SIZE;
496
+ _inboxRootRingCursorOf[localToken] = nextCursor;
497
+ _inboxRootRingOf[localToken][nextCursor] = root.remoteRoot.root;
498
+ }
499
+
451
500
  emit NewInboxTreeRoot({
452
501
  token: localToken, nonce: root.remoteRoot.nonce, root: root.remoteRoot.root, caller: _msgSender()
453
502
  });
@@ -1345,10 +1394,21 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1345
1394
  });
1346
1395
  executedLeafHashOf[terminalToken][index] = leafHash;
1347
1396
 
1348
- // Calculate the root and compare it to the current inbox root.
1349
- _validateBranchRoot({
1350
- expectedRoot: _inboxOf[terminalToken].root, leafHash: leafHash, index: index, leaves: leaves
1351
- });
1397
+ // Select which retained inbox root this proof should be validated against. A proof generated against any of
1398
+ // the last `_INBOX_ROOT_RING_SIZE` accepted roots is honored, not only the latest, so a proof does not become
1399
+ // unusable the instant a newer root arrives. If the proof matches no retained root, the latest root is used as
1400
+ // the fallback so the failure path reverts with the canonical `JBSucker_InvalidProof` against the live root.
1401
+ //
1402
+ // This widening is double-spend-safe: the `_executedFor[terminalToken]` guard above is keyed by leaf `index`,
1403
+ // not by root. The merkle branch binds `(leafHash, index)` to whichever retained root it matches, so the same
1404
+ // leaf carries the same `index` regardless of which retained root proves it — once executed, every later
1405
+ // proof
1406
+ // for that leaf (against any retained root) is rejected by the bitmap before reaching this point.
1407
+ bytes32 expectedRoot =
1408
+ _selectRetainedInboxRoot({terminalToken: terminalToken, leafHash: leafHash, index: index, leaves: leaves});
1409
+
1410
+ // Calculate the root and compare it to the selected retained inbox root.
1411
+ _validateBranchRoot({expectedRoot: expectedRoot, leafHash: leafHash, index: index, leaves: leaves});
1352
1412
  }
1353
1413
 
1354
1414
  /// @notice Validates a branch root against the expected root.
@@ -1628,6 +1688,51 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1628
1688
  }
1629
1689
  }
1630
1690
 
1691
+ /// @notice Selects which retained inbox root a proof should be validated against, honoring a small window of
1692
+ /// recently-accepted roots rather than only the latest.
1693
+ /// @dev Computes the branch root implied by the proof once, then returns the first retained ring root it matches.
1694
+ /// Falls back to the latest inbox root (`_inboxOf[terminalToken].root`) when the proof matches no retained root, so
1695
+ /// the caller's subsequent `_validateBranchRoot` reverts against the live root exactly as it did before the ring
1696
+ /// existed. This is `view` and side-effect free; the double-spend guard lives entirely in `_validate`'s bitmap.
1697
+ /// @param terminalToken The terminal token whose retained inbox roots are searched.
1698
+ /// @param leafHash The precomputed leaf hash for the leaf being validated.
1699
+ /// @param index The index of the leaf in the inbox tree.
1700
+ /// @param leaves The merkle branch proving the leaf's inclusion.
1701
+ /// @return expectedRoot The retained root the proof matches, or the latest inbox root if none match.
1702
+ function _selectRetainedInboxRoot(
1703
+ address terminalToken,
1704
+ bytes32 leafHash,
1705
+ uint256 index,
1706
+ bytes32[_TREE_DEPTH] calldata leaves
1707
+ )
1708
+ internal
1709
+ view
1710
+ virtual
1711
+ returns (bytes32 expectedRoot)
1712
+ {
1713
+ // The latest accepted root. Used as the fallback so the failure path is unchanged.
1714
+ bytes32 latestRoot = _inboxOf[terminalToken].root;
1715
+
1716
+ // Compute the root implied by this proof once.
1717
+ bytes32 computedRoot = JBSuckerLib.computeBranchRoot({item: leafHash, branch: leaves, index: index});
1718
+
1719
+ // Honor the latest root first (the common case), then any other retained root in the ring.
1720
+ if (computedRoot == latestRoot) return latestRoot;
1721
+
1722
+ bytes32[_INBOX_ROOT_RING_SIZE] storage ring = _inboxRootRingOf[terminalToken];
1723
+ for (uint256 i; i < _INBOX_ROOT_RING_SIZE;) {
1724
+ bytes32 retained = ring[i];
1725
+ // Skip empty slots; a real inbox root is never `bytes32(0)`.
1726
+ if (retained != bytes32(0) && computedRoot == retained) return retained;
1727
+ unchecked {
1728
+ ++i;
1729
+ }
1730
+ }
1731
+
1732
+ // No retained root matched. Fall back to the latest root so `_validateBranchRoot` reverts against it.
1733
+ return latestRoot;
1734
+ }
1735
+
1631
1736
  /// @notice Convert a bytes32 remote address to a local EVM address.
1632
1737
  /// @param remote The bytes32 representation of the address.
1633
1738
  /// @return The EVM address (lower 20 bytes).
@@ -34,6 +34,15 @@ interface IJBSucker is IERC165 {
34
34
  address caller
35
35
  );
36
36
 
37
+ /// @notice Emitted when a single leaf in a batch `claim(JBClaim[])` fails so the rest of the batch can proceed.
38
+ /// @dev The failing leaf's state changes are fully reverted (the batch routes each leaf through an external
39
+ /// `this.claim` sub-call), so the leaf remains claimable later once the underlying cause is resolved (e.g. its
40
+ /// inbox root arrives, or a transient mint/add-to-balance dependency recovers).
41
+ /// @param token The terminal token address of the failing leaf.
42
+ /// @param index The leaf index in the inbox tree.
43
+ /// @param caller The address that submitted the batch.
44
+ event ClaimFailed(address indexed token, uint256 index, address caller);
45
+
37
46
  /// @notice Emitted when a leaf is inserted into the outbox tree.
38
47
  /// @param beneficiary The beneficiary on the remote chain.
39
48
  /// @param token The terminal token address.
File without changes
File without changes
File without changes
File without changes