@bananapus/suckers-v6 0.0.68 → 0.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.68",
3
+ "version": "0.0.69",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -84,7 +84,7 @@ contract DeployScript is Script, Sphinx {
84
84
  bool registryAlreadyDeployed = _isDeployed({
85
85
  salt: _REGISTRY_SALT,
86
86
  creationCode: type(JBSuckerRegistry).creationCode,
87
- arguments: abi.encode(core.directory, core.permissions, safeAddress(), trustedForwarder)
87
+ arguments: abi.encode(core.directory, core.permissions, core.prices, safeAddress(), trustedForwarder)
88
88
  });
89
89
 
90
90
  if (!registryAlreadyDeployed) {
@@ -93,6 +93,7 @@ contract DeployScript is Script, Sphinx {
93
93
  new JBSuckerRegistry{salt: _REGISTRY_SALT}({
94
94
  directory: core.directory,
95
95
  permissions: core.permissions,
96
+ prices: core.prices,
96
97
  initialOwner: safeAddress(),
97
98
  trustedForwarder: trustedForwarder
98
99
  })
@@ -106,7 +107,7 @@ contract DeployScript is Script, Sphinx {
106
107
  initCodeHash: keccak256(
107
108
  abi.encodePacked(
108
109
  type(JBSuckerRegistry).creationCode,
109
- abi.encode(core.directory, core.permissions, safeAddress(), trustedForwarder)
110
+ abi.encode(core.directory, core.permissions, core.prices, safeAddress(), trustedForwarder)
110
111
  )
111
112
  ),
112
113
  deployer: address(0x4e59b44847b379578588920cA78FbF26c0B4956C)
@@ -210,7 +211,6 @@ contract DeployScript is Script, Sphinx {
210
211
  deployer: _opDeployer,
211
212
  directory: core.directory,
212
213
  permissions: core.permissions,
213
- prices: core.prices,
214
214
  tokens: core.tokens,
215
215
  feeProjectId: 1,
216
216
  registry: registry,
@@ -262,7 +262,6 @@ contract DeployScript is Script, Sphinx {
262
262
  deployer: _opDeployer,
263
263
  directory: core.directory,
264
264
  permissions: core.permissions,
265
- prices: core.prices,
266
265
  tokens: core.tokens,
267
266
  feeProjectId: 1,
268
267
  registry: registry,
@@ -345,7 +344,6 @@ contract DeployScript is Script, Sphinx {
345
344
  deployer: _baseDeployer,
346
345
  directory: core.directory,
347
346
  permissions: core.permissions,
348
- prices: core.prices,
349
347
  tokens: core.tokens,
350
348
  feeProjectId: 1,
351
349
  registry: registry,
@@ -397,7 +395,6 @@ contract DeployScript is Script, Sphinx {
397
395
  deployer: _baseDeployer,
398
396
  directory: core.directory,
399
397
  permissions: core.permissions,
400
- prices: core.prices,
401
398
  tokens: core.tokens,
402
399
  feeProjectId: 1,
403
400
  registry: registry,
@@ -476,7 +473,6 @@ contract DeployScript is Script, Sphinx {
476
473
  deployer: _arbDeployer,
477
474
  directory: core.directory,
478
475
  permissions: core.permissions,
479
- prices: core.prices,
480
476
  tokens: core.tokens,
481
477
  feeProjectId: 1,
482
478
  registry: registry,
@@ -531,7 +527,6 @@ contract DeployScript is Script, Sphinx {
531
527
  deployer: _arbDeployer,
532
528
  directory: core.directory,
533
529
  permissions: core.permissions,
534
- prices: core.prices,
535
530
  tokens: core.tokens,
536
531
  feeProjectId: 1,
537
532
  registry: registry,
@@ -708,7 +703,6 @@ contract DeployScript is Script, Sphinx {
708
703
  salt: salt,
709
704
  directory: core.directory,
710
705
  permissions: core.permissions,
711
- prices: core.prices,
712
706
  tokens: core.tokens,
713
707
  configurator: safeAddress(),
714
708
  forwarder: trustedForwarder,
@@ -725,7 +719,6 @@ contract DeployScript is Script, Sphinx {
725
719
  bytes32 salt,
726
720
  IJBDirectory directory,
727
721
  IJBPermissions permissions,
728
- IJBPrices prices,
729
722
  IJBTokens tokens,
730
723
  address configurator,
731
724
  address forwarder,
@@ -781,7 +774,6 @@ contract DeployScript is Script, Sphinx {
781
774
  directory: directory,
782
775
  tokens: tokens,
783
776
  permissions: permissions,
784
- prices: prices,
785
777
  feeProjectId: 1,
786
778
  registry: registry,
787
779
  trustedForwarder: forwarder
@@ -8,7 +8,6 @@ import {AddressAliasHelper} from "@arbitrum/nitro-contracts/src/libraries/Addres
8
8
  import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
9
9
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
10
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
11
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
12
11
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
13
12
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
14
13
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
@@ -56,19 +55,17 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
56
55
 
57
56
  /// @param directory A contract storing directories of terminals and controllers for each project.
58
57
  /// @param permissions A contract storing permissions.
59
- /// @param prices The price oracle used to convert peer-chain balances and surplus.
60
58
  /// @param tokens A contract that manages token minting and burning.
61
59
  constructor(
62
60
  JBArbitrumSuckerDeployer deployer,
63
61
  IJBDirectory directory,
64
62
  IJBPermissions permissions,
65
- IJBPrices prices,
66
63
  IJBTokens tokens,
67
64
  uint256 feeProjectId,
68
65
  IJBSuckerRegistry registry,
69
66
  address trustedForwarder
70
67
  )
71
- JBSucker(directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
68
+ JBSucker(directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
72
69
  {
73
70
  GATEWAYROUTER = JBArbitrumSuckerDeployer(deployer).arbGatewayRouter();
74
71
  ARBINBOX = JBArbitrumSuckerDeployer(deployer).arbInbox();
@@ -3,7 +3,6 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
5
5
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
6
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
7
6
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
8
7
 
9
8
  import {JBOptimismSucker} from "./JBOptimismSucker.sol";
@@ -19,19 +18,17 @@ contract JBBaseSucker is JBOptimismSucker {
19
18
  /// @param deployer A contract that deploys clones of this contract.
20
19
  /// @param directory A contract storing directories of terminals and controllers for each project.
21
20
  /// @param permissions A contract storing permissions.
22
- /// @param prices The price oracle used to convert peer-chain balances and surplus.
23
21
  /// @param tokens A contract that manages token minting and burning.
24
22
  constructor(
25
23
  JBOptimismSuckerDeployer deployer,
26
24
  IJBDirectory directory,
27
25
  IJBPermissions permissions,
28
- IJBPrices prices,
29
26
  IJBTokens tokens,
30
27
  uint256 feeProjectId,
31
28
  IJBSuckerRegistry registry,
32
29
  address trustedForwarder
33
30
  )
34
- JBOptimismSucker(deployer, directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
31
+ JBOptimismSucker(deployer, directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
35
32
  {}
36
33
 
37
34
  //*********************************************************************//
@@ -4,7 +4,6 @@ pragma solidity 0.8.28;
4
4
  // External packages (alphabetized)
5
5
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
6
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
7
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
8
7
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
9
8
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
10
9
  import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/contracts/interfaces/IAny2EVMMessageReceiver.sol";
@@ -83,7 +82,6 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
83
82
  /// @param deployer A contract that deploys the clones for this contract.
84
83
  /// @param directory A contract storing directories of terminals and controllers for each project.
85
84
  /// @param permissions A contract storing permissions.
86
- /// @param prices The price oracle used to convert peer-chain balances and surplus.
87
85
  /// @param tokens A contract that manages token minting and burning.
88
86
  /// @param feeProjectId The ID of the project that receives fees.
89
87
  /// @param registry The sucker registry that tracks deployed suckers.
@@ -92,13 +90,12 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
92
90
  JBCCIPSuckerDeployer deployer,
93
91
  IJBDirectory directory,
94
92
  IJBPermissions permissions,
95
- IJBPrices prices,
96
93
  IJBTokens tokens,
97
94
  uint256 feeProjectId,
98
95
  IJBSuckerRegistry registry,
99
96
  address trustedForwarder
100
97
  )
101
- JBSucker(directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
98
+ JBSucker(directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
102
99
  {
103
100
  // Read the remote chain ID from the deployer.
104
101
  REMOTE_CHAIN_ID = IJBCCIPSuckerDeployer(deployer).ccipRemoteChainId();
@@ -3,7 +3,6 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
5
5
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
6
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
7
6
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
8
7
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
9
8
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
@@ -39,19 +38,17 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
39
38
  /// @param deployer A contract that deploys clones of this contract.
40
39
  /// @param directory A contract storing directories of terminals and controllers for each project.
41
40
  /// @param permissions A contract storing permissions.
42
- /// @param prices The price oracle used to convert peer-chain balances and surplus.
43
41
  /// @param tokens A contract that manages token minting and burning.
44
42
  constructor(
45
43
  JBOptimismSuckerDeployer deployer,
46
44
  IJBDirectory directory,
47
45
  IJBPermissions permissions,
48
- IJBPrices prices,
49
46
  IJBTokens tokens,
50
47
  uint256 feeProjectId,
51
48
  IJBSuckerRegistry registry,
52
49
  address trustedForwarder
53
50
  )
54
- JBSucker(directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
51
+ JBSucker(directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
55
52
  {
56
53
  // Fetch the messenger and bridge by doing a callback to the deployer contract.
57
54
  OPBRIDGE = JBOptimismSuckerDeployer(deployer).opBridge();
package/src/JBSucker.sol CHANGED
@@ -7,11 +7,12 @@ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol
7
7
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
8
  import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
9
9
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
10
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
11
10
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
12
11
  import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
13
12
  import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
13
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
14
14
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
15
+ import {JBFixedPointNumber} from "@bananapus/core-v6/src/libraries/JBFixedPointNumber.sol";
15
16
  import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
16
17
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
17
18
  import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
@@ -36,9 +37,10 @@ import {MerkleLib} from "./utils/MerkleLib.sol";
36
37
 
37
38
  // Local: structs (alphabetized)
38
39
  import {JBClaim} from "./structs/JBClaim.sol";
39
- import {JBDenominatedAmount} from "./structs/JBDenominatedAmount.sol";
40
40
  import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
41
41
  import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
42
+ import {JBPeerChainContext} from "./structs/JBPeerChainContext.sol";
43
+ import {JBSourceContext} from "./structs/JBSourceContext.sol";
42
44
  import {JBOutboxTree} from "./structs/JBOutboxTree.sol";
43
45
  import {JBPeerChainValue} from "./structs/JBPeerChainValue.sol";
44
46
  import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
@@ -132,9 +134,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
132
134
  /// @notice The project ID that receives the `toRemoteFee` payment. Typically the protocol project (ID 1).
133
135
  uint256 public immutable FEE_PROJECT_ID;
134
136
 
135
- /// @notice The price oracle used to convert peer-chain balances and surplus.
136
- IJBPrices public immutable PRICES;
137
-
138
137
  /// @notice The project registry (ERC-721 ownership).
139
138
  IJBProjects public immutable override PROJECTS;
140
139
 
@@ -177,6 +176,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
177
176
  /// @notice The retained failed transport-payment refund ETH owed to each original bridge caller.
178
177
  mapping(address account => uint256 amount) public retainedTransportPaymentRefundOf;
179
178
 
179
+ /// @notice The source chain freshness key for the most recent accepted peer snapshot.
180
+ /// @dev Only snapshots with a strictly newer source freshness key are accepted, preventing stale rollbacks.
181
+ /// The historical name is retained for ABI compatibility with the `JBMessageRoot.sourceTimestamp` field.
182
+ /// Returns 0 if no snapshot has been received yet.
183
+ uint256 public snapshotTimestamp;
184
+
180
185
  //*********************************************************************//
181
186
  // -------------------- internal stored properties ------------------- //
182
187
  //*********************************************************************//
@@ -230,29 +235,28 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
230
235
  // -------------------- private stored properties -------------------- //
231
236
  //*********************************************************************//
232
237
 
238
+ /// @notice Caches a local token's authoritative accounting-context currency, derived once and reused on later
239
+ /// snapshots. A project's accounting-context currency is immutable once set, so the cached value never goes stale;
240
+ /// only an authoritative read is cached (a not-yet-configured token uses the convention without caching, so a later
241
+ /// snapshot re-reads it once its context exists).
242
+ /// @custom:param token The local token.
243
+ mapping(address token => uint32 currency) private _cachedCurrencyOf;
244
+
233
245
  /// @notice The ID of the project (on the local chain) that this sucker is associated with.
234
246
  uint256 private _localProjectId;
235
247
 
236
- /// @notice The last known project-wide surplus on the peer chain. Updated each time a bridge message is received.
237
- /// @dev The `currency` and `decimals` fields describe the denomination; `value` is the surplus amount.
238
- JBDenominatedAmount private _peerChainSurplus;
248
+ /// @notice Tie-breaker mixed into outbound snapshot freshness keys when multiple roots are sent at one timestamp.
249
+ uint256 private _outboundSnapshotSequence;
239
250
 
240
251
  /// @notice Optional explicit peer sucker address on the remote chain.
241
252
  /// @dev A zero value preserves the default same-address deterministic peer.
242
253
  bytes32 private _peer;
243
254
 
244
- /// @notice The last known total recorded balance on the peer chain. Updated each time a bridge message is received.
245
- /// @dev The `currency` and `decimals` fields describe the denomination; `value` is the balance amount.
246
- JBDenominatedAmount private _peerChainBalance;
247
-
248
- /// @notice The source chain freshness key for the most recent accepted peer snapshot.
249
- /// @dev Only snapshots with a strictly newer source freshness key are accepted, preventing stale rollbacks.
250
- /// The historical name is retained for ABI compatibility with the `JBMessageRoot.sourceTimestamp` field.
251
- /// Returns 0 if no snapshot has been received yet.
252
- uint256 public snapshotTimestamp;
253
-
254
- /// @notice Tie-breaker mixed into outbound snapshot freshness keys when multiple roots are sent at one timestamp.
255
- uint256 private _outboundSnapshotSequence;
255
+ /// @notice The peer chain's per-currency surplus and balance from the latest snapshot. Rebuilt in full each time a
256
+ /// fresher snapshot is received, so a context that dropped out of the new snapshot is simply absent — no
257
+ /// per-entry
258
+ /// versioning or clearing is needed. A read sums these and values them into the requested currency.
259
+ JBPeerChainContext[] private _peerContexts;
256
260
 
257
261
  //*********************************************************************//
258
262
  // ---------------------------- constructor -------------------------- //
@@ -260,7 +264,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
260
264
 
261
265
  /// @param directory A contract storing directories of terminals and controllers for each project.
262
266
  /// @param permissions A contract storing permissions.
263
- /// @param prices The price oracle used to convert peer-chain balances and surplus.
264
267
  /// @param tokens A contract that manages token minting and burning.
265
268
  /// @param feeProjectId The project ID that receives the `toRemoteFee` payment (typically 1).
266
269
  /// @param registry The sucker registry that manages the global `toRemoteFee`.
@@ -268,7 +271,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
268
271
  constructor(
269
272
  IJBDirectory directory,
270
273
  IJBPermissions permissions,
271
- IJBPrices prices,
272
274
  IJBTokens tokens,
273
275
  uint256 feeProjectId,
274
276
  IJBSuckerRegistry registry,
@@ -279,7 +281,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
279
281
  {
280
282
  DIRECTORY = directory;
281
283
  FEE_PROJECT_ID = feeProjectId;
282
- PRICES = prices;
283
284
  PROJECTS = directory.PROJECTS();
284
285
  REGISTRY = registry;
285
286
  TOKENS = tokens;
@@ -514,18 +515,66 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
514
515
  // This prevents a staler per-token message from rolling back shared state (surplus, balance, supply)
515
516
  // that was already updated by a fresher message for a different token.
516
517
  if (root.sourceTimestamp > snapshotTimestamp) {
518
+ // Advance the snapshot freshness key (used by the registry to dedup same-peer suckers).
517
519
  snapshotTimestamp = root.sourceTimestamp;
518
520
 
519
521
  // Update unconditionally — a legitimate zero supply must clear phantom cached supply.
520
522
  peerChainTotalSupply = root.sourceTotalSupply;
521
523
 
522
- // Store the surplus and balance snapshots from the source chain.
523
- _peerChainSurplus = JBDenominatedAmount({
524
- value: root.sourceSurplus, currency: uint32(root.sourceCurrency), decimals: root.sourceDecimals
525
- });
526
- _peerChainBalance = JBDenominatedAmount({
527
- value: root.sourceBalance, currency: uint32(root.sourceCurrency), decimals: root.sourceDecimals
528
- });
524
+ // Rebuild the per-currency context set from scratch. A context that dropped out of this fresher snapshot is
525
+ // simply absent from the new set, so no per-entry clearing is needed.
526
+ delete _peerContexts;
527
+
528
+ // Fold each source context into the local currency it resolves to. Resolution prefers the token mapping (so
529
+ // a same-asset token at a different remote address binds to the right local context) and falls back to
530
+ // identity for same-address tokens; the local currency is then derived from that resolved local token's
531
+ // authoritative accounting context, NOT trusted from the wire, so a same-asset token at a different address
532
+ // still folds under the receiver's own currency. Multiple source contexts that resolve to the same local
533
+ // currency (e.g. the same token across multiple terminals) are summed.
534
+ uint256 numContexts = root.sourceContexts.length;
535
+ for (uint256 i; i < numContexts;) {
536
+ JBSourceContext calldata ctx = root.sourceContexts[i];
537
+
538
+ address contextToken = _localTokenForRemoteToken[ctx.token];
539
+ if (contextToken == address(0)) contextToken = _toAddress(ctx.token);
540
+ (uint32 contextCurrency, bool authoritative) = _localCurrencyOf(contextToken);
541
+ // Cache an authoritative currency so later snapshots reuse it instead of re-reading the terminal. The
542
+ // accounting-context currency is immutable, so the cache never goes stale; a not-yet-configured token
543
+ // is left uncached and re-read next time.
544
+ if (authoritative && _cachedCurrencyOf[contextToken] == 0) {
545
+ _cachedCurrencyOf[contextToken] = contextCurrency;
546
+ }
547
+
548
+ // Accumulate into an existing same-currency entry, or append a new one. The context set is small
549
+ // (one entry per distinct local currency), so a linear scan is cheaper than a mapping.
550
+ uint256 numStored = _peerContexts.length;
551
+ bool merged;
552
+ for (uint256 j; j < numStored;) {
553
+ if (_peerContexts[j].currency == contextCurrency) {
554
+ _peerContexts[j].surplus = _saturatingAddU128(_peerContexts[j].surplus, ctx.surplus);
555
+ _peerContexts[j].balance = _saturatingAddU128(_peerContexts[j].balance, ctx.balance);
556
+ merged = true;
557
+ break;
558
+ }
559
+ unchecked {
560
+ ++j;
561
+ }
562
+ }
563
+ if (!merged) {
564
+ _peerContexts.push(
565
+ JBPeerChainContext({
566
+ currency: contextCurrency,
567
+ decimals: ctx.decimals,
568
+ surplus: ctx.surplus,
569
+ balance: ctx.balance
570
+ })
571
+ );
572
+ }
573
+
574
+ unchecked {
575
+ ++i;
576
+ }
577
+ }
529
578
  }
530
579
  }
531
580
 
@@ -774,78 +823,19 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
774
823
  return _outboxOf[token];
775
824
  }
776
825
 
777
- /// @notice The peer chain balance, converted from the source denomination to the requested currency and decimal
778
- /// precision using the local JBPrices oracle.
779
- /// @param decimals The decimal precision for the returned value.
780
- /// @param currency The currency to normalize to (e.g. `uint256(uint160(JBConstants.NATIVE_TOKEN))` for ETH).
781
- /// @return A `JBDenominatedAmount` with the converted value.
782
- function peerChainBalanceOf(uint256 decimals, uint256 currency) external view returns (JBDenominatedAmount memory) {
783
- return JBDenominatedAmount({
784
- value: _convertPeerValue({source: _peerChainBalance, decimals: decimals, currency: currency}),
785
- // forge-lint: disable-next-line(unsafe-typecast)
786
- currency: uint32(currency),
787
- // forge-lint: disable-next-line(unsafe-typecast)
788
- decimals: uint8(decimals)
789
- });
790
- }
791
-
792
- /// @notice The peer chain balance bundled with the peer chain ID and snapshot freshness key.
793
- /// @dev Lets aggregators (e.g. `JBSuckerRegistry`) read the value, the peer chain it belongs to, and its
794
- /// freshness in one call instead of three separate staticcalls. The `value` is identical to
795
- /// `peerChainBalanceOf`.
796
- /// @param decimals The decimal precision for the returned value.
797
- /// @param currency The currency to normalize to (e.g. `uint256(uint160(JBConstants.NATIVE_TOKEN))` for ETH).
798
- /// @return A `JBPeerChainValue` with the converted balance, peer chain ID, and snapshot freshness key.
799
- function peerChainBalanceValueOf(
800
- uint256 decimals,
801
- uint256 currency
802
- )
803
- external
804
- view
805
- returns (JBPeerChainValue memory)
806
- {
807
- return JBPeerChainValue({
808
- value: _convertPeerValue({source: _peerChainBalance, decimals: decimals, currency: currency}),
809
- peerChainId: peerChainId(),
810
- snapshotTimestamp: snapshotTimestamp
811
- });
812
- }
813
-
814
- /// @notice The peer chain surplus, converted from the source denomination to the requested currency and decimal
815
- /// precision using the local JBPrices oracle.
816
- /// @param decimals The decimal precision for the returned value.
817
- /// @param currency The currency to normalize to (e.g. `uint256(uint160(JBConstants.NATIVE_TOKEN))` for ETH).
818
- /// @return A `JBDenominatedAmount` with the converted value.
819
- function peerChainSurplusOf(uint256 decimals, uint256 currency) external view returns (JBDenominatedAmount memory) {
820
- return JBDenominatedAmount({
821
- value: _convertPeerValue({source: _peerChainSurplus, decimals: decimals, currency: currency}),
822
- // forge-lint: disable-next-line(unsafe-typecast)
823
- currency: uint32(currency),
824
- // forge-lint: disable-next-line(unsafe-typecast)
825
- decimals: uint8(decimals)
826
- });
827
- }
828
-
829
- /// @notice The peer chain surplus bundled with the peer chain ID and snapshot freshness key.
830
- /// @dev Lets aggregators (e.g. `JBSuckerRegistry`) read the value, the peer chain it belongs to, and its
831
- /// freshness in one call instead of three separate staticcalls. The `value` is identical to
832
- /// `peerChainSurplusOf`.
833
- /// @param decimals The decimal precision for the returned value.
834
- /// @param currency The currency to normalize to (e.g. `uint256(uint160(JBConstants.NATIVE_TOKEN))` for ETH).
835
- /// @return A `JBPeerChainValue` with the converted surplus, peer chain ID, and snapshot freshness key.
836
- function peerChainSurplusValueOf(
837
- uint256 decimals,
838
- uint256 currency
839
- )
826
+ /// @notice The peer chain's raw per-context surplus and balance from the latest snapshot, bundled with the peer
827
+ /// chain ID and snapshot freshness key.
828
+ /// @dev The contexts are un-valued, in each context's own currency and decimals — the registry dedups same-peer
829
+ /// suckers by freshness, then values each context into a requested currency. The sucker consults no price oracle.
830
+ /// @return contexts The per-currency surplus and balance from the latest snapshot.
831
+ /// @return chainId The peer chain these contexts belong to.
832
+ /// @return snapshot The source freshness key of the latest snapshot.
833
+ function peerChainContextsOf()
840
834
  external
841
835
  view
842
- returns (JBPeerChainValue memory)
836
+ returns (JBPeerChainContext[] memory contexts, uint256 chainId, uint256 snapshot)
843
837
  {
844
- return JBPeerChainValue({
845
- value: _convertPeerValue({source: _peerChainSurplus, decimals: decimals, currency: currency}),
846
- peerChainId: peerChainId(),
847
- snapshotTimestamp: snapshotTimestamp
848
- });
838
+ return (_peerContexts, peerChainId(), snapshotTimestamp);
849
839
  }
850
840
 
851
841
  /// @notice The peer chain total supply bundled with the peer chain ID and snapshot freshness key.
@@ -1658,25 +1648,43 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1658
1648
  return ERC2771Context._contextSuffixLength();
1659
1649
  }
1660
1650
 
1661
- /// @notice Convert a peer chain snapshot value to the requested currency and decimal precision.
1662
- /// @dev Delegates to `JBSuckerLib.convertPeerValue` (deployed library, called via DELEGATECALL) to reduce
1663
- /// child contract bytecode.
1664
- /// @param source The peer chain snapshot containing value, currency, and decimals.
1665
- /// @param decimals The target decimal precision.
1666
- /// @param currency The target currency (e.g. `uint256(uint160(JBConstants.NATIVE_TOKEN))` for ETH).
1667
- /// @return converted The converted value.
1668
- function _convertPeerValue(
1669
- JBDenominatedAmount memory source,
1670
- uint256 decimals,
1671
- uint256 currency
1672
- )
1673
- internal
1674
- view
1675
- returns (uint256 converted)
1676
- {
1677
- converted = JBSuckerLib.convertPeerValue({
1678
- prices: PRICES, projectId: projectId(), source: source, decimals: decimals, currency: currency
1679
- });
1651
+ /// @notice The authoritative accounting-context currency the project uses for a local token. Peer context is keyed
1652
+ /// by this currency so a consumer reads it under the same currency it already works in (which the project may set
1653
+ /// to a well-known id like USD rather than the token-keyed convention).
1654
+ /// @dev Both lookups use a low-level staticcall guarded by a returndata-length check, so a missing or
1655
+ /// non-conforming directory/terminal (including one that returns short/empty data) can't block a bridge message —
1656
+ /// it just yields the fallback. Returns the cached value when one exists (the accounting-context currency is
1657
+ /// immutable, so the cache never goes stale). Falls back to the conventional `uint32(uint160(token))` only when the
1658
+ /// project has no local accounting context for the token yet; that fallback is NOT cached, so a later snapshot
1659
+ /// re-reads it once the context exists.
1660
+ /// @param token The resolved local token.
1661
+ /// @return currency The project's accounting-context currency for the token.
1662
+ /// @return authoritative Whether `currency` came from a cached or configured accounting context (true) or the
1663
+ /// convention fallback (false). `fromRemote` caches only authoritative results, since those are immutable.
1664
+ function _localCurrencyOf(address token) internal view returns (uint32 currency, bool authoritative) {
1665
+ // Reuse the value derived on an earlier snapshot — no need to read the terminal again.
1666
+ uint32 cached = _cachedCurrencyOf[token];
1667
+ if (cached != 0) return (cached, true);
1668
+
1669
+ uint256 forProjectId = projectId();
1670
+
1671
+ // Resolve the project's primary terminal for the token. An `address` return needs a full word.
1672
+ (bool terminalOk, bytes memory terminalData) =
1673
+ address(DIRECTORY).staticcall(abi.encodeCall(IJBDirectory.primaryTerminalOf, (forProjectId, token)));
1674
+ if (terminalOk && terminalData.length >= 32) {
1675
+ address terminal = abi.decode(terminalData, (address));
1676
+ if (terminal != address(0)) {
1677
+ // Read the token's accounting context. The struct encodes to three words.
1678
+ (bool contextOk, bytes memory contextData) =
1679
+ terminal.staticcall(abi.encodeCall(IJBTerminal.accountingContextForTokenOf, (forProjectId, token)));
1680
+ if (contextOk && contextData.length >= 96) {
1681
+ JBAccountingContext memory accountingContext = abi.decode(contextData, (JBAccountingContext));
1682
+ if (accountingContext.currency != 0) return (accountingContext.currency, true);
1683
+ }
1684
+ }
1685
+ }
1686
+ // forge-lint: disable-next-line(unsafe-typecast)
1687
+ return (uint32(uint160(token)), false);
1680
1688
  }
1681
1689
 
1682
1690
  /// @notice The calldata. Preferred to use over `msg.data`.
@@ -1746,6 +1754,21 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1746
1754
  }
1747
1755
  }
1748
1756
 
1757
+ /// @notice Adds two `uint128` amounts, saturating at `type(uint128).max` instead of overflowing.
1758
+ /// @dev Saturation keeps a pathological peer snapshot from reverting the receive path; the cap can only
1759
+ /// under-report a remote amount, the safe direction.
1760
+ /// @param a The first amount.
1761
+ /// @param b The second amount.
1762
+ /// @return The saturated sum.
1763
+ function _saturatingAddU128(uint128 a, uint128 b) internal pure returns (uint128) {
1764
+ unchecked {
1765
+ uint256 sum = uint256(a) + uint256(b);
1766
+ // The cast only runs when `sum <= type(uint128).max`, so it cannot truncate.
1767
+ // forge-lint: disable-next-line(unsafe-typecast)
1768
+ return sum > type(uint128).max ? type(uint128).max : uint128(sum);
1769
+ }
1770
+ }
1771
+
1749
1772
  /// @notice Selects which retained inbox root a proof should be validated against, honoring a small window of
1750
1773
  /// recently-accepted roots rather than only the latest.
1751
1774
  /// @dev Computes the branch root implied by the proof once, then returns the first retained ring root it matches.
@@ -1857,7 +1880,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1857
1880
 
1858
1881
  JBMessageRoot memory message = JBSuckerLib.buildSnapshotMessage({
1859
1882
  directory: DIRECTORY,
1860
- prices: PRICES,
1861
1883
  projectId: projectId(),
1862
1884
  remoteToken: remoteToken.addr,
1863
1885
  amount: amount,