@bananapus/suckers-v6 0.0.76 → 0.0.78

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
@@ -36,14 +36,15 @@ import {JBSuckerLib} from "./libraries/JBSuckerLib.sol";
36
36
  import {MerkleLib} from "./utils/MerkleLib.sol";
37
37
 
38
38
  // Local: structs (alphabetized)
39
+ import {JBAccountingSnapshot} from "./structs/JBAccountingSnapshot.sol";
39
40
  import {JBClaim} from "./structs/JBClaim.sol";
40
41
  import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
41
42
  import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
42
- import {JBPeerChainContext} from "./structs/JBPeerChainContext.sol";
43
- import {JBSourceContext} from "./structs/JBSourceContext.sol";
44
43
  import {JBOutboxTree} from "./structs/JBOutboxTree.sol";
44
+ import {JBPeerChainContext} from "./structs/JBPeerChainContext.sol";
45
45
  import {JBPeerChainValue} from "./structs/JBPeerChainValue.sol";
46
46
  import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
47
+ import {JBSourceContext} from "./structs/JBSourceContext.sol";
47
48
  import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
48
49
 
49
50
  /// @notice Bridges a Juicebox project's tokens and their backing terminal-token funds between two chains. Token
@@ -68,6 +69,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
68
69
  // --------------------------- custom errors ------------------------- //
69
70
  //*********************************************************************//
70
71
 
72
+ /// @notice Thrown when a bridge-specific implementation does not support accounting-only messages.
73
+ error JBSucker_AccountingSyncUnsupported();
74
+
71
75
  /// @notice Thrown when a terminal-token or project-token amount being bridged exceeds the `uint128` cap enforced
72
76
  /// for cross-VM compatibility.
73
77
  error JBSucker_AmountExceedsUint128(uint256 amount);
@@ -401,39 +405,43 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
401
405
  }
402
406
  }
403
407
 
404
- /// @notice Claim a single bridged entry: verifies the merkle proof against the inbox root, mints the specified
405
- /// project tokens for the beneficiary, and deposits the terminal tokens into the project's local balance.
406
- /// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
407
- function claim(JBClaim calldata claimData) public virtual override {
408
- // Attempt to validate the proof against the inbox tree for the terminal token. The leaf hash includes
409
- // `claimData.leaf.metadata` so the proof is only valid for the exact (amount, beneficiary, metadata) tuple the
410
- // origin committed to.
411
- _validate({
412
- projectTokenCount: claimData.leaf.projectTokenCount,
413
- terminalToken: claimData.token,
414
- terminalTokenAmount: claimData.leaf.terminalTokenAmount,
415
- beneficiary: claimData.leaf.beneficiary,
416
- metadata: claimData.leaf.metadata,
417
- index: claimData.leaf.index,
418
- leaves: claimData.proof
419
- });
408
+ /// @notice Claim retained failed-fee ETH.
409
+ /// @param beneficiary The address that should receive the retained ETH.
410
+ function claimRetainedToRemoteFee(address payable beneficiary) external override {
411
+ if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary({beneficiary: bytes32(0)});
420
412
 
421
- emit Claimed({
422
- beneficiary: claimData.leaf.beneficiary,
423
- token: claimData.token,
424
- projectTokenCount: claimData.leaf.projectTokenCount,
425
- terminalTokenAmount: claimData.leaf.terminalTokenAmount,
426
- index: claimData.leaf.index,
427
- metadata: claimData.leaf.metadata,
428
- caller: _msgSender()
413
+ address account = _msgSender();
414
+ uint256 amount = retainedToRemoteFeeOf[account];
415
+ if (amount == 0) revert JBSucker_NoRetainedToRemoteFee(account);
416
+
417
+ retainedToRemoteFeeOf[account] = 0;
418
+ retainedToRemoteFeeBalance -= amount;
419
+
420
+ _sendNativeTo({beneficiary: beneficiary, amount: amount});
421
+
422
+ // State was cleared before sending ETH; the event is emitted after the transfer so failed sends do not log.
423
+ emit RetainedToRemoteFeeClaimed({
424
+ account: account, beneficiary: beneficiary, amount: amount, caller: _msgSender()
429
425
  });
426
+ }
430
427
 
431
- // Give the user their project tokens, send the project its funds.
432
- _handleClaim({
433
- terminalToken: claimData.token,
434
- terminalTokenAmount: claimData.leaf.terminalTokenAmount,
435
- projectTokenAmount: claimData.leaf.projectTokenCount,
436
- beneficiary: claimData.leaf.beneficiary
428
+ /// @notice Claim retained failed transport-payment refund ETH.
429
+ /// @param beneficiary The address that should receive the retained ETH.
430
+ function claimRetainedTransportPaymentRefund(address payable beneficiary) external override {
431
+ if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary({beneficiary: bytes32(0)});
432
+
433
+ address account = _msgSender();
434
+ uint256 amount = retainedTransportPaymentRefundOf[account];
435
+ if (amount == 0) revert JBSucker_NoRetainedTransportPaymentRefund(account);
436
+
437
+ retainedTransportPaymentRefundOf[account] = 0;
438
+ retainedTransportPaymentRefundBalance -= amount;
439
+
440
+ _sendNativeTo({beneficiary: beneficiary, amount: amount});
441
+
442
+ // State was cleared before sending ETH; the event is emitted after the transfer so failed sends do not log.
443
+ emit RetainedTransportPaymentRefundClaimed({
444
+ account: account, beneficiary: beneficiary, amount: amount, caller: _msgSender()
437
445
  });
438
446
  }
439
447
 
@@ -571,78 +579,38 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
571
579
  });
572
580
  }
573
581
 
574
- // --- Project-wide shared state update (gated by source freshness key) ---
575
- // Only accept snapshots whose source freshness key is strictly newer than the last accepted one.
576
- // This prevents a staler per-token message from rolling back shared state (surplus, balance, supply)
577
- // that was already updated by a fresher message for a different token.
578
- if (root.sourceTimestamp > snapshotTimestamp) {
579
- // Advance the snapshot freshness key (used by the registry to dedup same-peer suckers).
580
- snapshotTimestamp = root.sourceTimestamp;
581
-
582
- // Update unconditionally — a legitimate zero supply must clear phantom cached supply.
583
- peerChainTotalSupply = root.sourceTotalSupply;
584
-
585
- // Rebuild the per-currency context set from scratch. A context that dropped out of this fresher snapshot is
586
- // simply absent from the new set, so no per-entry clearing is needed.
587
- delete _peerContexts;
588
-
589
- // Fold each source context into the local currency it resolves to. Resolution prefers the token mapping (so
590
- // a same-asset token at a different remote address binds to the right local context) and falls back to
591
- // identity for same-address tokens; the local currency is then derived from that resolved local token's
592
- // authoritative accounting context, NOT trusted from the wire, so a same-asset token at a different address
593
- // still folds under the receiver's own currency. Multiple source contexts that resolve to the same local
594
- // currency (e.g. the same token across multiple terminals) are summed.
595
- uint256 numContexts = root.sourceContexts.length;
596
- for (uint256 i; i < numContexts;) {
597
- JBSourceContext calldata ctx = root.sourceContexts[i];
598
-
599
- address contextToken = _localTokenForRemoteToken[ctx.token];
600
- if (contextToken == address(0)) contextToken = _toAddress(ctx.token);
601
- (uint32 contextCurrency, bool authoritative) = _localCurrencyOf(contextToken);
602
- // Cache an authoritative currency so later snapshots reuse it instead of re-reading the terminal. The
603
- // accounting-context currency is immutable, so the cache never goes stale; a not-yet-configured token
604
- // is left uncached and re-read next time.
605
- if (authoritative && _cachedCurrencyOf[contextToken] == 0) {
606
- _cachedCurrencyOf[contextToken] = contextCurrency;
607
- }
582
+ _storePeerChainAccounting({
583
+ sourceTimestamp: root.sourceTimestamp,
584
+ sourceTotalSupply: root.sourceTotalSupply,
585
+ sourceContexts: root.sourceContexts
586
+ });
587
+ }
608
588
 
609
- // Accumulate into an existing entry that matches on BOTH currency AND decimals, or append a new one.
610
- // The decimals must match: `surplus`/`balance` are raw, un-valued token amounts, so two contexts that
611
- // share a currency but carry different decimals (e.g. a 6-decimal and an 18-decimal representation of
612
- // the same currency) are on different scales and CANNOT be summed directly doing so would corrupt
613
- // the
614
- // aggregate. Keeping them as separate entries lets the read side (`remoteSurplusOf` -> `_valued`)
615
- // decimal-adjust each one independently before summing. The context set is small (one entry per
616
- // distinct local currency+decimals), so a linear scan is cheaper than a mapping.
617
- uint256 numStored = _peerContexts.length;
618
- bool merged;
619
- for (uint256 j; j < numStored;) {
620
- if (_peerContexts[j].currency == contextCurrency && _peerContexts[j].decimals == ctx.decimals) {
621
- _peerContexts[j].surplus = _saturatingAddU128(_peerContexts[j].surplus, ctx.surplus);
622
- _peerContexts[j].balance = _saturatingAddU128(_peerContexts[j].balance, ctx.balance);
623
- merged = true;
624
- break;
625
- }
626
- unchecked {
627
- ++j;
628
- }
629
- }
630
- if (!merged) {
631
- _peerContexts.push(
632
- JBPeerChainContext({
633
- currency: contextCurrency,
634
- decimals: ctx.decimals,
635
- surplus: ctx.surplus,
636
- balance: ctx.balance
637
- })
638
- );
639
- }
589
+ /// @notice Receive a peer-chain accounting snapshot from the remote sucker without updating any token-local inbox
590
+ /// root.
591
+ /// @dev This can only be called by the messenger contract on the local chain, with a message from the remote peer.
592
+ /// It shares the same freshness gate as `fromRemote`, so stale accounting-only messages cannot roll back fresher
593
+ /// state delivered by a root message.
594
+ /// @param snapshot The peer-chain accounting snapshot to receive.
595
+ function fromRemoteAccounting(JBAccountingSnapshot calldata snapshot) external override {
596
+ // Make sure that the message came from our peer.
597
+ // Use msg.sender (not _msgSender()) because bridge messengers never use ERC2771 meta-transactions.
598
+ // Using _msgSender() would allow a trusted forwarder to spoof the bridge messenger address via the
599
+ // ERC-2771 calldata suffix.
600
+ if (!_isRemotePeer(msg.sender)) {
601
+ revert JBSucker_NotPeer({caller: _toBytes32(msg.sender)});
602
+ }
640
603
 
641
- unchecked {
642
- ++i;
643
- }
644
- }
604
+ // Validate the message version to reject incompatible messages.
605
+ if (snapshot.version != MESSAGE_VERSION) {
606
+ revert JBSucker_InvalidMessageVersion({received: snapshot.version, expected: MESSAGE_VERSION});
645
607
  }
608
+
609
+ _storePeerChainAccounting({
610
+ sourceTimestamp: snapshot.sourceTimestamp,
611
+ sourceTotalSupply: snapshot.sourceTotalSupply,
612
+ sourceContexts: snapshot.sourceContexts
613
+ });
646
614
  }
647
615
 
648
616
  /// @notice Configure which remote-chain tokens each local terminal token maps to, enabling (or disabling) those
@@ -789,6 +757,26 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
789
757
  emit DeprecationTimeUpdated({timestamp: timestamp, caller: _msgSender()});
790
758
  }
791
759
 
760
+ /// @notice Send the latest peer-chain accounting data without sending an outbox root or paying the registry
761
+ /// `toRemoteFee`.
762
+ /// @dev The caller still provides any bridge transport payment through `msg.value`.
763
+ function syncAccountingData() external payable override {
764
+ // Accounting-only messages are outbound bridge messages and follow the same deprecation boundary as roots.
765
+ _requireSendingEnabled();
766
+
767
+ uint256 sourceTimestamp = _nextSourceTimestamp();
768
+ JBAccountingSnapshot memory snapshot = JBSuckerLib.buildAccountingSnapshot({
769
+ directory: DIRECTORY,
770
+ projectId: projectId(),
771
+ messageVersion: MESSAGE_VERSION,
772
+ sourceTimestamp: sourceTimestamp
773
+ });
774
+
775
+ emit AccountingDataSynced({sourceTimestamp: sourceTimestamp, caller: _msgSender()});
776
+
777
+ _sendAccountingSnapshotOverAMB({transportPayment: msg.value, snapshot: snapshot});
778
+ }
779
+
792
780
  /// @notice Send the accumulated outbox merkle root and locked terminal-token funds for a given `token` across the
793
781
  /// bridge to the remote peer sucker. Anyone can call this once entries exist in the outbox. Requires `msg.value`
794
782
  /// to cover the registry's `toRemoteFee` plus any bridge transport payment.
@@ -939,12 +927,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
939
927
  return amount;
940
928
  }
941
929
 
942
- /// @notice Returns the chain on which the peer is located.
943
- /// @dev `public` (not `external`) so the combined peer-chain views in this contract can read it internally
944
- /// without a self-call; subclasses implement the bridge-specific chain ID.
945
- /// @return chain ID of the peer.
946
- function peerChainId() public view virtual returns (uint256);
947
-
948
930
  /// @notice The peer sucker on the remote chain, as a bytes32 for cross-VM compatibility.
949
931
  /// @dev Defaults to `_toBytes32(address(this))`, assuming deterministic cross-chain deployment via CREATE2. The
950
932
  /// deployer (`JBSuckerDeployer`) uses `salt = keccak256(abi.encodePacked(_msgSender(), salt))` to ensure
@@ -957,6 +939,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
957
939
  return _toBytes32(address(this));
958
940
  }
959
941
 
942
+ /// @notice Returns the chain on which the peer is located.
943
+ /// @dev `public` (not `external`) so the combined peer-chain views in this contract can read it internally
944
+ /// without a self-call; subclasses implement the bridge-specific chain ID.
945
+ /// @return chain ID of the peer.
946
+ function peerChainId() public view virtual returns (uint256);
947
+
960
948
  /// @notice The ID of the project (on the local chain) that this sucker is associated with.
961
949
  /// @return The local project ID.
962
950
  function projectId() public view returns (uint256) {
@@ -1003,43 +991,39 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1003
991
  // ----------------------- public transactions ----------------------- //
1004
992
  //*********************************************************************//
1005
993
 
1006
- /// @notice Claim retained failed-fee ETH.
1007
- /// @param beneficiary The address that should receive the retained ETH.
1008
- function claimRetainedToRemoteFee(address payable beneficiary) external override {
1009
- if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary({beneficiary: bytes32(0)});
1010
-
1011
- address account = _msgSender();
1012
- uint256 amount = retainedToRemoteFeeOf[account];
1013
- if (amount == 0) revert JBSucker_NoRetainedToRemoteFee(account);
1014
-
1015
- retainedToRemoteFeeOf[account] = 0;
1016
- retainedToRemoteFeeBalance -= amount;
1017
-
1018
- _sendNativeTo({beneficiary: beneficiary, amount: amount});
1019
-
1020
- // State was cleared before sending ETH; the event is emitted after the transfer so failed sends do not log.
1021
- emit RetainedToRemoteFeeClaimed({
1022
- account: account, beneficiary: beneficiary, amount: amount, caller: _msgSender()
994
+ /// @notice Claim a single bridged entry: verifies the merkle proof against the inbox root, mints the specified
995
+ /// project tokens for the beneficiary, and deposits the terminal tokens into the project's local balance.
996
+ /// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
997
+ function claim(JBClaim calldata claimData) public virtual override {
998
+ // Attempt to validate the proof against the inbox tree for the terminal token. The leaf hash includes
999
+ // `claimData.leaf.metadata` so the proof is only valid for the exact (amount, beneficiary, metadata) tuple the
1000
+ // origin committed to.
1001
+ _validate({
1002
+ projectTokenCount: claimData.leaf.projectTokenCount,
1003
+ terminalToken: claimData.token,
1004
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
1005
+ beneficiary: claimData.leaf.beneficiary,
1006
+ metadata: claimData.leaf.metadata,
1007
+ index: claimData.leaf.index,
1008
+ leaves: claimData.proof
1023
1009
  });
1024
- }
1025
-
1026
- /// @notice Claim retained failed transport-payment refund ETH.
1027
- /// @param beneficiary The address that should receive the retained ETH.
1028
- function claimRetainedTransportPaymentRefund(address payable beneficiary) external override {
1029
- if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary({beneficiary: bytes32(0)});
1030
-
1031
- address account = _msgSender();
1032
- uint256 amount = retainedTransportPaymentRefundOf[account];
1033
- if (amount == 0) revert JBSucker_NoRetainedTransportPaymentRefund(account);
1034
1010
 
1035
- retainedTransportPaymentRefundOf[account] = 0;
1036
- retainedTransportPaymentRefundBalance -= amount;
1037
-
1038
- _sendNativeTo({beneficiary: beneficiary, amount: amount});
1011
+ emit Claimed({
1012
+ beneficiary: claimData.leaf.beneficiary,
1013
+ token: claimData.token,
1014
+ projectTokenCount: claimData.leaf.projectTokenCount,
1015
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
1016
+ index: claimData.leaf.index,
1017
+ metadata: claimData.leaf.metadata,
1018
+ caller: _msgSender()
1019
+ });
1039
1020
 
1040
- // State was cleared before sending ETH; the event is emitted after the transfer so failed sends do not log.
1041
- emit RetainedTransportPaymentRefundClaimed({
1042
- account: account, beneficiary: beneficiary, amount: amount, caller: _msgSender()
1021
+ // Give the user their project tokens, send the project its funds.
1022
+ _handleClaim({
1023
+ terminalToken: claimData.token,
1024
+ terminalTokenAmount: claimData.leaf.terminalTokenAmount,
1025
+ projectTokenAmount: claimData.leaf.projectTokenCount,
1026
+ beneficiary: claimData.leaf.beneficiary
1043
1027
  });
1044
1028
  }
1045
1029
 
@@ -1056,20 +1040,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1056
1040
  _initialize({initialProjectId: localProjectId, remotePeer: remotePeer});
1057
1041
  }
1058
1042
 
1059
- /// @notice Initializes the sucker's project and optional peer address.
1060
- /// @param initialProjectId The ID of the project (on the local chain) that this sucker is associated with.
1061
- /// @param remotePeer The remote peer address. Leave zero to use the default deterministic same-address peer.
1062
- function _initialize(uint256 initialProjectId, bytes32 remotePeer) internal {
1063
- _localProjectId = initialProjectId;
1064
- _peer = remotePeer;
1065
- deployer = _msgSender();
1066
- }
1067
-
1068
1043
  /// @notice Map an ERC-20 token on the local chain to a remote-chain ERC-20 token for bridging.
1069
1044
  /// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
1070
1045
  /// them.
1071
1046
  function mapToken(JBTokenMapping calldata map) public payable override {
1072
- _mapToken({map: map, transportPaymentValue: msg.value});
1047
+ uint256 transportPaymentSpent = _mapToken({map: map, transportPaymentValue: msg.value});
1048
+ if (msg.value > transportPaymentSpent) {
1049
+ _sendNativeTo({beneficiary: payable(_msgSender()), amount: msg.value - transportPaymentSpent});
1050
+ }
1073
1051
  }
1074
1052
 
1075
1053
  //*********************************************************************//
@@ -1165,6 +1143,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1165
1143
  });
1166
1144
  }
1167
1145
 
1146
+ /// @notice Initializes the sucker's project and optional peer address.
1147
+ /// @param initialProjectId The ID of the project (on the local chain) that this sucker is associated with.
1148
+ /// @param remotePeer The remote peer address. Leave zero to use the default deterministic same-address peer.
1149
+ function _initialize(uint256 initialProjectId, bytes32 remotePeer) internal {
1150
+ _localProjectId = initialProjectId;
1151
+ _peer = remotePeer;
1152
+ deployer = _msgSender();
1153
+ }
1154
+
1168
1155
  /// @notice Inserts a new leaf into the outbox merkle tree for the specified `token`.
1169
1156
  /// @param projectTokenCount The amount of project tokens to cash out.
1170
1157
  /// @param token The terminal token to cash out for.
@@ -1377,6 +1364,49 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1377
1364
  assert(reclaimedAmount == _balanceOf({token: token, addr: address(this)}) - balanceBefore);
1378
1365
  }
1379
1366
 
1367
+ /// @notice Retain a failed `toRemoteFee` payment for later caller refund.
1368
+ /// @param account The account that can reclaim the retained fee.
1369
+ /// @param amount The retained fee amount.
1370
+ function _retainToRemoteFee(address account, uint256 amount) internal {
1371
+ retainedToRemoteFeeOf[account] += amount;
1372
+ retainedToRemoteFeeBalance += amount;
1373
+ emit RetainedToRemoteFee({account: account, amount: amount, caller: _msgSender()});
1374
+ }
1375
+
1376
+ /// @notice Retains a failed transport-payment refund as account-scoped native credit.
1377
+ /// @param account The account that can reclaim the retained refund.
1378
+ /// @param amount The retained refund amount.
1379
+ function _retainTransportPaymentRefund(address account, uint256 amount) internal {
1380
+ retainedTransportPaymentRefundOf[account] += amount;
1381
+ retainedTransportPaymentRefundBalance += amount;
1382
+ emit RetainedTransportPaymentRefund({account: account, amount: amount, caller: _msgSender()});
1383
+ }
1384
+
1385
+ /// @notice Performs the logic to send an accounting-only message to the peer over the AMB.
1386
+ /// @dev Bridge-specific implementations override this for supported transports.
1387
+ /// @param transportPayment The amount of `msg.value` paid to the transport for this message.
1388
+ /// @param snapshot The accounting snapshot to send to the remote chain.
1389
+ // forge-lint: disable-next-line(mixed-case-function)
1390
+ function _sendAccountingSnapshotOverAMB(
1391
+ uint256 transportPayment,
1392
+ JBAccountingSnapshot memory snapshot
1393
+ )
1394
+ internal
1395
+ virtual
1396
+ {
1397
+ transportPayment;
1398
+ snapshot;
1399
+ revert JBSucker_AccountingSyncUnsupported();
1400
+ }
1401
+
1402
+ /// @notice Send native tokens, reverting if the recipient rejects them.
1403
+ /// @param beneficiary The recipient.
1404
+ /// @param amount The amount to send.
1405
+ function _sendNativeTo(address payable beneficiary, uint256 amount) internal {
1406
+ (bool success,) = beneficiary.call{value: amount}("");
1407
+ if (!success) revert JBSucker_RefundFailed({beneficiary: beneficiary, amount: amount});
1408
+ }
1409
+
1380
1410
  /// @notice Send the outbox root for the specified token to the remote peer.
1381
1411
  /// @dev Some bridges require a nonzero `transportPayment`; zero-cost bridges must reject nonzero values.
1382
1412
  /// @param transportPayment The amount of `msg.value` paid to the transport for this message.
@@ -1436,14 +1466,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1436
1466
  });
1437
1467
  }
1438
1468
 
1439
- /// @notice Send native tokens, reverting if the recipient rejects them.
1440
- /// @param beneficiary The recipient.
1441
- /// @param amount The amount to send.
1442
- function _sendNativeTo(address payable beneficiary, uint256 amount) internal {
1443
- (bool success,) = beneficiary.call{value: amount}("");
1444
- if (!success) revert JBSucker_RefundFailed({beneficiary: beneficiary, amount: amount});
1445
- }
1446
-
1447
1469
  /// @notice Performs the logic to send a message to the peer over the AMB.
1448
1470
  /// @dev This is chain/sucker/bridge specific logic.
1449
1471
  /// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
@@ -1464,6 +1486,81 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1464
1486
  internal
1465
1487
  virtual;
1466
1488
 
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(
1496
+ uint256 sourceTimestamp,
1497
+ uint256 sourceTotalSupply,
1498
+ JBSourceContext[] calldata sourceContexts
1499
+ )
1500
+ internal
1501
+ {
1502
+ // Only accept snapshots whose source freshness key is strictly newer than the last accepted one.
1503
+ if (sourceTimestamp <= snapshotTimestamp) return;
1504
+
1505
+ // Advance the snapshot freshness key used by the registry to dedup same-peer suckers.
1506
+ snapshotTimestamp = sourceTimestamp;
1507
+
1508
+ // Update unconditionally — a legitimate zero supply must clear phantom cached supply.
1509
+ peerChainTotalSupply = sourceTotalSupply;
1510
+
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;
1514
+
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
+ uint256 numContexts = sourceContexts.length;
1520
+ 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
+
1558
+ unchecked {
1559
+ ++i;
1560
+ }
1561
+ }
1562
+ }
1563
+
1467
1564
  /// @notice Validates a leaf as being in the inbox merkle tree and registers the leaf as executed (to prevent
1468
1565
  /// double-spending).
1469
1566
  /// @dev Reverts if the leaf is invalid.
@@ -1657,28 +1754,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1657
1754
  return IERC20(token).balanceOf(addr);
1658
1755
  }
1659
1756
 
1660
- /// @notice Compute the merkle root of an outbox tree by reading its branch into memory and delegating
1661
- /// to JBSuckerLib.computeTreeRoot (via DELEGATECALL). Replaces inlined MerkleLib.root() to save ~3KB.
1662
- /// @param tree The storage-backed merkle tree.
1663
- /// @return The merkle root.
1664
- function _computeOutboxRoot(MerkleLib.Tree storage tree) internal view returns (bytes32) {
1665
- uint256 count = tree.count;
1666
- // An empty tree has a known zero root.
1667
- if (count == 0) return MerkleLib.Z_32;
1668
-
1669
- // Copy only the non-zero branch slots from storage into memory for the root computation.
1670
- bytes32[_TREE_DEPTH] memory branch;
1671
- for (uint256 i; i < _TREE_DEPTH;) {
1672
- if (count & (uint256(1) << i) != 0) {
1673
- branch[i] = tree.branch[i];
1674
- }
1675
- unchecked {
1676
- ++i;
1677
- }
1678
- }
1679
- return JBSuckerLib.computeTreeRoot({branch: branch, count: count});
1680
- }
1681
-
1682
1757
  /// @notice Builds a hash as they are stored in the merkle tree.
1683
1758
  /// @param projectTokenCount The number of project tokens to cash out.
1684
1759
  /// @param terminalTokenAmount The amount of terminal tokens to reclaim from the cash out.
@@ -1707,6 +1782,28 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1707
1782
  }
1708
1783
  }
1709
1784
 
1785
+ /// @notice Compute the merkle root of an outbox tree by reading its branch into memory and delegating
1786
+ /// to JBSuckerLib.computeTreeRoot (via DELEGATECALL). Replaces inlined MerkleLib.root() to save ~3KB.
1787
+ /// @param tree The storage-backed merkle tree.
1788
+ /// @return The merkle root.
1789
+ function _computeOutboxRoot(MerkleLib.Tree storage tree) internal view returns (bytes32) {
1790
+ uint256 count = tree.count;
1791
+ // An empty tree has a known zero root.
1792
+ if (count == 0) return MerkleLib.Z_32;
1793
+
1794
+ // Copy only the non-zero branch slots from storage into memory for the root computation.
1795
+ bytes32[_TREE_DEPTH] memory branch;
1796
+ for (uint256 i; i < _TREE_DEPTH;) {
1797
+ if (count & (uint256(1) << i) != 0) {
1798
+ branch[i] = tree.branch[i];
1799
+ }
1800
+ unchecked {
1801
+ ++i;
1802
+ }
1803
+ }
1804
+ return JBSuckerLib.computeTreeRoot({branch: branch, count: count});
1805
+ }
1806
+
1710
1807
  /// @notice The length of the context suffix for ERC-2771 meta-transactions.
1711
1808
  /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
1712
1809
  /// @return The suffix length in bytes.
@@ -1779,24 +1876,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1779
1876
  return PROJECTS.ownerOf(forProjectId);
1780
1877
  }
1781
1878
 
1782
- /// @notice Retain a failed `toRemoteFee` payment for later caller refund.
1783
- /// @param account The account that can reclaim the retained fee.
1784
- /// @param amount The retained fee amount.
1785
- function _retainToRemoteFee(address account, uint256 amount) internal {
1786
- retainedToRemoteFeeOf[account] += amount;
1787
- retainedToRemoteFeeBalance += amount;
1788
- emit RetainedToRemoteFee({account: account, amount: amount, caller: _msgSender()});
1789
- }
1790
-
1791
- /// @notice Retains a failed transport-payment refund as account-scoped native credit.
1792
- /// @param account The account that can reclaim the retained refund.
1793
- /// @param amount The retained refund amount.
1794
- function _retainTransportPaymentRefund(address account, uint256 amount) internal {
1795
- retainedTransportPaymentRefundOf[account] += amount;
1796
- retainedTransportPaymentRefundBalance += amount;
1797
- emit RetainedTransportPaymentRefund({account: account, amount: amount, caller: _msgSender()});
1798
- }
1799
-
1800
1879
  /// @notice Returns the peer address as an EVM address.
1801
1880
  /// @return The peer address.
1802
1881
  function _peerAddress() internal view returns (address) {
@@ -1937,12 +2016,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1937
2016
  )
1938
2017
  private
1939
2018
  {
1940
- uint256 sourceTimestamp;
1941
- unchecked {
1942
- // High bits preserve the source-chain timestamp for operators/indexers. Low bits make same-timestamp
1943
- // roots distinct so the receiver can still reject stale project-wide snapshots with a strict `>`.
1944
- sourceTimestamp = (block.timestamp << 128) | ++_outboundSnapshotSequence;
1945
- }
2019
+ uint256 sourceTimestamp = _nextSourceTimestamp();
1946
2020
 
1947
2021
  JBMessageRoot memory message = JBSuckerLib.buildSnapshotMessage({
1948
2022
  directory: DIRECTORY,
@@ -1965,4 +2039,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1965
2039
  message: message
1966
2040
  });
1967
2041
  }
2042
+
2043
+ /// @notice Build the next source-chain freshness key.
2044
+ /// @dev High bits preserve the source-chain timestamp for operators/indexers. Low bits make same-timestamp
2045
+ /// snapshots distinct so the receiver can reject stale project-wide snapshots with a strict `>`.
2046
+ /// @return sourceTimestamp The next freshness key.
2047
+ function _nextSourceTimestamp() private returns (uint256 sourceTimestamp) {
2048
+ unchecked {
2049
+ sourceTimestamp = (block.timestamp << 128) | ++_outboundSnapshotSequence;
2050
+ }
2051
+ }
1968
2052
  }