@bananapus/suckers-v6 0.0.78 → 0.0.79

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/src/JBSucker.sol CHANGED
@@ -10,7 +10,6 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
10
10
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
11
11
  import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
12
12
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
13
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
14
13
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
15
14
  import {JBFixedPointNumber} from "@bananapus/core-v6/src/libraries/JBFixedPointNumber.sol";
16
15
  import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
@@ -37,6 +36,7 @@ import {MerkleLib} from "./utils/MerkleLib.sol";
37
36
 
38
37
  // Local: structs (alphabetized)
39
38
  import {JBAccountingSnapshot} from "./structs/JBAccountingSnapshot.sol";
39
+ import {JBChainAccounting} from "./structs/JBChainAccounting.sol";
40
40
  import {JBClaim} from "./structs/JBClaim.sol";
41
41
  import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
42
42
  import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
@@ -185,6 +185,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
185
185
  /// double-spend guard.
186
186
  uint256 internal constant _INBOX_ROOT_RING_SIZE = 4;
187
187
 
188
+ /// @notice Extra destination gas budgeted per source accounting context carried in a gossip bundle.
189
+ /// @dev Covers the receiver's per-context storage writes in `_storeChainAccounting`, so the messaging gas limit
190
+ /// scales with the bundle (via `_messagingGasLimit`) instead of a fixed cap that would bound how large the
191
+ /// cross-chain mesh can grow before the destination call runs out of gas.
192
+ uint256 internal constant _MESSENGER_SOURCE_CONTEXT_GAS_LIMIT = 75_000;
193
+
188
194
  /// @notice The depth of the merkle tree used to store the outbox and inbox.
189
195
  uint32 internal constant _TREE_DEPTH = 32;
190
196
 
@@ -223,10 +229,13 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
223
229
  /// @custom:param index The leaf's index in the inbox tree.
224
230
  mapping(address token => mapping(uint256 index => bytes32)) public override executedLeafHashOf;
225
231
 
226
- /// @notice The last known total token supply on the peer chain, updated each time a bridge message is received.
227
- /// @dev Used by data hooks to compute `effectiveTotalSupply = localSupply + sum(peerChainTotalSupply)` across all
228
- /// suckers, preventing cash out tax bypass on chains where a holder dominates the local supply.
229
- uint256 public peerChainTotalSupply;
232
+ /// @notice The last known total token supply on each peer chain, updated each time a bridge message carries that
233
+ /// chain's accounting record.
234
+ /// @dev The registry sums the freshest value across every peer chain to drive cross-chain cash out tax, preventing
235
+ /// a holder who dominates one chain's local supply from bypassing the tax. Returns 0 for a chain no record has been
236
+ /// received for.
237
+ /// @custom:param chainId The peer chain to read the last known total supply of.
238
+ mapping(uint256 chainId => uint256) public peerChainTotalSupplyOf;
230
239
 
231
240
  /// @notice The total retained failed-fee ETH excluded from native add-to-balance accounting.
232
241
  uint256 public retainedToRemoteFeeBalance;
@@ -242,11 +251,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
242
251
  /// @custom:param account The address owed the retained ETH.
243
252
  mapping(address account => uint256 amount) public retainedTransportPaymentRefundOf;
244
253
 
245
- /// @notice The source chain freshness key for the most recent accepted peer snapshot.
246
- /// @dev Only snapshots with a strictly newer source freshness key are accepted, preventing stale rollbacks.
247
- /// Named to align with the `JBMessageRoot.sourceTimestamp` field it tracks.
248
- /// Returns 0 if no snapshot has been received yet.
249
- uint256 public snapshotTimestamp;
254
+ /// @notice The source-chain freshness key of the most recent accepted accounting record for each peer chain.
255
+ /// @dev A record for a peer chain is accepted only when its freshness key is strictly newer than the stored one,
256
+ /// so stale relays cannot roll back that chain's surplus, balance, or supply. Each peer chain is gated
257
+ /// independently. Returns 0 for a chain no record has been received for.
258
+ /// @custom:param chainId The peer chain to read the latest accepted freshness key of.
259
+ mapping(uint256 chainId => uint256) public snapshotTimestampOf;
250
260
 
251
261
  //*********************************************************************//
252
262
  // -------------------- internal stored properties ------------------- //
@@ -301,12 +311,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
301
311
  // -------------------- private stored properties -------------------- //
302
312
  //*********************************************************************//
303
313
 
304
- /// @notice Caches a local token's authoritative accounting-context currency, derived once and reused on later
305
- /// snapshots. A project's accounting-context currency is immutable once set, so the cached value never goes stale;
306
- /// only an authoritative read is cached (a not-yet-configured token uses the convention without caching, so a later
307
- /// snapshot re-reads it once its context exists).
308
- /// @custom:param token The local token.
309
- mapping(address token => uint32 currency) private _cachedCurrencyOf;
314
+ /// @notice Whether a peer chain already has an entry in `_peerChainIds`, so the enumerable set inserts each chain
315
+ /// at most once.
316
+ /// @custom:param chainId The peer chain to check membership of.
317
+ mapping(uint256 chainId => bool) private _isKnownPeerChainId;
310
318
 
311
319
  /// @notice The ID of the project (on the local chain) that this sucker is associated with.
312
320
  uint256 private _localProjectId;
@@ -318,10 +326,19 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
318
326
  /// @dev A zero value preserves the default same-address deterministic peer.
319
327
  bytes32 private _peer;
320
328
 
321
- /// @notice The peer chain's per-currency surplus and balance from the latest snapshot.
322
- /// @dev Rebuilt from each fresher snapshot; dropped contexts are absent without per-entry versioning or clearing.
323
- /// A read sums these and values them into the requested currency.
324
- JBPeerChainContext[] private _peerContexts;
329
+ /// @notice The set of peer chains this sucker holds an accounting record for.
330
+ /// @dev The registry enumerates this to aggregate every chain's value and to gather records for re-gossiping. A
331
+ /// chain is appended on its first accepted record and never removed, so the set stays small and bounded by the
332
+ /// project's chain count.
333
+ uint256[] private _peerChainIds;
334
+
335
+ /// @notice Each peer chain's raw, un-valued per-context surplus and balance from its latest accepted record.
336
+ /// @dev Stored exactly as received — in the source chain's own token addresses and decimals — so the record can
337
+ /// be
338
+ /// re-gossiped to other chains faithfully and resolved to local currencies at read time. A fresher record for a
339
+ /// chain replaces that chain's set from scratch; a context dropped by the fresher record simply vanishes.
340
+ /// @custom:param chainId The peer chain to read the raw contexts of.
341
+ mapping(uint256 chainId => JBSourceContext[]) private _peerContextsOf;
325
342
 
326
343
  //*********************************************************************//
327
344
  // ---------------------------- constructor -------------------------- //
@@ -579,11 +596,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
579
596
  });
580
597
  }
581
598
 
582
- _storePeerChainAccounting({
583
- sourceTimestamp: root.sourceTimestamp,
584
- sourceTotalSupply: root.sourceTotalSupply,
585
- sourceContexts: root.sourceContexts
586
- });
599
+ // Store every accounting record in the gossip bundle that rode along with this root, keeping the freshest per
600
+ // source chain. The token-local inbox update above and this per-chain accounting are gated independently.
601
+ _storeAccountingBundle(root.accounts);
587
602
  }
588
603
 
589
604
  /// @notice Receive a peer-chain accounting snapshot from the remote sucker without updating any token-local inbox
@@ -606,11 +621,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
606
621
  revert JBSucker_InvalidMessageVersion({received: snapshot.version, expected: MESSAGE_VERSION});
607
622
  }
608
623
 
609
- _storePeerChainAccounting({
610
- sourceTimestamp: snapshot.sourceTimestamp,
611
- sourceTotalSupply: snapshot.sourceTotalSupply,
612
- sourceContexts: snapshot.sourceContexts
613
- });
624
+ // Store every accounting record in the gossip bundle, keeping the freshest per source chain.
625
+ _storeAccountingBundle(snapshot.accounts);
614
626
  }
615
627
 
616
628
  /// @notice Configure which remote-chain tokens each local terminal token maps to, enabling (or disabling) those
@@ -767,7 +779,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
767
779
  uint256 sourceTimestamp = _nextSourceTimestamp();
768
780
  JBAccountingSnapshot memory snapshot = JBSuckerLib.buildAccountingSnapshot({
769
781
  directory: DIRECTORY,
782
+ registry: REGISTRY,
770
783
  projectId: projectId(),
784
+ exceptChainId: peerChainId(),
771
785
  messageVersion: MESSAGE_VERSION,
772
786
  sourceTimestamp: sourceTimestamp
773
787
  });
@@ -868,28 +882,112 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
868
882
  return _outboxOf[token];
869
883
  }
870
884
 
871
- /// @notice The peer chain's raw per-context surplus and balance from the latest snapshot, bundled with the peer
872
- /// chain ID and snapshot freshness key.
873
- /// @dev The contexts are un-valued, in each context's own currency and decimals the registry dedups same-peer
874
- /// suckers by freshness, then values each context into a requested currency. The sucker consults no price oracle.
875
- /// @return contexts The per-currency surplus and balance from the latest snapshot.
876
- /// @return chainId The peer chain these contexts belong to.
877
- /// @return snapshot The source freshness key of the latest snapshot.
878
- function peerChainContextsOf()
885
+ /// @notice The raw, un-valued accounting record this sucker holds for every peer chain it has heard about.
886
+ /// @dev The registry reads this to gather a project's full cross-chain knowledge and re-gossip it. Records are
887
+ /// returned exactly as received (in each source chain's own token addresses and decimals) so the next receiver
888
+ /// resolves them to its own local currencies independently.
889
+ /// @return accounts One raw accounting record per known peer chain.
890
+ function peerChainAccountsOf() external view returns (JBChainAccounting[] memory accounts) {
891
+ uint256[] storage chainIds = _peerChainIds;
892
+ uint256 numChains = chainIds.length;
893
+ accounts = new JBChainAccounting[](numChains);
894
+ for (uint256 i; i < numChains;) {
895
+ uint256 chainId = chainIds[i];
896
+ accounts[i] = JBChainAccounting({
897
+ chainId: chainId,
898
+ totalSupply: peerChainTotalSupplyOf[chainId],
899
+ contexts: _peerContextsOf[chainId],
900
+ timestamp: snapshotTimestampOf[chainId]
901
+ });
902
+ unchecked {
903
+ ++i;
904
+ }
905
+ }
906
+ }
907
+
908
+ /// @notice One peer chain's per-currency surplus and balance from its latest accepted record, plus that record's
909
+ /// freshness key.
910
+ /// @dev Resolves each raw stored context to its local currency and folds same-currency, same-decimals entries
911
+ /// together. The result is un-valued — the registry values each context into a requested currency; the sucker
912
+ /// consults no price oracle. Contexts that share a currency but carry different decimals are kept separate because
913
+ /// the raw amounts are on different scales and cannot be summed directly.
914
+ /// @param chainId The peer chain to read the contexts of.
915
+ /// @return contexts The per-currency surplus and balance for the chain.
916
+ /// @return snapshot The source freshness key of the chain's latest accepted record.
917
+ function peerChainContextsOf(uint256 chainId)
879
918
  external
880
919
  view
881
- returns (JBPeerChainContext[] memory contexts, uint256 chainId, uint256 snapshot)
920
+ returns (JBPeerChainContext[] memory contexts, uint256 snapshot)
882
921
  {
883
- return (_peerContexts, peerChainId(), snapshotTimestamp);
922
+ JBSourceContext[] storage rawContexts = _peerContextsOf[chainId];
923
+ uint256 numRaw = rawContexts.length;
924
+
925
+ // Copy the raw contexts to memory and resolve each source-local token to a local token (mapping first, identity
926
+ // fallback). The bytecode-heavy currency resolution and fold then run in the library.
927
+ JBSourceContext[] memory raw = new JBSourceContext[](numRaw);
928
+ address[] memory localTokens = new address[](numRaw);
929
+ for (uint256 i; i < numRaw;) {
930
+ JBSourceContext storage ctx = rawContexts[i];
931
+ raw[i] = ctx;
932
+ address contextToken = _localTokenForRemoteToken[ctx.token];
933
+ localTokens[i] = contextToken == address(0) ? _toAddress(ctx.token) : contextToken;
934
+ unchecked {
935
+ ++i;
936
+ }
937
+ }
938
+
939
+ contexts = JBSuckerLib.foldPeerContexts({
940
+ directory: DIRECTORY, projectId: projectId(), localTokens: localTokens, rawContexts: raw
941
+ });
942
+ snapshot = snapshotTimestampOf[chainId];
943
+ }
944
+
945
+ /// @notice The peer chains this sucker reports accounting for.
946
+ /// @dev With `includeVirtual` false, returns only the directly-connected peer chain (the one this sucker is bridged
947
+ /// to). With `includeVirtual` true, also returns every other chain it has learned about through gossip relayed by
948
+ /// that peer. The directly-connected peer is always present (with a zero value until its first record) so the
949
+ /// registry can enumerate the chain the moment this sucker is deployed. Until the sucker receives its first record,
950
+ /// that entry is an empty sentinel (value 0, freshness 0) which the registry skips, so a freshly-deployed active
951
+ /// sucker takes over a chain's accounting only once it has synced real data — a deprecated sucker's record keeps
952
+ /// answering for the chain during the migration window.
953
+ /// @param includeVirtual Whether to also include virtually-known (gossiped) peer chains.
954
+ /// @return chainIds The peer chain IDs.
955
+ function peerChainIds(bool includeVirtual) external view returns (uint256[] memory chainIds) {
956
+ uint256 directPeer = peerChainId();
957
+ // The direct peer is a real remote chain — never the local chain or chain 0.
958
+ bool directValid = directPeer != 0 && directPeer != block.chainid;
959
+
960
+ // Directly-connected: just the bridged peer chain.
961
+ if (!includeVirtual) {
962
+ if (!directValid) return new uint256[](0);
963
+ chainIds = new uint256[](1);
964
+ chainIds[0] = directPeer;
965
+ return chainIds;
966
+ }
967
+
968
+ // Virtual-inclusive: every chain heard about, plus the direct peer if no record has placed it there yet.
969
+ bool appendDirect = directValid && !_isKnownPeerChainId[directPeer];
970
+ uint256 len = _peerChainIds.length;
971
+ chainIds = new uint256[](appendDirect ? len + 1 : len);
972
+ for (uint256 i; i < len;) {
973
+ chainIds[i] = _peerChainIds[i];
974
+ unchecked {
975
+ ++i;
976
+ }
977
+ }
978
+ if (appendDirect) chainIds[len] = directPeer;
884
979
  }
885
980
 
886
- /// @notice The peer chain total supply bundled with the peer chain ID and snapshot freshness key.
887
- /// @dev Lets aggregators (e.g. `JBSuckerRegistry`) read the value, peer chain, and freshness in one call instead
888
- /// of three separate staticcalls. The `value` is identical to `peerChainTotalSupply`.
981
+ /// @notice One peer chain's total supply bundled with the peer chain ID and the record's freshness key.
982
+ /// @dev Lets the registry read the value, the peer chain it belongs to, and its freshness in one call. The `value`
983
+ /// matches `peerChainTotalSupplyOf(chainId)`.
984
+ /// @param chainId The peer chain to read the total supply value of.
889
985
  /// @return A `JBPeerChainValue` with the total supply, peer chain ID, and snapshot freshness key.
890
- function peerChainTotalSupplyValue() external view returns (JBPeerChainValue memory) {
986
+ function peerChainTotalSupplyValue(uint256 chainId) external view returns (JBPeerChainValue memory) {
891
987
  return JBPeerChainValue({
892
- value: peerChainTotalSupply, peerChainId: peerChainId(), snapshotTimestamp: snapshotTimestamp
988
+ value: peerChainTotalSupplyOf[chainId],
989
+ peerChainId: chainId,
990
+ snapshotTimestamp: snapshotTimestampOf[chainId]
893
991
  });
894
992
  }
895
993
 
@@ -1308,13 +1406,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1308
1406
  });
1309
1407
  }
1310
1408
 
1311
- /// @notice What is the maximum time it takes for a message to be received on the other side.
1312
- /// @dev Be sure to keep in mind if a message fails having to retry and the time it takes to retry.
1313
- /// @return The maximum time it takes for a message to be received on the other side.
1314
- function _maxMessagingDelay() internal pure virtual returns (uint40) {
1315
- return 14 days;
1316
- }
1317
-
1318
1409
  /// @notice Cash out project tokens for terminal tokens.
1319
1410
  /// @param projectToken The project token to cash out (unused, kept for interface compatibility).
1320
1411
  /// @param count The number of project tokens to cash out.
@@ -1486,75 +1577,72 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1486
1577
  internal
1487
1578
  virtual;
1488
1579
 
1489
- /// @notice Store the latest peer-chain accounting snapshot if it is fresher than the cached one.
1490
- /// @dev The context set is rebuilt from scratch for each fresher snapshot. A stale snapshot is ignored instead of
1491
- /// reverting so bridge delivery order cannot roll back supply, surplus, or balance.
1492
- /// @param sourceTimestamp The source-chain freshness key.
1493
- /// @param sourceTotalSupply The source-chain total project-token supply.
1494
- /// @param sourceContexts The source-chain per-context surplus and balance.
1495
- function _storePeerChainAccounting(
1580
+ /// @notice Store every record in a cross-chain accounting bundle, keeping the freshest record per source chain.
1581
+ /// @dev Shared by both inbound paths `fromRemote` (root) and `fromRemoteAccounting` so a root send and an
1582
+ /// accounting-only send propagate the same gossip bundle.
1583
+ /// @param accounts The per-source-chain accounting records carried by an inbound message.
1584
+ function _storeAccountingBundle(JBChainAccounting[] calldata accounts) internal {
1585
+ uint256 numAccounts = accounts.length;
1586
+ for (uint256 i; i < numAccounts;) {
1587
+ JBChainAccounting calldata account = accounts[i];
1588
+ _storeChainAccounting({
1589
+ chainId: account.chainId,
1590
+ sourceTimestamp: account.timestamp,
1591
+ sourceTotalSupply: account.totalSupply,
1592
+ sourceContexts: account.contexts
1593
+ });
1594
+ unchecked {
1595
+ ++i;
1596
+ }
1597
+ }
1598
+ }
1599
+
1600
+ /// @notice Store one source chain's accounting record if it is fresher than the one already held for that chain.
1601
+ /// @dev A record for the local chain is ignored — a chain reads its own accounting directly. Each peer chain is
1602
+ /// gated independently on a strictly-newer freshness key, so a stale relay cannot roll back any chain and records
1603
+ /// delivered out of order converge. Contexts are stored raw (in the source chain's own token addresses) so the
1604
+ /// record can be re-gossiped faithfully; each receiver resolves them to its own local currencies at read time.
1605
+ /// @param chainId The source chain the record describes.
1606
+ /// @param sourceTimestamp The record's source-chain freshness key.
1607
+ /// @param sourceTotalSupply The source chain's total project-token supply.
1608
+ /// @param sourceContexts The source chain's raw per-context surplus and balance.
1609
+ function _storeChainAccounting(
1610
+ uint256 chainId,
1496
1611
  uint256 sourceTimestamp,
1497
1612
  uint256 sourceTotalSupply,
1498
1613
  JBSourceContext[] calldata sourceContexts
1499
1614
  )
1500
1615
  internal
1501
1616
  {
1502
- // Only accept snapshots whose source freshness key is strictly newer than the last accepted one.
1503
- if (sourceTimestamp <= snapshotTimestamp) return;
1617
+ // A chain reads its own accounting locally, so never store a record describing the local chain — even one a
1618
+ // peer forwarded back to us. Chain 0 is not a real chain ID, so reject it as malformed.
1619
+ if (chainId == block.chainid || chainId == 0) return;
1504
1620
 
1505
- // Advance the snapshot freshness key used by the registry to dedup same-peer suckers.
1506
- snapshotTimestamp = sourceTimestamp;
1621
+ // Accept a record only when its source freshness key is strictly newer than the last accepted one for this
1622
+ // chain. Ignoring (not reverting) means bridge delivery order cannot roll a chain back.
1623
+ if (sourceTimestamp <= snapshotTimestampOf[chainId]) return;
1624
+
1625
+ // Advance this chain's freshness key, which the registry uses to dedup redundant same-chain records.
1626
+ snapshotTimestampOf[chainId] = sourceTimestamp;
1507
1627
 
1508
1628
  // Update unconditionally — a legitimate zero supply must clear phantom cached supply.
1509
- peerChainTotalSupply = sourceTotalSupply;
1629
+ peerChainTotalSupplyOf[chainId] = sourceTotalSupply;
1630
+
1631
+ // Append the chain to the enumerable set on its first accepted record so the registry can enumerate it.
1632
+ if (!_isKnownPeerChainId[chainId]) {
1633
+ _isKnownPeerChainId[chainId] = true;
1634
+ _peerChainIds.push(chainId);
1635
+ }
1510
1636
 
1511
- // Rebuild the per-currency context set from scratch. A context that dropped out of this fresher snapshot is
1512
- // simply absent from the new set, so no per-entry clearing is needed.
1513
- delete _peerContexts;
1637
+ // Rebuild this chain's raw context set from scratch. A context dropped by the fresher record is simply absent.
1638
+ // Each context is stored verbatim so it can be re-gossiped faithfully; folding to a local currency happens at
1639
+ // read time in `peerChainContextsOf`.
1640
+ delete _peerContextsOf[chainId];
1641
+ JBSourceContext[] storage storedContexts = _peerContextsOf[chainId];
1514
1642
 
1515
- // Fold each source context into the local currency it resolves to. Resolution prefers the token mapping (so a
1516
- // same-asset token at a different remote address binds to the right local context) and falls back to identity
1517
- // for same-address tokens; the local currency is then derived from that resolved local token's authoritative
1518
- // accounting context, NOT trusted from the wire.
1519
1643
  uint256 numContexts = sourceContexts.length;
1520
1644
  for (uint256 i; i < numContexts;) {
1521
- JBSourceContext calldata ctx = sourceContexts[i];
1522
-
1523
- address contextToken = _localTokenForRemoteToken[ctx.token];
1524
- if (contextToken == address(0)) contextToken = _toAddress(ctx.token);
1525
- (uint32 contextCurrency, bool authoritative) = _localCurrencyOf(contextToken);
1526
-
1527
- // Cache an authoritative currency so later snapshots reuse it instead of re-reading the terminal. The
1528
- // accounting-context currency is immutable, so the cache never goes stale; a not-yet-configured token is
1529
- // left uncached and re-read next time.
1530
- if (authoritative && _cachedCurrencyOf[contextToken] == 0) {
1531
- _cachedCurrencyOf[contextToken] = contextCurrency;
1532
- }
1533
-
1534
- // Accumulate into an existing entry that matches on BOTH currency AND decimals, or append a new one. The
1535
- // decimals must match: `surplus`/`balance` are raw, un-valued token amounts, so two contexts that share a
1536
- // currency but carry different decimals are on different scales and cannot be summed directly.
1537
- uint256 numStored = _peerContexts.length;
1538
- bool merged;
1539
- for (uint256 j; j < numStored;) {
1540
- if (_peerContexts[j].currency == contextCurrency && _peerContexts[j].decimals == ctx.decimals) {
1541
- _peerContexts[j].surplus = _saturatingAddU128(_peerContexts[j].surplus, ctx.surplus);
1542
- _peerContexts[j].balance = _saturatingAddU128(_peerContexts[j].balance, ctx.balance);
1543
- merged = true;
1544
- break;
1545
- }
1546
- unchecked {
1547
- ++j;
1548
- }
1549
- }
1550
- if (!merged) {
1551
- _peerContexts.push(
1552
- JBPeerChainContext({
1553
- currency: contextCurrency, decimals: ctx.decimals, surplus: ctx.surplus, balance: ctx.balance
1554
- })
1555
- );
1556
- }
1557
-
1645
+ storedContexts.push(sourceContexts[i]);
1558
1646
  unchecked {
1559
1647
  ++i;
1560
1648
  }
@@ -1811,43 +1899,30 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1811
1899
  return ERC2771Context._contextSuffixLength();
1812
1900
  }
1813
1901
 
1814
- /// @notice The authoritative accounting-context currency the project uses for a local token. Peer context is keyed
1815
- /// by this currency so a consumer reads it under the same currency it already works in (which the project may set
1816
- /// to a well-known id like USD rather than the token-keyed convention).
1817
- /// @dev Both lookups use a low-level staticcall guarded by a returndata-length check, so a missing or
1818
- /// non-conforming directory/terminal (including one that returns short/empty data) can't block a bridge message —
1819
- /// it just yields the fallback. Returns the cached value when one exists (the accounting-context currency is
1820
- /// immutable, so the cache never goes stale). Falls back to the conventional `uint32(uint160(token))` only when the
1821
- /// project has no local accounting context for the token yet; that fallback is NOT cached, so a later snapshot
1822
- /// re-reads it once the context exists.
1823
- /// @param token The resolved local token.
1824
- /// @return currency The project's accounting-context currency for the token.
1825
- /// @return authoritative Whether `currency` came from a cached or configured accounting context (true) or the
1826
- /// convention fallback (false). `fromRemote` caches only authoritative results, since those are immutable.
1827
- function _localCurrencyOf(address token) internal view returns (uint32 currency, bool authoritative) {
1828
- // Reuse the value derived on an earlier snapshot — no need to read the terminal again.
1829
- uint32 cached = _cachedCurrencyOf[token];
1830
- if (cached != 0) return (cached, true);
1831
-
1832
- uint256 forProjectId = projectId();
1833
-
1834
- // Resolve the project's primary terminal for the token. An `address` return needs a full word.
1835
- (bool terminalOk, bytes memory terminalData) =
1836
- address(DIRECTORY).staticcall(abi.encodeCall(IJBDirectory.primaryTerminalOf, (forProjectId, token)));
1837
- if (terminalOk && terminalData.length >= 32) {
1838
- address terminal = abi.decode(terminalData, (address));
1839
- if (terminal != address(0)) {
1840
- // Read the token's accounting context. The struct encodes to three words.
1841
- (bool contextOk, bytes memory contextData) =
1842
- terminal.staticcall(abi.encodeCall(IJBTerminal.accountingContextForTokenOf, (forProjectId, token)));
1843
- if (contextOk && contextData.length >= 96) {
1844
- JBAccountingContext memory accountingContext = abi.decode(contextData, (JBAccountingContext));
1845
- if (accountingContext.currency != 0) return (accountingContext.currency, true);
1846
- }
1902
+ /// @notice What is the maximum time it takes for a message to be received on the other side.
1903
+ /// @dev Be sure to keep in mind if a message fails having to retry and the time it takes to retry.
1904
+ /// @return The maximum time it takes for a message to be received on the other side.
1905
+ function _maxMessagingDelay() internal pure virtual returns (uint40) {
1906
+ return 14 days;
1907
+ }
1908
+
1909
+ /// @notice The destination gas limit a gossip-carrying message needs to store its bundle on the remote chain.
1910
+ /// @dev A fixed base (`MESSENGER_BASE_GAS_LIMIT`) plus `_MESSENGER_SOURCE_CONTEXT_GAS_LIMIT` per source context, so
1911
+ /// the budget grows with the bundle. Without this, a fixed cap would bound the mesh: once the bundle's contexts
1912
+ /// exceed the cap, the destination `fromRemote`/`fromRemoteAccounting` runs out of gas. Every bridge variant sizes
1913
+ /// its outbound gas limit from this so the OP-stack, Arbitrum, and CCIP suckers scale identically.
1914
+ /// @param accounts The accounting records carried by the message.
1915
+ /// @return gasLimit The destination gas limit to request from the bridge.
1916
+ function _messagingGasLimit(JBChainAccounting[] memory accounts) internal pure returns (uint256 gasLimit) {
1917
+ uint256 contextCount;
1918
+ uint256 numAccounts = accounts.length;
1919
+ for (uint256 i; i < numAccounts;) {
1920
+ contextCount += accounts[i].contexts.length;
1921
+ unchecked {
1922
+ ++i;
1847
1923
  }
1848
1924
  }
1849
- // forge-lint: disable-next-line(unsafe-typecast)
1850
- return (uint32(uint160(token)), false);
1925
+ return MESSENGER_BASE_GAS_LIMIT + (contextCount * _MESSENGER_SOURCE_CONTEXT_GAS_LIMIT);
1851
1926
  }
1852
1927
 
1853
1928
  /// @notice The calldata. Preferred to use over `msg.data`.
@@ -1899,21 +1974,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1899
1974
  }
1900
1975
  }
1901
1976
 
1902
- /// @notice Adds two `uint128` amounts, saturating at `type(uint128).max` instead of overflowing.
1903
- /// @dev Saturation keeps a pathological peer snapshot from reverting the receive path; the cap can only
1904
- /// under-report a remote amount, the safe direction.
1905
- /// @param a The first amount.
1906
- /// @param b The second amount.
1907
- /// @return The saturated sum.
1908
- function _saturatingAddU128(uint128 a, uint128 b) internal pure returns (uint128) {
1909
- unchecked {
1910
- uint256 sum = uint256(a) + uint256(b);
1911
- // The cast only runs when `sum <= type(uint128).max`, so it cannot truncate.
1912
- // forge-lint: disable-next-line(unsafe-typecast)
1913
- return sum > type(uint128).max ? type(uint128).max : uint128(sum);
1914
- }
1915
- }
1916
-
1917
1977
  /// @notice Selects which retained inbox root a proof should be validated against, honoring a small window of
1918
1978
  /// recently-accepted roots rather than only the latest.
1919
1979
  /// @dev Computes the branch root implied by the proof once, then returns the first retained ring root it matches.
@@ -2020,7 +2080,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
2020
2080
 
2021
2081
  JBMessageRoot memory message = JBSuckerLib.buildSnapshotMessage({
2022
2082
  directory: DIRECTORY,
2083
+ registry: REGISTRY,
2023
2084
  projectId: projectId(),
2085
+ exceptChainId: peerChainId(),
2024
2086
  remoteToken: remoteToken.addr,
2025
2087
  amount: amount,
2026
2088
  nonce: nonce,