@bananapus/suckers-v6 0.0.78 → 1.0.0

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.
@@ -6,6 +6,7 @@ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
6
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
7
7
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
8
8
  import {JBAccountingSnapshot} from "../structs/JBAccountingSnapshot.sol";
9
+ import {JBChainAccounting} from "../structs/JBChainAccounting.sol";
9
10
  import {JBClaim} from "../structs/JBClaim.sol";
10
11
  import {JBInboxTreeRoot} from "../structs/JBInboxTreeRoot.sol";
11
12
  import {JBOutboxTree} from "../structs/JBOutboxTree.sol";
@@ -157,33 +158,51 @@ interface IJBSucker is IERC165 {
157
158
  /// @return The peer address.
158
159
  function peer() external view returns (bytes32);
159
160
 
160
- /// @notice The chain ID of the remote peer.
161
- /// @return chainId The remote chain ID.
162
- function peerChainId() external view returns (uint256 chainId);
163
-
164
- /// @notice The peer chain's raw per-context surplus and balance from the latest snapshot, bundled with the peer
165
- /// chain ID and snapshot freshness key.
166
- /// @dev Un-valued each context is in its own currency and decimals. The registry dedups same-peer suckers by
167
- /// freshness, then values each context into a requested currency. The sucker consults no price oracle.
168
- /// @return contexts The per-currency surplus and balance from the latest snapshot.
169
- /// @return chainId The peer chain these contexts belong to.
170
- /// @return snapshot The source freshness key of the latest snapshot.
171
- function peerChainContextsOf()
161
+ /// @notice The raw, un-valued accounting record this sucker holds for every peer chain it has heard about.
162
+ /// @dev The registry reads this to gather a project's full cross-chain knowledge and re-gossip it. Records are
163
+ /// returned exactly as received so the next receiver resolves them to its own local currencies independently.
164
+ /// @return accounts One raw accounting record per known peer chain.
165
+ function peerChainAccountsOf() external view returns (JBChainAccounting[] memory accounts);
166
+
167
+ /// @notice One peer chain's per-currency surplus and balance from its latest accepted record, plus its freshness
168
+ /// key.
169
+ /// @dev Un-valued each context is in its own currency and decimals. The registry values each context into a
170
+ /// requested currency; the sucker consults no price oracle.
171
+ /// @param chainId The peer chain to read the contexts of.
172
+ /// @return contexts The per-currency surplus and balance for the chain.
173
+ /// @return snapshot The source freshness key of the chain's latest accepted record.
174
+ function peerChainContextsOf(uint256 chainId)
172
175
  external
173
176
  view
174
- returns (JBPeerChainContext[] memory contexts, uint256 chainId, uint256 snapshot);
177
+ returns (JBPeerChainContext[] memory contexts, uint256 snapshot);
178
+
179
+ /// @notice The chain ID of the remote peer this sucker is directly paired with.
180
+ /// @return chainId The remote chain ID.
181
+ function peerChainId() external view returns (uint256 chainId);
175
182
 
176
- /// @notice The last known total token supply on the peer chain, updated each time a bridge message is received.
177
- /// @dev Used by data hooks to compute `effectiveTotalSupply = localSupply + sum(peerChainTotalSupply)` across all
178
- /// suckers, preventing cash out tax bypass on chains where a holder dominates the local supply.
183
+ /// @notice The peer chains this sucker reports accounting for. With `includeVirtual` false, returns only the
184
+ /// directly-connected peer chain (the one it is bridged to); with `includeVirtual` true, also returns every chain
185
+ /// learned about through gossip relayed by that peer.
186
+ /// @dev The directly-connected peer is always present (with a zero value until its first record) so a
187
+ /// freshly-deployed active sucker immediately owns that chain's accounting during a migration window.
188
+ /// @param includeVirtual Whether to also include virtually-known (gossiped) peer chains.
189
+ /// @return chainIds The peer chain IDs.
190
+ function peerChainIds(bool includeVirtual) external view returns (uint256[] memory chainIds);
191
+
192
+ /// @notice The last known total token supply on a peer chain, updated each time a bridge message carries that
193
+ /// chain's accounting record.
194
+ /// @dev The registry sums the freshest value across every peer chain to drive cross-chain cash out tax, preventing
195
+ /// a holder who dominates one chain's local supply from bypassing the tax.
196
+ /// @param chainId The peer chain to read the total supply of.
179
197
  /// @return The peer chain's total supply.
180
- function peerChainTotalSupply() external view returns (uint256);
198
+ function peerChainTotalSupplyOf(uint256 chainId) external view returns (uint256);
181
199
 
182
- /// @notice The peer chain total supply bundled with the peer chain ID and snapshot freshness key.
183
- /// @dev Lets aggregators read the value, the peer chain it belongs to, and its freshness in one call. The
184
- /// `value` matches `peerChainTotalSupply`.
200
+ /// @notice One peer chain's total supply bundled with the peer chain ID and the record's freshness key.
201
+ /// @dev Lets the registry read the value, the peer chain it belongs to, and its freshness in one call. The `value`
202
+ /// matches `peerChainTotalSupplyOf(chainId)`.
203
+ /// @param chainId The peer chain to read the total supply value of.
185
204
  /// @return A `JBPeerChainValue` with the total supply, peer chain ID, and snapshot freshness key.
186
- function peerChainTotalSupplyValue() external view returns (JBPeerChainValue memory);
205
+ function peerChainTotalSupplyValue(uint256 chainId) external view returns (JBPeerChainValue memory);
187
206
 
188
207
  /// @notice The ID of the project on the local chain that this sucker is associated with.
189
208
  /// @return The project ID.
@@ -194,10 +213,11 @@ interface IJBSucker is IERC165 {
194
213
  /// @return The remote token info.
195
214
  function remoteTokenFor(address token) external view returns (JBRemoteToken memory);
196
215
 
197
- /// @notice The freshness key of the latest accepted peer-chain economic snapshot.
216
+ /// @notice The freshness key of the latest accepted accounting record for a peer chain.
198
217
  /// @dev Higher values are fresher. The key is source-chain monotonic, not a value magnitude.
199
- /// @return The latest peer-chain snapshot freshness key.
200
- function snapshotTimestamp() external view returns (uint256);
218
+ /// @param chainId The peer chain to read the latest accepted freshness key of.
219
+ /// @return The latest accepted freshness key for the chain.
220
+ function snapshotTimestampOf(uint256 chainId) external view returns (uint256);
201
221
 
202
222
  /// @notice The current deprecation state of this sucker.
203
223
  /// @return The sucker state.
@@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
4
4
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
5
5
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
6
6
 
7
+ import {JBChainAccounting} from "../structs/JBChainAccounting.sol";
7
8
  import {JBSuckerDeployerConfig} from "../structs/JBSuckerDeployerConfig.sol";
8
9
  import {JBSuckersPair} from "../structs/JBSuckersPair.sol";
9
10
 
@@ -68,9 +69,23 @@ interface IJBSuckerRegistry {
68
69
  /// @return Whether the sucker belongs to the project.
69
70
  function isSuckerOf(uint256 projectId, address addr) external view returns (bool);
70
71
 
72
+ /// @notice The freshest accounting record per source chain that a project's suckers hold, for re-gossiping.
73
+ /// @dev A sucker building an outbound gossip bundle calls this to gather the project's full cross-chain knowledge,
74
+ /// deduped per chain (freshest wins; active supersedes deprecated), excluding the destination and local chains.
75
+ /// @param projectId The ID of the project.
76
+ /// @param exceptChainId The destination chain to exclude.
77
+ /// @return accounts The deduped raw accounting records, one per known source chain.
78
+ function peerChainAccountsOf(
79
+ uint256 projectId,
80
+ uint256 exceptChainId
81
+ )
82
+ external
83
+ view
84
+ returns (JBChainAccounting[] memory accounts);
85
+
71
86
  /// @notice The cumulative total supply across all remote peer chains for a project.
72
- /// @dev Dedupes same-peer active suckers by freshest snapshot, then sums peer-chain values. Silently skips suckers
73
- /// that revert.
87
+ /// @dev Aggregates over every (sucker, chain) pair and dedups per chain by freshest record. Silently skips suckers
88
+ /// and records that revert.
74
89
  /// @param projectId The ID of the project.
75
90
  /// @return totalSupply The combined peer chain total supply.
76
91
  function remoteTotalSupplyOf(uint256 projectId) external view returns (uint256 totalSupply);
@@ -95,9 +110,9 @@ interface IJBSuckerRegistry {
95
110
  function toRemoteFee() external view returns (uint256);
96
111
 
97
112
  /// @notice The cumulative peer-chain balance across all remote peer chains for a project, valued into a currency.
98
- /// @dev Dedups same-peer active suckers by freshest snapshot, then sums each sucker's balance valued into
99
- /// `currency`. A context whose currency already matches is taken at par (no feed); a missing cross-currency feed
100
- /// reverts and that sucker is silently skipped (conservative, bias-low).
113
+ /// @dev Aggregates over every (sucker, chain) pair and dedups per chain by freshest record, then sums each chain's
114
+ /// balance valued into `currency`. A context whose currency already matches is taken at par (no feed); a missing
115
+ /// cross-currency feed reverts and that (sucker, chain) is silently skipped (conservative, bias-low).
101
116
  /// @param projectId The ID of the project.
102
117
  /// @param currency The currency to value the combined balance into.
103
118
  /// @param decimals The decimal precision for the returned value.
@@ -112,9 +127,9 @@ interface IJBSuckerRegistry {
112
127
  returns (uint256 balance);
113
128
 
114
129
  /// @notice The cumulative peer-chain surplus across all remote peer chains for a project, valued into a currency.
115
- /// @dev Dedups same-peer active suckers by freshest snapshot, then sums each sucker's surplus valued into
116
- /// `currency`. A context whose currency already matches is taken at par (no feed); a missing cross-currency feed
117
- /// reverts and that sucker is silently skipped (conservative, bias-low).
130
+ /// @dev Aggregates over every (sucker, chain) pair and dedups per chain by freshest record, then sums each chain's
131
+ /// surplus valued into `currency`. A context whose currency already matches is taken at par (no feed); a missing
132
+ /// cross-currency feed reverts and that (sucker, chain) is silently skipped (conservative, bias-low).
118
133
  /// @param projectId The ID of the project.
119
134
  /// @param currency The currency to value the combined surplus into.
120
135
  /// @param decimals The decimal precision for the returned value.
@@ -12,10 +12,13 @@ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadat
12
12
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
13
13
 
14
14
  import {IJBPeerChainAdjustedAccounts} from "../interfaces/IJBPeerChainAdjustedAccounts.sol";
15
+ import {IJBSuckerRegistry} from "../interfaces/IJBSuckerRegistry.sol";
15
16
  import {JBPeerChainAdjustedAccountsLib} from "./JBPeerChainAdjustedAccountsLib.sol";
16
17
  import {JBAccountingSnapshot} from "../structs/JBAccountingSnapshot.sol";
18
+ import {JBChainAccounting} from "../structs/JBChainAccounting.sol";
17
19
  import {JBInboxTreeRoot} from "../structs/JBInboxTreeRoot.sol";
18
20
  import {JBMessageRoot} from "../structs/JBMessageRoot.sol";
21
+ import {JBPeerChainContext} from "../structs/JBPeerChainContext.sol";
19
22
  import {JBSourceContext} from "../structs/JBSourceContext.sol";
20
23
  import {MerkleLib} from "../utils/MerkleLib.sol";
21
24
 
@@ -34,20 +37,25 @@ library JBSuckerLib {
34
37
  uint256 internal constant _CURRENT_RULESET_OF_RETURN_BYTES = (9 + 19) * 32;
35
38
 
36
39
  //*********************************************************************//
37
- // ---------------------- external transactions ---------------------- //
40
+ // ------------------------- external views -------------------------- //
38
41
  //*********************************************************************//
39
42
 
40
- /// @notice Build the cross-chain accounting snapshot (total supply plus per-context surplus and balance).
43
+ /// @notice Build the cross-chain accounting gossip bundle (the local chain's record plus known peer records).
41
44
  /// @dev Extracted from `JBSucker.syncAccountingData` to reduce child contract bytecode. Called via DELEGATECALL.
42
- /// The snapshot carries each context's surplus and balance in its own currency, without price-feed valuation.
45
+ /// Each record carries its source chain's surplus and balance per context in that context's own currency, without
46
+ /// price-feed valuation.
43
47
  /// @param directory The JB directory to look up controllers and terminals.
48
+ /// @param registry The sucker registry that aggregates the project's per-chain records.
44
49
  /// @param projectId The project ID.
50
+ /// @param exceptChainId The destination chain, excluded from the gathered peer records.
45
51
  /// @param messageVersion The message format version.
46
- /// @param sourceTimestamp The monotonic source freshness key for this snapshot.
52
+ /// @param sourceTimestamp The monotonic source freshness key for the local chain's record.
47
53
  /// @return snapshot The constructed accounting snapshot.
48
54
  function buildAccountingSnapshot(
49
55
  IJBDirectory directory,
56
+ IJBSuckerRegistry registry,
50
57
  uint256 projectId,
58
+ uint256 exceptChainId,
51
59
  uint8 messageVersion,
52
60
  uint256 sourceTimestamp
53
61
  )
@@ -55,34 +63,39 @@ library JBSuckerLib {
55
63
  view
56
64
  returns (JBAccountingSnapshot memory snapshot)
57
65
  {
58
- // Snapshot the project's per-context surplus and balance, un-valued. No price oracle is consulted on send.
59
- (uint256 localTotalSupply, JBSourceContext[] memory sourceContexts) =
60
- _snapshotAccountsOf({directory: directory, projectId: projectId});
61
-
62
66
  // Construct the accounting-only message without any token-local merkle root.
63
67
  snapshot = JBAccountingSnapshot({
64
68
  version: messageVersion,
65
- sourceTotalSupply: localTotalSupply,
66
- sourceContexts: sourceContexts,
67
- sourceTimestamp: sourceTimestamp
69
+ accounts: _buildGossipBundle({
70
+ directory: directory,
71
+ registry: registry,
72
+ projectId: projectId,
73
+ exceptChainId: exceptChainId,
74
+ sourceTimestamp: sourceTimestamp
75
+ })
68
76
  });
69
77
  }
70
78
 
71
- /// @notice Build the cross-chain snapshot message (total supply plus per-context surplus and balance).
79
+ /// @notice Build the cross-chain root message, carrying the accounting gossip bundle alongside the outbox root.
72
80
  /// @dev Extracted from `JBSucker._buildSnapshotAndSend` to reduce child contract bytecode. Called via DELEGATECALL.
73
- /// The snapshot carries each context's surplus and balance in its own currency, without price-feed valuation.
81
+ /// Each accounting record carries its source chain's surplus and balance per context in that context's own
82
+ /// currency, without price-feed valuation.
74
83
  /// @param directory The JB directory to look up controllers and terminals.
84
+ /// @param registry The sucker registry that aggregates the project's per-chain records.
75
85
  /// @param projectId The project ID.
86
+ /// @param exceptChainId The destination chain, excluded from the gathered peer records.
76
87
  /// @param remoteToken The remote token bytes32 address.
77
88
  /// @param amount The amount of terminal tokens to bridge.
78
89
  /// @param nonce The outbox nonce for this send.
79
90
  /// @param root The merkle root of the outbox tree.
80
91
  /// @param messageVersion The message format version.
81
- /// @param sourceTimestamp The monotonic source freshness key for this snapshot.
92
+ /// @param sourceTimestamp The monotonic source freshness key for the local chain's record.
82
93
  /// @return message The constructed JBMessageRoot.
83
94
  function buildSnapshotMessage(
84
95
  IJBDirectory directory,
96
+ IJBSuckerRegistry registry,
85
97
  uint256 projectId,
98
+ uint256 exceptChainId,
86
99
  bytes32 remoteToken,
87
100
  uint256 amount,
88
101
  uint64 nonce,
@@ -94,26 +107,22 @@ library JBSuckerLib {
94
107
  view
95
108
  returns (JBMessageRoot memory message)
96
109
  {
97
- // Snapshot the project's per-context surplus and balance, un-valued. No price oracle is consulted on send.
98
- (uint256 localTotalSupply, JBSourceContext[] memory sourceContexts) =
99
- _snapshotAccountsOf({directory: directory, projectId: projectId});
100
-
101
- // Construct the cross-chain message with the per-context snapshot data.
110
+ // Construct the cross-chain message with the outbox root and the accounting gossip bundle.
102
111
  message = JBMessageRoot({
103
112
  version: messageVersion,
104
113
  token: remoteToken,
105
114
  amount: amount,
106
115
  remoteRoot: JBInboxTreeRoot({nonce: nonce, root: root}),
107
- sourceTotalSupply: localTotalSupply,
108
- sourceContexts: sourceContexts,
109
- sourceTimestamp: sourceTimestamp
116
+ accounts: _buildGossipBundle({
117
+ directory: directory,
118
+ registry: registry,
119
+ projectId: projectId,
120
+ exceptChainId: exceptChainId,
121
+ sourceTimestamp: sourceTimestamp
122
+ })
110
123
  });
111
124
  }
112
125
 
113
- //*********************************************************************//
114
- // ------------------------- external views -------------------------- //
115
- //*********************************************************************//
116
-
117
126
  /// @notice Compute a branch root from a leaf, branch, and index. Wraps MerkleLib.branchRoot so its
118
127
  /// ~170 lines of unrolled assembly live in the library's bytecode instead of each sucker's.
119
128
  /// @param item The leaf hash.
@@ -177,10 +186,130 @@ library JBSuckerLib {
177
186
  }
178
187
  }
179
188
 
189
+ /// @notice Folds a peer chain's raw source contexts into per-currency surplus and balance, resolving each to a
190
+ /// local currency.
191
+ /// @dev Extracted from `JBSucker.peerChainContextsOf` to reduce child contract bytecode. Called via DELEGATECALL.
192
+ /// Each `localTokens[i]` is the local token the sucker resolved `rawContexts[i].token` to (via its token mapping or
193
+ /// identity); this derives that token's authoritative accounting-context currency and merges entries that share
194
+ /// BOTH currency AND decimals. The accounting-context currency is immutable, so re-resolving on each read is safe.
195
+ /// Entries that share a currency but carry different decimals stay separate, since the raw amounts are on different
196
+ /// scales. No price oracle is consulted.
197
+ /// @param directory The JB directory to look up the project's terminals.
198
+ /// @param projectId The project whose accounting contexts to read.
199
+ /// @param localTokens The local token each raw context resolves to, parallel to `rawContexts`.
200
+ /// @param rawContexts The peer chain's raw per-context surplus and balance.
201
+ /// @return contexts The per-currency surplus and balance for the chain.
202
+ function foldPeerContexts(
203
+ IJBDirectory directory,
204
+ uint256 projectId,
205
+ address[] memory localTokens,
206
+ JBSourceContext[] memory rawContexts
207
+ )
208
+ external
209
+ view
210
+ returns (JBPeerChainContext[] memory contexts)
211
+ {
212
+ uint256 numRaw = rawContexts.length;
213
+
214
+ // The folded set is no larger than the raw set, so allocate to that upper bound and track the populated length.
215
+ JBPeerChainContext[] memory buf = new JBPeerChainContext[](numRaw);
216
+ uint256 count;
217
+
218
+ for (uint256 i; i < numRaw;) {
219
+ uint8 ctxDecimals = rawContexts[i].decimals;
220
+ uint128 ctxSurplus = rawContexts[i].surplus;
221
+ uint128 ctxBalance = rawContexts[i].balance;
222
+ uint32 ctxCurrency = _currencyOf({directory: directory, projectId: projectId, token: localTokens[i]});
223
+
224
+ // Fold into an existing entry that matches on BOTH currency AND decimals, or append a new one.
225
+ bool merged;
226
+ for (uint256 j; j < count;) {
227
+ if (buf[j].currency == ctxCurrency && buf[j].decimals == ctxDecimals) {
228
+ buf[j].surplus = _saturatingAddU128(buf[j].surplus, ctxSurplus);
229
+ buf[j].balance = _saturatingAddU128(buf[j].balance, ctxBalance);
230
+ merged = true;
231
+ break;
232
+ }
233
+ unchecked {
234
+ ++j;
235
+ }
236
+ }
237
+ if (!merged) {
238
+ buf[count++] = JBPeerChainContext({
239
+ currency: ctxCurrency, decimals: ctxDecimals, surplus: ctxSurplus, balance: ctxBalance
240
+ });
241
+ }
242
+
243
+ unchecked {
244
+ ++i;
245
+ }
246
+ }
247
+
248
+ // Trim the over-allocated buffer to the folded length.
249
+ contexts = new JBPeerChainContext[](count);
250
+ for (uint256 k; k < count;) {
251
+ contexts[k] = buf[k];
252
+ unchecked {
253
+ ++k;
254
+ }
255
+ }
256
+ }
257
+
180
258
  //*********************************************************************//
181
259
  // ------------------------- internal views -------------------------- //
182
260
  //*********************************************************************//
183
261
 
262
+ /// @notice Assemble a cross-chain accounting gossip bundle: the local chain's own record plus every peer-chain
263
+ /// record the project's suckers currently hold, excluding the destination chain.
264
+ /// @dev The local record is taken fresh from `_snapshotAccountsOf` (including any data-hook adjusted accounts). The
265
+ /// peer records are gathered from the registry, which is the only contract that sees a hub chain's per-peer
266
+ /// suckers together; it dedups them to the freshest per chain. A reverting or unset registry yields a local-only
267
+ /// bundle, so a standalone sucker still propagates its own record. Forwarded peer records keep their own origin
268
+ /// chain and freshness key so the receiver gates each chain independently.
269
+ /// @param directory The JB directory to look up controllers and terminals.
270
+ /// @param registry The sucker registry that aggregates the project's per-chain records.
271
+ /// @param projectId The project to snapshot.
272
+ /// @param exceptChainId The destination chain, excluded from the gathered peer records.
273
+ /// @param sourceTimestamp The local record's freshness key.
274
+ /// @return accounts The assembled gossip bundle, with the local chain's record first.
275
+ function _buildGossipBundle(
276
+ IJBDirectory directory,
277
+ IJBSuckerRegistry registry,
278
+ uint256 projectId,
279
+ uint256 exceptChainId,
280
+ uint256 sourceTimestamp
281
+ )
282
+ internal
283
+ view
284
+ returns (JBChainAccounting[] memory accounts)
285
+ {
286
+ // Snapshot the local chain's own supply and per-context surplus/balance, un-valued. No price oracle is read.
287
+ (uint256 localTotalSupply, JBSourceContext[] memory localContexts) =
288
+ _snapshotAccountsOf({directory: directory, projectId: projectId});
289
+
290
+ // Gather every other chain's record the project knows, deduped per chain and minus the destination. The
291
+ // accounting gossip is best-effort, so a reverting registry must never break the essential root/token bridge:
292
+ // catch the failure and propagate just this chain's own record.
293
+ JBChainAccounting[] memory peers;
294
+ try registry.peerChainAccountsOf({projectId: projectId, exceptChainId: exceptChainId}) returns (
295
+ JBChainAccounting[] memory gathered
296
+ ) {
297
+ peers = gathered;
298
+ } catch {}
299
+
300
+ // The local record leads; forwarded peer records follow verbatim, keeping their own origin chain and freshness.
301
+ accounts = new JBChainAccounting[](peers.length + 1);
302
+ accounts[0] = JBChainAccounting({
303
+ chainId: block.chainid, totalSupply: localTotalSupply, contexts: localContexts, timestamp: sourceTimestamp
304
+ });
305
+ for (uint256 i; i < peers.length;) {
306
+ accounts[i + 1] = peers[i];
307
+ unchecked {
308
+ ++i;
309
+ }
310
+ }
311
+ }
312
+
184
313
  /// @notice Builds the project's per-accounting-context surplus and balance, each in the context's own currency,
185
314
  /// with no price-feed valuation.
186
315
  /// @dev Loops every terminal and accounting context, reading the raw per-token surplus (requested in the token's
@@ -253,6 +382,42 @@ library JBSuckerLib {
253
382
  } catch {}
254
383
  }
255
384
 
385
+ /// @notice The project's authoritative accounting-context currency for a local token, or a convention fallback.
386
+ /// @dev Reads the token's accounting context from its primary terminal via length-guarded staticcalls, so a missing
387
+ /// or non-conforming directory/terminal just yields the fallback. Falls back to `uint32(uint160(token))` when the
388
+ /// project has no local accounting context for the token yet. The accounting-context currency is immutable.
389
+ /// @param directory The JB directory to look up the project's primary terminal for the token.
390
+ /// @param projectId The project whose accounting context to read.
391
+ /// @param token The local token to resolve the currency of.
392
+ /// @return currency The project's accounting-context currency for the token.
393
+ function _currencyOf(
394
+ IJBDirectory directory,
395
+ uint256 projectId,
396
+ address token
397
+ )
398
+ internal
399
+ view
400
+ returns (uint32 currency)
401
+ {
402
+ // Resolve the project's primary terminal for the token. An `address` return needs a full word.
403
+ (bool terminalOk, bytes memory terminalData) =
404
+ address(directory).staticcall(abi.encodeCall(IJBDirectory.primaryTerminalOf, (projectId, token)));
405
+ if (terminalOk && terminalData.length >= 32) {
406
+ address terminal = abi.decode(terminalData, (address));
407
+ if (terminal != address(0)) {
408
+ // Read the token's accounting context. The struct encodes to three words.
409
+ (bool contextOk, bytes memory contextData) =
410
+ terminal.staticcall(abi.encodeCall(IJBTerminal.accountingContextForTokenOf, (projectId, token)));
411
+ if (contextOk && contextData.length >= 96) {
412
+ JBAccountingContext memory accountingContext = abi.decode(contextData, (JBAccountingContext));
413
+ if (accountingContext.currency != 0) return accountingContext.currency;
414
+ }
415
+ }
416
+ }
417
+ // forge-lint: disable-next-line(unsafe-typecast)
418
+ return uint32(uint160(token));
419
+ }
420
+
256
421
  /// @notice Optional project-specific adjusted accounts to add to peer-chain snapshots.
257
422
  /// @dev Reads the current ruleset's data hook and asks it for extra supply plus per-context surplus/balance, each
258
423
  /// in the context's own currency. Non-supporting or broken hooks are ignored so a project's baseline snapshot stays
@@ -339,6 +504,21 @@ library JBSuckerLib {
339
504
  });
340
505
  }
341
506
 
507
+ /// @notice Adds two `uint128` amounts, saturating at `type(uint128).max` instead of overflowing.
508
+ /// @dev Saturation keeps a pathological peer record from reverting the read path; the cap can only under-report a
509
+ /// remote amount, the safe direction.
510
+ /// @param a The first amount.
511
+ /// @param b The second amount.
512
+ /// @return The saturated sum.
513
+ function _saturatingAddU128(uint128 a, uint128 b) internal pure returns (uint128) {
514
+ unchecked {
515
+ uint256 sum = uint256(a) + uint256(b);
516
+ // The cast only runs when `sum <= type(uint128).max`, so it cannot truncate.
517
+ // forge-lint: disable-next-line(unsafe-typecast)
518
+ return sum > type(uint128).max ? type(uint128).max : uint128(sum);
519
+ }
520
+ }
521
+
342
522
  /// @notice Builds the local accounting values used in outbound peer-chain snapshots.
343
523
  /// @dev Project token supply stays a single currency-agnostic scalar. Surplus and balance are emitted per context
344
524
  /// in that context's currency, with no price-feed valuation. The receiving chain folds each context into its
@@ -372,6 +552,15 @@ library JBSuckerLib {
372
552
  if (address(controller) != address(0) && address(controller).code.length != 0) {
373
553
  (additionalSupply, hookContexts) =
374
554
  _peerChainAdjustedAccountsOf({controller: controller, projectId: projectId});
555
+ // Fail soft to the baseline snapshot. A malformed hook that returns a `supply` which would overflow the
556
+ // controller supply contributes no extra supply or contexts — the documented fail-soft model — instead
557
+ // of
558
+ // reverting the whole snapshot and bricking every outbound send (`toRemote` / `syncAccountingData`) while
559
+ // the hook stays active.
560
+ if (additionalSupply > type(uint256).max - localTotalSupply) {
561
+ additionalSupply = 0;
562
+ hookContexts = new JBSourceContext[](0);
563
+ }
375
564
  localTotalSupply += additionalSupply;
376
565
  }
377
566
 
@@ -1,19 +1,17 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {JBSourceContext} from "./JBSourceContext.sol";
4
+ import {JBChainAccounting} from "./JBChainAccounting.sol";
5
5
 
6
- /// @notice A peer-chain accounting snapshot, without any token-local merkle root or transported value.
6
+ /// @notice A cross-chain accounting gossip bundle, sent without any token-local merkle root or transported value.
7
+ /// @dev Carries the sending chain's own accounting record plus every peer-chain record the sender currently holds,
8
+ /// each stamped with its originating chain's freshness key. The receiving chain stores the freshest record per source
9
+ /// chain, so accounting propagates across a hub-and-spoke sucker mesh without a direct sucker between every pair of
10
+ /// chains.
7
11
  /// @custom:member version The message format version. Used to reject incompatible messages.
8
- /// @custom:member sourceTotalSupply The total token supply (including reserved tokens) on the source chain at the
9
- /// time the message was sent. Used by the receiving chain to track cross-chain supply for cash out tax calculations.
10
- /// @custom:member sourceContexts The source chain's surplus and balance per accounting context, each in the context's
11
- /// own currency and decimals, un-valued.
12
- /// @custom:member sourceTimestamp A monotonic source-chain freshness key for the snapshot. Used by the receiving
13
- /// chain to reject stale surplus/balance/supply updates.
12
+ /// @custom:member accounts One accounting record per source chain known to the sender: its own chain plus every peer
13
+ /// chain it has heard about, excluding the destination chain.
14
14
  struct JBAccountingSnapshot {
15
15
  uint8 version;
16
- uint256 sourceTotalSupply;
17
- JBSourceContext[] sourceContexts;
18
- uint256 sourceTimestamp;
16
+ JBChainAccounting[] accounts;
19
17
  }
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {JBSourceContext} from "./JBSourceContext.sol";
5
+
6
+ /// @notice One source chain's project-wide accounting, carried as a record in a cross-chain gossip bundle.
7
+ /// @dev A sucker sends its own chain's record alongside every peer-chain record it already holds, each stamped with
8
+ /// the originating chain's own freshness key. The receiving chain stores the freshest record per source chain, so a
9
+ /// project's accounting propagates across a hub-and-spoke sucker mesh (L2s bridged only through mainnet) without a
10
+ /// direct sucker between every pair of chains. `contexts` carry the source chain's own token addresses, so each
11
+ /// receiver resolves them to its own local currencies independently. Trust is transitive across the mesh: a receiver
12
+ /// authenticates only the directly-bridged peer that delivers a bundle, not the origin of each forwarded record, so
13
+ /// any authenticated peer can forward a record for any other chain. A record is therefore only as trustworthy as the
14
+ /// project's same-address sucker invariant — the same CREATE2 same-bytecode assumption every paired sucker already
15
+ /// relies on — and a peer running adversarial bytecode could forge another chain's record. The freshest-per-chain
16
+ /// gate bounds rollback, not authorship; the supply view it feeds is clamped downstream by each chain's own local
17
+ /// surplus, so a forged record cannot by itself over-credit a cash out.
18
+ /// @custom:member chainId The source chain this record describes. A receiver ignores a record for its own chain, since
19
+ /// it reads its own local accounting directly.
20
+ /// @custom:member totalSupply The total token supply (including reserved tokens) on the source chain when the record
21
+ /// was taken. Used by the receiving chain to track cross-chain supply for cash out tax calculations.
22
+ /// @custom:member contexts The source chain's surplus and balance per accounting context, each in the context's own
23
+ /// currency and decimals, un-valued. The receiver resolves each entry to its same-asset local context and folds it in
24
+ /// at par.
25
+ /// @custom:member timestamp A monotonic source-chain freshness key for the record. The receiver keeps the freshest
26
+ /// record per source chain, so stale relays cannot roll back surplus, balance, or supply.
27
+ struct JBChainAccounting {
28
+ uint256 chainId;
29
+ uint256 totalSupply;
30
+ JBSourceContext[] contexts;
31
+ uint256 timestamp;
32
+ }
@@ -1,31 +1,27 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
+ import {JBChainAccounting} from "./JBChainAccounting.sol";
4
5
  import {JBInboxTreeRoot} from "./JBInboxTreeRoot.sol";
5
- import {JBSourceContext} from "./JBSourceContext.sol";
6
6
 
7
- /// @notice Information about the remote (inbox) tree's root, passed in a message from the remote chain.
8
- /// @dev The snapshot carries surplus and balance per accounting context in each context's own currency — the source
9
- /// chain performs no price-feed valuation. The receiving chain folds each context into its same-asset local context at
10
- /// par, so no price oracle is consulted in the cross-chain surplus path. Project token supply stays a single
11
- /// currency-agnostic scalar.
7
+ /// @notice Information about the remote (inbox) tree's root passed in a message from the remote chain, carried
8
+ /// alongside a cross-chain accounting gossip bundle.
9
+ /// @dev The accounting bundle carries the sending chain's own record plus every peer-chain record the sender holds,
10
+ /// each in the originating chain's own currency and decimals, un-valued. The receiving chain folds each context into
11
+ /// its same-asset local context at par and stores the freshest record per source chain, so no price oracle is
12
+ /// consulted in the cross-chain surplus path and accounting propagates across the sucker mesh as roots are relayed.
13
+ /// Project token supply stays a single currency-agnostic scalar per source chain.
12
14
  /// @custom:member version The message format version. Used to reject incompatible messages.
13
15
  /// @custom:member token The remote token address (bytes32 for cross-VM compatibility with SVM).
14
16
  /// @custom:member amount The amount of tokens to send.
15
17
  /// @custom:member remoteRoot The root of the merkle tree.
16
- /// @custom:member sourceTotalSupply The total token supply (including reserved tokens) on the source chain at the
17
- /// time the message was sent. Used by the receiving chain to track cross-chain supply for cash out tax calculations.
18
- /// @custom:member sourceContexts The source chain's surplus and balance per accounting context, each in the context's
19
- /// own currency and decimals, un-valued. The receiving chain resolves each entry to its same-asset local context and
20
- /// folds it in at par.
21
- /// @custom:member sourceTimestamp A monotonic source-chain freshness key for the snapshot. Used by the receiving
22
- /// chain to reject stale surplus/balance/supply updates without blocking token-local inbox root updates.
18
+ /// @custom:member accounts One accounting record per source chain known to the sender: its own chain plus every peer
19
+ /// chain it has heard about, excluding the destination chain. Used by the receiving chain to track cross-chain
20
+ /// supply, surplus, and balance for cash out tax calculations.
23
21
  struct JBMessageRoot {
24
22
  uint8 version;
25
23
  bytes32 token;
26
24
  uint256 amount;
27
25
  JBInboxTreeRoot remoteRoot;
28
- uint256 sourceTotalSupply;
29
- JBSourceContext[] sourceContexts;
30
- uint256 sourceTimestamp;
26
+ JBChainAccounting[] accounts;
31
27
  }
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {JBChainAccounting} from "./JBChainAccounting.sol";
5
+
6
+ /// @notice Scratch space used while gathering the freshest accounting record per peer chain across a project's suckers.
7
+ /// @dev Sized to the total records across the project's suckers for in-memory de-duplication; `chainCount` tracks
8
+ /// populated entries. Bundled into one struct so the gather helpers stay under the stack-slot limit.
9
+ /// @custom:member chainIds The peer chain IDs that have been observed.
10
+ /// @custom:member records The selected record for each observed peer chain.
11
+ /// @custom:member hasActiveRecord Whether the selected record came from an active sucker instead of a deprecated one.
12
+ /// @custom:member chainCount The number of populated peer-chain entries.
13
+ struct PeerAccountScratch {
14
+ uint256[] chainIds;
15
+ JBChainAccounting[] records;
16
+ bool[] hasActiveRecord;
17
+ uint256 chainCount;
18
+ }