@bananapus/suckers-v6 0.0.74 → 0.0.76

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.74",
3
+ "version": "0.0.76",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,111 @@
1
+ # Sucker Entry Points
2
+
3
+ Use this file when you already know the task is in `nana-suckers-v6` and need the concrete contract/function surface to open next.
4
+
5
+ > V6 is testnet-only. Deployed sucker, registry, and deployer addresses live in `deploy-all-v6`'s address output, not here.
6
+
7
+ ## Purpose
8
+
9
+ A sucker bridges a Juicebox project's token economy between two chains. Cashed-out positions are committed to a local outbox merkle tree, the root is relayed to the peer sucker on the remote chain, and beneficiaries prove inclusion against the inbox root to recreate their position on the destination chain. The `JBSucker` base owns the shared flow; chain-specific subclasses (`JBArbitrumSucker`, `JBOptimismSucker`, `JBCCIPSucker`, `JBBaseSucker`) own transport delivery and verification.
10
+
11
+ ## Contracts
12
+
13
+ | Contract | Role |
14
+ |----------|------|
15
+ | `JBSucker` | Abstract base. Owns the prepare/relay/claim/token-mapping/deprecation/emergency flow and dual merkle (outbox/inbox) state. |
16
+ | `JBArbitrumSucker` | Arbitrum bridge transport. |
17
+ | `JBOptimismSucker` | OP Stack bridge transport. |
18
+ | `JBCCIPSucker` | Chainlink CCIP transport. |
19
+ | `JBBaseSucker` | Base/OP Stack transport. |
20
+ | `JBSuckerRegistry` | Project-to-sucker inventory, deployer allowlist, shared `toRemote` fee, deprecation removal, cross-chain surplus/supply aggregation. |
21
+
22
+ `IJBSucker` is the minimal interface; `IJBSuckerExtended` adds the deprecation, emergency-hatch, and retained-fee surface.
23
+
24
+ ## Structs
25
+
26
+ ### `JBTokenMapping` (argument to `mapToken` / `mapTokens`)
27
+
28
+ | Field | Type | Meaning |
29
+ |-------|------|---------|
30
+ | `localToken` | `address` | The local token address. |
31
+ | `minGas` | `uint32` | The minimum gas to use when bridging this token. |
32
+ | `remoteToken` | `bytes32` | The remote token address (bytes32 for cross-VM compatibility). |
33
+
34
+ ### `JBClaim` (argument to `claim` / `exitThroughEmergencyHatch`)
35
+
36
+ | Field | Type | Meaning |
37
+ |-------|------|---------|
38
+ | `token` | `address` | The local terminal token to claim. |
39
+ | `leaf` | `JBLeaf` | The leaf to claim from (see below). |
40
+ | `proof` | `bytes32[32]` | The merkle proof. Must be of length `JBSucker._TREE_DEPTH` (32). |
41
+
42
+ ### `JBLeaf` (the `leaf` field of `JBClaim`)
43
+
44
+ | Field | Type | Meaning |
45
+ |-------|------|---------|
46
+ | `index` | `uint256` | The leaf's index in the tree. |
47
+ | `beneficiary` | `bytes32` | The beneficiary (bytes32 for cross-VM compatibility). |
48
+ | `projectTokenCount` | `uint256` | The number of project tokens to claim. |
49
+ | `terminalTokenAmount` | `uint256` | The amount of terminal tokens to claim. |
50
+ | `metadata` | `bytes32` | Opaque, caller-defined payload covered by the leaf hash. `bytes32(0)` when no extra context. |
51
+
52
+ ## Key functions
53
+
54
+ ### JBSucker — bridge flow (`IJBSucker`)
55
+
56
+ | Function | What it does |
57
+ |----------|--------------|
58
+ | `prepare(uint256 projectTokenCount, bytes32 beneficiary, uint256 minTokensReclaimed, address token, bytes32 metadata)` | Cash out `projectTokenCount` project tokens into `token` and insert a leaf for `beneficiary` into the outbox tree for bridging. `minTokensReclaimed` bounds slippage; `metadata` is an opaque attribution payload carried in the leaf hash (`bytes32(0)` for a plain bridge). |
59
+ | `toRemote(address token) payable` | Send the current outbox tree root and bridged assets for `token` to the remote peer through the chain-specific transport. `payable` to fund the transport message and the registry's `toRemoteFee`. |
60
+ | `claim(JBClaim calldata claimData)` | Claim bridged project tokens for the leaf's beneficiary by proving inclusion against the inbox root. |
61
+ | `claim(JBClaim[] calldata claims)` | Claim multiple leaves in one call. Each leaf is routed through an external `this.claim` sub-call, so one failing leaf emits `ClaimFailed` and is reverted in isolation while the rest of the batch proceeds; the failed leaf stays claimable later. |
62
+ | `mapToken(JBTokenMapping calldata map) payable` | Map a single local token to a remote token for bridging. Mappings are immutable once the outbox tree has entries (can only be disabled, not remapped). Requires `MAP_SUCKER_TOKEN` permission (initial mappings are applied at deploy under `DEPLOY_SUCKERS`). |
63
+ | `mapTokens(JBTokenMapping[] calldata maps) payable` | Map multiple local tokens to remote tokens in one call. |
64
+
65
+ ### JBSucker — deprecation & emergency (`IJBSuckerExtended`)
66
+
67
+ | Function | What it does |
68
+ |----------|--------------|
69
+ | `setDeprecation(uint40 timestamp)` | Set or update the deprecation timestamp. Drives the `JBSuckerState` lifecycle: `ENABLED` → `DEPRECATION_PENDING` → `SENDING_DISABLED` → `DEPRECATED`. Requires `SET_SUCKER_DEPRECATION` permission. |
70
+ | `enableEmergencyHatchFor(address[] calldata tokens)` | Open the emergency hatch for the given tokens, allowing direct claims without bridging when transport is unavailable. Requires `SUCKER_SAFETY` permission (project owner). |
71
+ | `exitThroughEmergencyHatch(JBClaim calldata claimData)` | Claim a leaf directly through an open emergency hatch when bridging cannot complete. |
72
+ | `claimRetainedToRemoteFee(address payable beneficiary)` | Withdraw ETH from a `toRemote` fee payment that previously failed and was retained for the caller. |
73
+ | `claimRetainedTransportPaymentRefund(address payable beneficiary)` | Withdraw ETH from a transport-payment refund that previously failed and was retained for the caller. |
74
+
75
+ ### JBSucker — key views (`IJBSucker` / `IJBSuckerExtended`)
76
+
77
+ | Function | What it does |
78
+ |----------|--------------|
79
+ | `state()` | Returns the current `JBSuckerState` (deprecation lifecycle stage). |
80
+ | `peer()` | Returns the peer sucker address on the remote chain (bytes32). |
81
+ | `peerChainId()` | Returns the remote peer chain ID. |
82
+ | `projectId()` | Returns the local project ID this sucker serves. |
83
+ | `isMapped(address token)` | Whether a token has been mapped for bridging. |
84
+ | `remoteTokenFor(address token)` | The `JBRemoteToken` info a local token maps to. |
85
+ | `inboxOf(address token)` | The inbox merkle tree root (`JBInboxTreeRoot`) for a token. |
86
+ | `outboxOf(address token)` | The outbox merkle tree (`JBOutboxTree`) for a token. |
87
+ | `amountToAddToBalanceOf(address token)` | Tokens received from bridging that are waiting to be added to the project's terminal balance. |
88
+ | `executedLeafHashOf(address token, uint256 index)` | The committed leaf hash at `(token, index)`, or `bytes32(0)` if unexecuted. Beneficiary contracts re-derive this to authenticate a settlement that a front-runner's direct `claim` already executed. |
89
+ | `peerChainTotalSupply()` | The last-known peer-chain total token supply (used by data hooks to compute effective cross-chain supply). |
90
+ | `peerChainContextsOf()` | Per-context raw surplus and balance from the latest peer snapshot, with chain ID and freshness key. Un-valued (each context in its own currency/decimals). |
91
+ | `retainedToRemoteFeeOf(address account)` | ETH owed to `account` from a failed `toRemote` fee payment. |
92
+ | `retainedTransportPaymentRefundOf(address account)` | ETH owed to `account` from a failed transport-payment refund. |
93
+
94
+ ### JBSuckerRegistry (`IJBSuckerRegistry`)
95
+
96
+ | Function | What it does |
97
+ |----------|--------------|
98
+ | `deploySuckersFor(uint256 projectId, bytes32 salt, JBSuckerDeployerConfig[] calldata configurations)` | Deploy one or more suckers for a project and apply each config's initial token mappings. Requires `DEPLOY_SUCKERS`. Returns the deployed sucker addresses. |
99
+ | `removeDeprecatedSucker(uint256 projectId, address sucker)` | Remove a fully deprecated sucker from a project's inventory. |
100
+ | `allowSuckerDeployer(address deployer)` / `allowSuckerDeployers(address[] calldata deployers)` | Add deployer(s) to the allowlist. Owner-only. |
101
+ | `removeSuckerDeployer(address deployer)` | Remove a deployer from the allowlist. Owner-only. |
102
+ | `setToRemoteFee(uint256 fee)` | Set the ETH fee (wei) paid into the fee project on each `toRemote` call. Reverts if `fee > MAX_TO_REMOTE_FEE`. Owner-only. |
103
+ | `suckersOf(uint256 projectId)` | All active suckers for a project. |
104
+ | `allSuckersOf(uint256 projectId)` | Every sucker ever registered for a project, including deprecated ones. |
105
+ | `suckerPairsOf(uint256 projectId)` | The local/remote sucker pairs (`JBSuckersPair[]`) for a project. |
106
+ | `isSuckerOf(uint256 projectId, address addr)` | Whether `addr` is a registry-deployed sucker for the project. |
107
+ | `suckerDeployerIsAllowed(address deployer)` | Whether a deployer is on the allowlist. |
108
+ | `remoteTotalSupplyOf(uint256 projectId)` | Combined peer-chain total supply across all remote chains (dedups same-peer suckers by freshest snapshot). |
109
+ | `totalRemoteSurplusOf(uint256 projectId, uint256 currency, uint256 decimals)` | Combined peer-chain surplus valued into `currency`. Matching-currency contexts taken at par; missing cross-currency feed skips that sucker (conservative). |
110
+ | `totalRemoteBalanceOf(uint256 projectId, uint256 currency, uint256 decimals)` | Combined peer-chain balance valued into `currency`, same valuation rules as above. |
111
+ | `toRemoteFee()` / `MAX_TO_REMOTE_FEE()` | The current `toRemote` fee and its hardcoded ceiling. |
package/src/JBSucker.sol CHANGED
@@ -143,6 +143,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
143
143
  /// @notice Thrown when `msg.value` is sent for an action that expects none.
144
144
  error JBSucker_UnexpectedMsgValue(uint256 value);
145
145
 
146
+ /// @notice Thrown when an ERC-20 terminal balance does not decrease by the amount added to the project balance.
147
+ error JBSucker_UnexpectedTokenBalance(address token, uint256 expectedBalance, uint256 actualBalance);
148
+
146
149
  /// @notice Thrown when a required beneficiary address is the zero address.
147
150
  error JBSucker_ZeroBeneficiary(bytes32 beneficiary);
148
151
 
@@ -648,42 +651,34 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
648
651
  /// @param maps A list of local and remote terminal token addresses to map, and minimum amount/gas limits for
649
652
  /// bridging them.
650
653
  function mapTokens(JBTokenMapping[] calldata maps) external payable override {
651
- uint256 numberToDisable;
654
+ uint256 disableCandidates;
652
655
 
653
- // Loop over the number of mappings and increase numberToDisable to correctly set transportPaymentValue.
654
- // Note: if all mappings are enable-only (no disables), `numberToDisable` stays 0 and `transportPaymentValue`
655
- // is set to 0 for each call. Any ETH sent with the transaction is refunded after the second loop.
656
+ // Count mappings that currently need a final outbox flush. This is an upper bound because duplicated disables
657
+ // in the same batch can become no-ops after the first one updates `numberOfClaimsSent`.
656
658
  for (uint256 h; h < maps.length;) {
657
659
  JBOutboxTree storage _outbox = _outboxOf[maps[h].localToken];
658
660
  if (maps[h].remoteToken == bytes32(0) && _outbox.numberOfClaimsSent != _outbox.tree.count) {
659
- numberToDisable++;
661
+ ++disableCandidates;
660
662
  }
661
663
  unchecked {
662
664
  ++h;
663
665
  }
664
666
  }
665
667
 
666
- // Perform each token mapping.
668
+ // Split the attached value across disable candidates, then refund any value not actually used by a final
669
+ // outbox flush. Enable-only and duplicate/no-op disable entries do not consume transport payment.
670
+ uint256 transportPaymentValue = disableCandidates == 0 ? 0 : msg.value / disableCandidates;
671
+ uint256 transportPaymentSpent;
667
672
  for (uint256 i; i < maps.length;) {
668
- _mapToken({map: maps[i], transportPaymentValue: numberToDisable > 0 ? msg.value / numberToDisable : 0});
673
+ transportPaymentSpent += _mapToken({map: maps[i], transportPaymentValue: transportPaymentValue});
669
674
  unchecked {
670
675
  ++i;
671
676
  }
672
677
  }
673
678
 
674
- // If no tokens were disabled, the full `msg.value` is unused refund it.
675
- if (numberToDisable == 0) {
676
- if (msg.value > 0) {
677
- _sendNativeTo({beneficiary: payable(_msgSender()), amount: msg.value});
678
- }
679
- } else {
680
- // Refund any remainder from integer division so dust wei isn't stuck in the contract.
681
- uint256 remainder = msg.value % numberToDisable;
682
- if (remainder > 0) {
683
- // Best-effort refund — don't revert if caller can't accept ETH.
684
- (bool _ok,) = _msgSender().call{value: remainder}("");
685
- _ok; // Silence unused-variable warning; failure is intentionally ignored.
686
- }
679
+ // Return enable-only value, duplicate/no-op disable value, and integer-division dust to the caller.
680
+ if (msg.value > transportPaymentSpent) {
681
+ _sendNativeTo({beneficiary: payable(_msgSender()), amount: msg.value - transportPaymentSpent});
687
682
  }
688
683
  }
689
684
 
@@ -1123,8 +1118,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1123
1118
  });
1124
1119
 
1125
1120
  if (isErc20) {
1126
- // Sanity check: catches fee-on-transfer / non-conforming ERC-20s that move less than `amount`.
1127
- assert(IERC20(token).balanceOf(address(this)) == balanceBefore - amount);
1121
+ // The terminal must pull exactly `amount`; fee-on-transfer or non-conforming tokens are unsupported.
1122
+ uint256 expectedBalance = balanceBefore - amount;
1123
+ uint256 actualBalance = IERC20(token).balanceOf(address(this));
1124
+ if (actualBalance != expectedBalance) {
1125
+ revert JBSucker_UnexpectedTokenBalance({
1126
+ token: token, expectedBalance: expectedBalance, actualBalance: actualBalance
1127
+ });
1128
+ }
1128
1129
  }
1129
1130
  }
1130
1131
 
@@ -1229,7 +1230,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1229
1230
  /// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
1230
1231
  /// them.
1231
1232
  /// @param transportPaymentValue The amount of `msg.value` to send for the token mapping.
1232
- function _mapToken(JBTokenMapping calldata map, uint256 transportPaymentValue) internal {
1233
+ /// @return transportPaymentSpent The amount of transport payment used by a final outbox flush.
1234
+ function _mapToken(
1235
+ JBTokenMapping calldata map,
1236
+ uint256 transportPaymentValue
1237
+ )
1238
+ internal
1239
+ returns (uint256 transportPaymentSpent)
1240
+ {
1233
1241
  address token = map.localToken;
1234
1242
  JBRemoteToken memory currentMapping = _remoteTokenFor[token];
1235
1243
 
@@ -1288,6 +1296,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1288
1296
  // _sendRoot uses the `currentMapping` parameter, not storage, so this is safe.
1289
1297
  _remoteTokenFor[token].enabled = false;
1290
1298
  _sendRoot({transportPayment: transportPaymentValue, token: token, remoteToken: currentMapping});
1299
+ transportPaymentSpent = transportPaymentValue;
1291
1300
  }
1292
1301
 
1293
1302
  // Update the reverse reservation if an unused local token is being remapped to a new remote token.
@@ -0,0 +1,108 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {JBSourceContext} from "../structs/JBSourceContext.sol";
5
+
6
+ /// @notice Helpers for reading optional `IJBPeerChainAdjustedAccounts` return data.
7
+ library JBPeerChainAdjustedAccountsLib {
8
+ /// @notice Decodes peer-chain adjusted accounting return data, falling back to no contribution if malformed.
9
+ /// @param data The raw return data from a `peerChainAdjustedAccountsOf` call.
10
+ /// @return supply The extra supply to include in `sourceTotalSupply`.
11
+ /// @return contexts The extra per-context surplus and balance to include in the snapshot, un-valued.
12
+ function decode(bytes memory data) internal pure returns (uint256 supply, JBSourceContext[] memory contexts) {
13
+ // `data` is a Solidity `bytes` value. In memory, its first word is the byte length, and the hook's ABI return
14
+ // payload begins one word later at `data + 32`.
15
+ //
16
+ // The payload for `(uint256, JBSourceContext[])` is:
17
+ // word 0: supply
18
+ // word 1: offset to the dynamic `contexts` array tail, relative to the payload start
19
+ // tail word 0: contexts.length
20
+ // tail words: each `JBSourceContext`, encoded as 4 ABI words.
21
+ //
22
+ // A valid return needs at least the two-word tuple head plus the array-length word. Anything shorter would
23
+ // make the reads below point outside the returned buffer, so the optional contribution is ignored.
24
+ if (data.length < 96) return (0, new JBSourceContext[](0));
25
+
26
+ // The tuple head is fixed-width, so read it directly instead of `abi.decode`:
27
+ // - `supply` is word 0 of the ABI payload.
28
+ // - `contextsOffset` is word 1 and points to the dynamic-array tail.
29
+ // Manual reads let malformed optional hooks fail soft instead of reverting the whole snapshot.
30
+ uint256 contextsOffset;
31
+ assembly ("memory-safe") {
32
+ // Skip the `bytes` length word and read payload word 0.
33
+ supply := mload(add(data, 32))
34
+ // Read payload word 1. The value is payload-relative, not memory-object-relative.
35
+ contextsOffset := mload(add(data, 64))
36
+ }
37
+
38
+ // The dynamic array tail must start after the two-word tuple head (`>= 64`), be ABI-word aligned, and leave
39
+ // room for its own length word. If any of these fail, `abi.decode` would revert; this helper treats the
40
+ // optional hook as absent.
41
+ if (contextsOffset < 64 || contextsOffset % 32 != 0 || contextsOffset > data.length - 32) {
42
+ return (0, new JBSourceContext[](0));
43
+ }
44
+
45
+ // `contextsOffset` was proven to leave room for the array length word, so this read is in-bounds. Add `32` to
46
+ // `data` first because offsets are relative to the payload start, not the `bytes` length word.
47
+ uint256 contextCount;
48
+ assembly ("memory-safe") {
49
+ contextCount := mload(add(add(data, 32), contextsOffset))
50
+ }
51
+
52
+ // Skip the array-length word to reach the first encoded `JBSourceContext`.
53
+ uint256 contextsStart = contextsOffset + 32;
54
+ // Each `JBSourceContext` is four ABI words: token, decimals, surplus, balance. This bounds check prevents both
55
+ // oversized allocation from a hostile length word and out-of-bounds reads in the loop.
56
+ if (contextCount > (data.length - contextsStart) / 128) return (0, new JBSourceContext[](0));
57
+
58
+ // Only allocate after proving the claimed array length fits inside the returned bytes.
59
+ contexts = new JBSourceContext[](contextCount);
60
+
61
+ for (uint256 i; i < contextCount; i++) {
62
+ // Move to the encoded struct for this index. The multiplication is safe because `contextCount` already
63
+ // proved every 128-byte struct fits in the buffer.
64
+ uint256 contextOffset = contextsStart + i * 128;
65
+ // Read narrowed fields as full words first. The ABI decoder would reject out-of-range values for
66
+ // `uint8`/`uint128`, so the manual decoder must check those ranges before casting.
67
+ bytes32 token;
68
+ uint256 decimals;
69
+ uint256 surplus;
70
+ uint256 contextBalance;
71
+
72
+ assembly ("memory-safe") {
73
+ // Point at the first word of the encoded `JBSourceContext`.
74
+ let contextPointer := add(add(data, 32), contextOffset)
75
+ // Struct word 0: source-local token, padded to bytes32.
76
+ token := mload(contextPointer)
77
+ // Struct word 1: decimal precision, encoded as a full ABI word.
78
+ decimals := mload(add(contextPointer, 32))
79
+ // Struct word 2: raw surplus in the context's own decimals.
80
+ surplus := mload(add(contextPointer, 64))
81
+ // Struct word 3: raw recorded balance in the context's own decimals.
82
+ contextBalance := mload(add(contextPointer, 96))
83
+ }
84
+
85
+ // Mirror ABI decoder type checks before narrowing. Returning `(0, [])` avoids silently truncating a
86
+ // malformed hook's values into smaller wire types.
87
+ if (decimals > type(uint8).max || surplus > type(uint128).max || contextBalance > type(uint128).max) {
88
+ return (0, new JBSourceContext[](0));
89
+ }
90
+
91
+ // Casting is safe because the guard above rejected larger values, but forge lint cannot infer that.
92
+ // forge-lint: disable-next-line(unsafe-typecast)
93
+ uint8 checkedDecimals = uint8(decimals);
94
+ // Casting is safe for the same reason as `checkedDecimals`.
95
+ // forge-lint: disable-next-line(unsafe-typecast)
96
+ uint128 checkedSurplus = uint128(surplus);
97
+ // Casting is safe for the same reason as `checkedDecimals`.
98
+ // forge-lint: disable-next-line(unsafe-typecast)
99
+ uint128 checkedBalance = uint128(contextBalance);
100
+
101
+ // Store the checked values using the struct's actual wire types. At this point every memory read was
102
+ // inside the buffer and every narrowed cast has been proven safe.
103
+ contexts[i] = JBSourceContext({
104
+ token: token, decimals: checkedDecimals, surplus: checkedSurplus, balance: checkedBalance
105
+ });
106
+ }
107
+ }
108
+ }
@@ -45,8 +45,19 @@ library JBRelayBeneficiary {
45
45
  return beneficiary;
46
46
  }
47
47
 
48
- // Decode the relay beneficiary address.
49
- address relayBeneficiary = abi.decode(data, (address));
48
+ // Load the first metadata word directly so malformed trailing bytes still fall back instead of reverting the
49
+ // surrounding payment.
50
+ uint256 relayBeneficiaryWord;
51
+ assembly ("memory-safe") {
52
+ relayBeneficiaryWord := mload(add(data, 32))
53
+ }
54
+ if (relayBeneficiaryWord > type(uint160).max) {
55
+ return beneficiary;
56
+ }
57
+
58
+ // The range guard above ensures the address cast cannot truncate.
59
+ // forge-lint: disable-next-line(unsafe-typecast)
60
+ address relayBeneficiary = address(uint160(relayBeneficiaryWord));
50
61
  if (relayBeneficiary == address(0)) {
51
62
  return beneficiary;
52
63
  }
@@ -12,6 +12,7 @@ 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 {JBPeerChainAdjustedAccountsLib} from "./JBPeerChainAdjustedAccountsLib.sol";
15
16
  import {JBInboxTreeRoot} from "../structs/JBInboxTreeRoot.sol";
16
17
  import {JBMessageRoot} from "../structs/JBMessageRoot.sol";
17
18
  import {JBSourceContext} from "../structs/JBSourceContext.sol";
@@ -249,15 +250,13 @@ library JBSuckerLib {
249
250
  address dataHook = ruleset.dataHook();
250
251
  if (dataHook == address(0) || dataHook.code.length == 0) return (0, new JBSourceContext[](0));
251
252
 
252
- // Ask the hook for any off-terminal supply and per-context surplus/balance. A non-supporting or broken hook is
253
- // caught and ignored so the baseline snapshot still goes out.
254
- try IJBPeerChainAdjustedAccounts(dataHook).peerChainAdjustedAccountsOf(projectId) returns (
255
- uint256 supply, JBSourceContext[] memory contexts
256
- ) {
257
- return (supply, contexts);
258
- } catch {
259
- return (0, new JBSourceContext[](0));
260
- }
253
+ // Ask the hook for any off-terminal supply and per-context surplus/balance. Non-supporting, broken, or
254
+ // malformed hooks are ignored so the baseline snapshot still goes out.
255
+ (bool hookCallSucceeded, bytes memory hookData) =
256
+ dataHook.staticcall(abi.encodeCall(IJBPeerChainAdjustedAccounts.peerChainAdjustedAccountsOf, (projectId)));
257
+ if (!hookCallSucceeded) return (0, new JBSourceContext[](0));
258
+
259
+ return JBPeerChainAdjustedAccountsLib.decode(hookData);
261
260
  }
262
261
 
263
262
  /// @notice Reads one accounting context's raw surplus and balance into a `JBSourceContext`, performing no price