@bananapus/suckers-v6 0.0.24 → 0.0.25

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.24",
3
+ "version": "0.0.25",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,6 +11,7 @@ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"
11
11
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
12
12
  import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
13
13
 
14
+ import {JBDenominatedAmount} from "./structs/JBDenominatedAmount.sol";
14
15
  import {JBSuckerState} from "./enums/JBSuckerState.sol";
15
16
  import {IJBSucker} from "./interfaces/IJBSucker.sol";
16
17
  import {IJBSuckerDeployer} from "./interfaces/IJBSuckerDeployer.sol";
@@ -189,6 +190,95 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
189
190
  }
190
191
  }
191
192
 
193
+ /// @notice The cumulative balance across all remote peer chains for a project, denominated in a given currency.
194
+ /// @dev Sums `peerChainBalanceOf` from each active sucker. Silently skips suckers that revert.
195
+ /// @param projectId The ID of the project.
196
+ /// @param decimals The decimal precision for the returned value.
197
+ /// @param currency The currency to normalize to.
198
+ /// @return balance The combined peer chain balance.
199
+ function remoteBalanceOf(
200
+ uint256 projectId,
201
+ uint256 decimals,
202
+ uint256 currency
203
+ )
204
+ external
205
+ view
206
+ override
207
+ returns (uint256 balance)
208
+ {
209
+ address[] memory allSuckers = _suckersOf[projectId].keys();
210
+ for (uint256 i; i < allSuckers.length;) {
211
+ // slither-disable-next-line unused-return
212
+ (, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
213
+ if (val == _SUCKER_EXISTS) {
214
+ // slither-disable-next-line calls-loop
215
+ try IJBSucker(allSuckers[i]).peerChainBalanceOf(decimals, currency) returns (
216
+ JBDenominatedAmount memory amt
217
+ ) {
218
+ balance += amt.value;
219
+ } catch {}
220
+ }
221
+ unchecked {
222
+ ++i;
223
+ }
224
+ }
225
+ }
226
+
227
+ /// @notice The cumulative surplus across all remote peer chains for a project, denominated in a given currency.
228
+ /// @dev Sums `peerChainSurplusOf` from each active sucker. Silently skips suckers that revert.
229
+ /// @param projectId The ID of the project.
230
+ /// @param decimals The decimal precision for the returned value.
231
+ /// @param currency The currency to normalize to.
232
+ /// @return surplus The combined peer chain surplus.
233
+ function remoteSurplusOf(
234
+ uint256 projectId,
235
+ uint256 decimals,
236
+ uint256 currency
237
+ )
238
+ external
239
+ view
240
+ override
241
+ returns (uint256 surplus)
242
+ {
243
+ address[] memory allSuckers = _suckersOf[projectId].keys();
244
+ for (uint256 i; i < allSuckers.length;) {
245
+ // slither-disable-next-line unused-return
246
+ (, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
247
+ if (val == _SUCKER_EXISTS) {
248
+ // slither-disable-next-line calls-loop
249
+ try IJBSucker(allSuckers[i]).peerChainSurplusOf(decimals, currency) returns (
250
+ JBDenominatedAmount memory amt
251
+ ) {
252
+ surplus += amt.value;
253
+ } catch {}
254
+ }
255
+ unchecked {
256
+ ++i;
257
+ }
258
+ }
259
+ }
260
+
261
+ /// @notice The cumulative total supply across all remote peer chains for a project.
262
+ /// @dev Sums `peerChainTotalSupply` from each active sucker. Silently skips suckers that revert.
263
+ /// @param projectId The ID of the project.
264
+ /// @return totalSupply The combined peer chain total supply.
265
+ function remoteTotalSupplyOf(uint256 projectId) external view override returns (uint256 totalSupply) {
266
+ address[] memory allSuckers = _suckersOf[projectId].keys();
267
+ for (uint256 i; i < allSuckers.length;) {
268
+ // slither-disable-next-line unused-return
269
+ (, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
270
+ if (val == _SUCKER_EXISTS) {
271
+ // slither-disable-next-line calls-loop
272
+ try IJBSucker(allSuckers[i]).peerChainTotalSupply() returns (uint256 supply) {
273
+ totalSupply += supply;
274
+ } catch {}
275
+ }
276
+ unchecked {
277
+ ++i;
278
+ }
279
+ }
280
+ }
281
+
192
282
  //*********************************************************************//
193
283
  // ------------------------ internal views --------------------------- //
194
284
  //*********************************************************************//
@@ -76,6 +76,42 @@ interface IJBSuckerRegistry {
76
76
  /// @return The addresses of the suckers.
77
77
  function suckersOf(uint256 projectId) external view returns (address[] memory);
78
78
 
79
+ /// @notice The cumulative total supply across all remote peer chains for a project.
80
+ /// @dev Sums `peerChainTotalSupply` from each active sucker. Silently skips suckers that revert.
81
+ /// @param projectId The ID of the project.
82
+ /// @return totalSupply The combined peer chain total supply.
83
+ function remoteTotalSupplyOf(uint256 projectId) external view returns (uint256 totalSupply);
84
+
85
+ /// @notice The cumulative balance across all remote peer chains for a project, denominated in a given currency.
86
+ /// @dev Sums `peerChainBalanceOf` from each active sucker. Silently skips suckers that revert.
87
+ /// @param projectId The ID of the project.
88
+ /// @param decimals The decimal precision for the returned value.
89
+ /// @param currency The currency to normalize to.
90
+ /// @return balance The combined peer chain balance.
91
+ function remoteBalanceOf(
92
+ uint256 projectId,
93
+ uint256 decimals,
94
+ uint256 currency
95
+ )
96
+ external
97
+ view
98
+ returns (uint256 balance);
99
+
100
+ /// @notice The cumulative surplus across all remote peer chains for a project, denominated in a given currency.
101
+ /// @dev Sums `peerChainSurplusOf` from each active sucker. Silently skips suckers that revert.
102
+ /// @param projectId The ID of the project.
103
+ /// @param decimals The decimal precision for the returned value.
104
+ /// @param currency The currency to normalize to.
105
+ /// @return surplus The combined peer chain surplus.
106
+ function remoteSurplusOf(
107
+ uint256 projectId,
108
+ uint256 decimals,
109
+ uint256 currency
110
+ )
111
+ external
112
+ view
113
+ returns (uint256 surplus);
114
+
79
115
  /// @notice The ETH fee (in wei) paid into the fee project on each toRemote() call.
80
116
  /// @return The current fee.
81
117
  function toRemoteFee() external view returns (uint256);
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
5
+
6
+ import {IJBSuckerRegistry} from "../interfaces/IJBSuckerRegistry.sol";
7
+
8
+ /// @notice Library to resolve the relay beneficiary from metadata injected by a relay terminal or sucker.
9
+ /// @dev When a sucker pays a project on behalf of a remote user, the sucker is both payer and beneficiary.
10
+ /// The real user's address is embedded in the payment metadata under the `ID` key. Data hooks and pay hooks
11
+ /// use this library to resolve the real beneficiary so that NFTs, credits, etc. accrue to the correct user.
12
+ library JBRelayBeneficiary {
13
+ /// @notice The metadata ID used to identify the relay beneficiary entry.
14
+ /// @dev Global constant (not per-contract) because the metadata is injected by the sucker but read by
15
+ /// unrelated hooks. Using `keccak256("JB_RELAY_BENEFICIARY")` ensures no collisions with contract-specific IDs.
16
+ bytes4 constant ID = bytes4(keccak256("JB_RELAY_BENEFICIARY"));
17
+
18
+ /// @notice Resolve the effective beneficiary for a payment.
19
+ /// @dev Returns `beneficiary` unchanged if the payer is not a registered sucker or if no relay data is found.
20
+ /// @param payer The address that called `terminal.pay()` (i.e. `context.payer`).
21
+ /// @param beneficiary The beneficiary set in the payment context (i.e. `context.beneficiary`).
22
+ /// @param projectId The project being paid.
23
+ /// @param metadata The payment metadata (`context.payerMetadata` or `context.metadata`).
24
+ /// @param registry The sucker registry used to verify that `payer` is a legitimate sucker.
25
+ /// @return effectiveBeneficiary The resolved beneficiary — the relay address if valid, or the original.
26
+ function resolve(
27
+ address payer,
28
+ address beneficiary,
29
+ uint256 projectId,
30
+ bytes memory metadata,
31
+ IJBSuckerRegistry registry
32
+ )
33
+ internal
34
+ view
35
+ returns (address effectiveBeneficiary)
36
+ {
37
+ // Only trust relay metadata when the payer is a registered sucker for this project.
38
+ if (!registry.isSuckerOf(projectId, payer)) {
39
+ return beneficiary;
40
+ }
41
+
42
+ // Try to find relay beneficiary data in the metadata.
43
+ (bool found, bytes memory data) = JBMetadataResolver.getDataFor({id: ID, metadata: metadata});
44
+ if (!found || data.length < 32) {
45
+ return beneficiary;
46
+ }
47
+
48
+ // Decode the relay beneficiary address.
49
+ address relayBeneficiary = abi.decode(data, (address));
50
+ if (relayBeneficiary == address(0)) {
51
+ return beneficiary;
52
+ }
53
+
54
+ return relayBeneficiary;
55
+ }
56
+ }
@@ -12,6 +12,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
12
12
  import "../../src/JBSucker.sol";
13
13
  import {IJBSuckerDeployer} from "../../src/interfaces/IJBSuckerDeployer.sol";
14
14
  import {IJBSuckerRegistry} from "../../src/interfaces/IJBSuckerRegistry.sol";
15
+ import {JBDenominatedAmount} from "../../src/structs/JBDenominatedAmount.sol";
15
16
 
16
17
  // forge-lint: disable-next-line(unaliased-plain-import)
17
18
  import "../../src/deployers/JBOptimismSuckerDeployer.sol";
@@ -586,6 +587,150 @@ contract DeployerTests is Test, TestBaseWorkflow, IERC721Receiver {
586
587
  assertTrue(registry.isSuckerOf(projectId, address(sucker2)));
587
588
  }
588
589
 
590
+ // ------------------------------------------------------------------
591
+ // Remote aggregate view tests
592
+ // ------------------------------------------------------------------
593
+
594
+ /// @notice remoteTotalSupplyOf sums peerChainTotalSupply across active suckers.
595
+ function testRemoteTotalSupplyOf(ICCIPRouter _ccipRouter) public {
596
+ vm.assume(uint160(address(_ccipRouter)) > 100);
597
+ _assumeNotDeployed(address(_ccipRouter));
598
+ vm.etch(address(_ccipRouter), "0x1");
599
+
600
+ _allowMapping(projectId, address(registry));
601
+
602
+ // Deploy two suckers to different chains.
603
+ IJBSuckerDeployer deployer1 = _addToRegistry(_setupCCIPDeployer(10, 1, _ccipRouter));
604
+ IJBSucker sucker1 = _deployThroughRegistry(deployer1, projectId, bytes32("salt1"));
605
+
606
+ IJBSuckerDeployer deployer2 = _addToRegistry(_setupCCIPDeployer(42_161, 2, _ccipRouter));
607
+ IJBSucker sucker2 = _deployThroughRegistry(deployer2, projectId, bytes32("salt2"));
608
+
609
+ // Mock peerChainTotalSupply on each sucker.
610
+ vm.mockCall(address(sucker1), abi.encodeCall(IJBSucker.peerChainTotalSupply, ()), abi.encode(100e18));
611
+ vm.mockCall(address(sucker2), abi.encodeCall(IJBSucker.peerChainTotalSupply, ()), abi.encode(250e18));
612
+
613
+ assertEq(registry.remoteTotalSupplyOf(projectId), 350e18);
614
+ }
615
+
616
+ /// @notice remoteBalanceOf sums peerChainBalanceOf across active suckers.
617
+ function testRemoteBalanceOf(ICCIPRouter _ccipRouter) public {
618
+ vm.assume(uint160(address(_ccipRouter)) > 100);
619
+ _assumeNotDeployed(address(_ccipRouter));
620
+ vm.etch(address(_ccipRouter), "0x1");
621
+
622
+ _allowMapping(projectId, address(registry));
623
+
624
+ IJBSuckerDeployer deployer1 = _addToRegistry(_setupCCIPDeployer(10, 1, _ccipRouter));
625
+ IJBSucker sucker1 = _deployThroughRegistry(deployer1, projectId, bytes32("salt1"));
626
+
627
+ IJBSuckerDeployer deployer2 = _addToRegistry(_setupCCIPDeployer(42_161, 2, _ccipRouter));
628
+ IJBSucker sucker2 = _deployThroughRegistry(deployer2, projectId, bytes32("salt2"));
629
+
630
+ uint256 ethCurrency = uint256(uint160(JBConstants.NATIVE_TOKEN));
631
+
632
+ // Mock peerChainBalanceOf on each sucker.
633
+ vm.mockCall(
634
+ address(sucker1),
635
+ abi.encodeCall(IJBSucker.peerChainBalanceOf, (18, ethCurrency)),
636
+ abi.encode(JBDenominatedAmount({value: 5e18, currency: uint32(ethCurrency), decimals: 18}))
637
+ );
638
+ vm.mockCall(
639
+ address(sucker2),
640
+ abi.encodeCall(IJBSucker.peerChainBalanceOf, (18, ethCurrency)),
641
+ abi.encode(JBDenominatedAmount({value: 3e18, currency: uint32(ethCurrency), decimals: 18}))
642
+ );
643
+
644
+ assertEq(registry.remoteBalanceOf(projectId, 18, ethCurrency), 8e18);
645
+ }
646
+
647
+ /// @notice remoteSurplusOf sums peerChainSurplusOf across active suckers.
648
+ function testRemoteSurplusOf(ICCIPRouter _ccipRouter) public {
649
+ vm.assume(uint160(address(_ccipRouter)) > 100);
650
+ _assumeNotDeployed(address(_ccipRouter));
651
+ vm.etch(address(_ccipRouter), "0x1");
652
+
653
+ _allowMapping(projectId, address(registry));
654
+
655
+ IJBSuckerDeployer deployer1 = _addToRegistry(_setupCCIPDeployer(10, 1, _ccipRouter));
656
+ IJBSucker sucker1 = _deployThroughRegistry(deployer1, projectId, bytes32("salt1"));
657
+
658
+ IJBSuckerDeployer deployer2 = _addToRegistry(_setupCCIPDeployer(42_161, 2, _ccipRouter));
659
+ IJBSucker sucker2 = _deployThroughRegistry(deployer2, projectId, bytes32("salt2"));
660
+
661
+ uint256 ethCurrency = uint256(uint160(JBConstants.NATIVE_TOKEN));
662
+
663
+ // Mock peerChainSurplusOf on each sucker.
664
+ vm.mockCall(
665
+ address(sucker1),
666
+ abi.encodeCall(IJBSucker.peerChainSurplusOf, (18, ethCurrency)),
667
+ abi.encode(JBDenominatedAmount({value: 10e18, currency: uint32(ethCurrency), decimals: 18}))
668
+ );
669
+ vm.mockCall(
670
+ address(sucker2),
671
+ abi.encodeCall(IJBSucker.peerChainSurplusOf, (18, ethCurrency)),
672
+ abi.encode(JBDenominatedAmount({value: 7e18, currency: uint32(ethCurrency), decimals: 18}))
673
+ );
674
+
675
+ assertEq(registry.remoteSurplusOf(projectId, 18, ethCurrency), 17e18);
676
+ }
677
+
678
+ /// @notice Remote views return 0 for a project with no suckers.
679
+ function testRemoteViewsZeroWithNoSuckers() public view {
680
+ assertEq(registry.remoteTotalSupplyOf(projectId), 0);
681
+ assertEq(registry.remoteBalanceOf(projectId, 18, uint256(uint160(JBConstants.NATIVE_TOKEN))), 0);
682
+ assertEq(registry.remoteSurplusOf(projectId, 18, uint256(uint160(JBConstants.NATIVE_TOKEN))), 0);
683
+ }
684
+
685
+ /// @notice Remote views skip deprecated suckers.
686
+ function testRemoteViewsSkipDeprecatedSuckers(ICCIPRouter _ccipRouter) public {
687
+ vm.assume(uint160(address(_ccipRouter)) > 100);
688
+ _assumeNotDeployed(address(_ccipRouter));
689
+ vm.etch(address(_ccipRouter), "0x1");
690
+
691
+ _allowMapping(projectId, address(registry));
692
+
693
+ IJBSuckerDeployer deployer1 = _addToRegistry(_setupCCIPDeployer(10, 1, _ccipRouter));
694
+ IJBSucker sucker1 = _deployThroughRegistry(deployer1, projectId, bytes32("salt1"));
695
+
696
+ // Mock peerChainTotalSupply.
697
+ vm.mockCall(address(sucker1), abi.encodeCall(IJBSucker.peerChainTotalSupply, ()), abi.encode(100e18));
698
+
699
+ // Before deprecation: 100e18.
700
+ assertEq(registry.remoteTotalSupplyOf(projectId), 100e18);
701
+
702
+ // Deprecate and remove.
703
+ JBSucker(payable(address(sucker1))).setDeprecation(uint40(block.timestamp + 14 days));
704
+ vm.warp(block.timestamp + 14 days);
705
+ registry.removeDeprecatedSucker(projectId, address(sucker1));
706
+
707
+ // After deprecation: 0.
708
+ assertEq(registry.remoteTotalSupplyOf(projectId), 0);
709
+ }
710
+
711
+ /// @notice Remote views silently skip suckers that revert.
712
+ function testRemoteViewsSkipRevertingSuckers(ICCIPRouter _ccipRouter) public {
713
+ vm.assume(uint160(address(_ccipRouter)) > 100);
714
+ _assumeNotDeployed(address(_ccipRouter));
715
+ vm.etch(address(_ccipRouter), "0x1");
716
+
717
+ _allowMapping(projectId, address(registry));
718
+
719
+ // Deploy two suckers.
720
+ IJBSuckerDeployer deployer1 = _addToRegistry(_setupCCIPDeployer(10, 1, _ccipRouter));
721
+ IJBSucker sucker1 = _deployThroughRegistry(deployer1, projectId, bytes32("salt1"));
722
+
723
+ IJBSuckerDeployer deployer2 = _addToRegistry(_setupCCIPDeployer(42_161, 2, _ccipRouter));
724
+ IJBSucker sucker2 = _deployThroughRegistry(deployer2, projectId, bytes32("salt2"));
725
+
726
+ // sucker1 returns 100e18, sucker2 reverts.
727
+ vm.mockCall(address(sucker1), abi.encodeCall(IJBSucker.peerChainTotalSupply, ()), abi.encode(100e18));
728
+ vm.mockCallRevert(address(sucker2), abi.encodeCall(IJBSucker.peerChainTotalSupply, ()), "boom");
729
+
730
+ // Should still return 100e18 (sucker2's revert is silently skipped).
731
+ assertEq(registry.remoteTotalSupplyOf(projectId), 100e18);
732
+ }
733
+
589
734
  /// @notice This function is called when we create a JB project.
590
735
  function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
591
736
  return IERC721Receiver.onERC721Received.selector;
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {JBRelayBeneficiary} from "../../src/libraries/JBRelayBeneficiary.sol";
7
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
8
+ import {IJBSuckerRegistry} from "../../src/interfaces/IJBSuckerRegistry.sol";
9
+
10
+ /// @notice Thin wrapper that exposes the internal library function for testing.
11
+ contract RelayBeneficiaryHarness {
12
+ function resolve(
13
+ address payer,
14
+ address beneficiary,
15
+ uint256 projectId,
16
+ bytes memory metadata,
17
+ IJBSuckerRegistry registry
18
+ )
19
+ external
20
+ view
21
+ returns (address)
22
+ {
23
+ return JBRelayBeneficiary.resolve({
24
+ payer: payer, beneficiary: beneficiary, projectId: projectId, metadata: metadata, registry: registry
25
+ });
26
+ }
27
+ }
28
+
29
+ contract RelayBeneficiaryTest is Test {
30
+ RelayBeneficiaryHarness harness;
31
+ IJBSuckerRegistry registry;
32
+
33
+ address payer = address(0xAAA);
34
+ address beneficiary = address(0xBBB);
35
+ address relayAddress = address(0xCCC);
36
+ uint256 projectId = 1;
37
+
38
+ function setUp() public {
39
+ harness = new RelayBeneficiaryHarness();
40
+ registry = IJBSuckerRegistry(makeAddr("registry"));
41
+ }
42
+
43
+ /// @notice Helper: mock isSuckerOf to return the given value.
44
+ function _mockIsSucker(bool isSucker) internal {
45
+ vm.mockCall(
46
+ address(registry), abi.encodeCall(IJBSuckerRegistry.isSuckerOf, (projectId, payer)), abi.encode(isSucker)
47
+ );
48
+ }
49
+
50
+ /// @notice Helper: build metadata containing the relay beneficiary address.
51
+ function _buildMetadata(address relay) internal pure returns (bytes memory) {
52
+ return JBMetadataResolver.addToMetadata({
53
+ originalMetadata: bytes(""), idToAdd: JBRelayBeneficiary.ID, dataToAdd: abi.encode(relay)
54
+ });
55
+ }
56
+
57
+ // ------------------------------------------------------------------
58
+ // Tests
59
+ // ------------------------------------------------------------------
60
+
61
+ function test_resolve_returnsOriginalIfNotSucker() public {
62
+ _mockIsSucker(false);
63
+
64
+ bytes memory metadata = _buildMetadata(relayAddress);
65
+
66
+ address result = harness.resolve(payer, beneficiary, projectId, metadata, registry);
67
+ assertEq(result, beneficiary, "Should return original beneficiary when payer is not a sucker");
68
+ }
69
+
70
+ function test_resolve_returnsOriginalIfNoMetadata() public {
71
+ _mockIsSucker(true);
72
+
73
+ address result = harness.resolve(payer, beneficiary, projectId, bytes(""), registry);
74
+ assertEq(result, beneficiary, "Should return original beneficiary when metadata is empty");
75
+ }
76
+
77
+ function test_resolve_returnsOriginalIfMetadataNotFound() public {
78
+ _mockIsSucker(true);
79
+
80
+ // Build metadata with a different ID so the relay key is absent.
81
+ bytes4 otherId = bytes4(keccak256("SOME_OTHER_KEY"));
82
+ bytes memory metadata = JBMetadataResolver.addToMetadata({
83
+ originalMetadata: bytes(""), idToAdd: otherId, dataToAdd: abi.encode(relayAddress)
84
+ });
85
+
86
+ address result = harness.resolve(payer, beneficiary, projectId, metadata, registry);
87
+ assertEq(result, beneficiary, "Should return original beneficiary when relay key is not in metadata");
88
+ }
89
+
90
+ function test_resolve_returnsOriginalIfRelayBeneficiaryZero() public {
91
+ _mockIsSucker(true);
92
+
93
+ bytes memory metadata = _buildMetadata(address(0));
94
+
95
+ address result = harness.resolve(payer, beneficiary, projectId, metadata, registry);
96
+ assertEq(result, beneficiary, "Should return original beneficiary when relay address is zero");
97
+ }
98
+
99
+ function test_resolve_returnsRelayBeneficiary() public {
100
+ _mockIsSucker(true);
101
+
102
+ bytes memory metadata = _buildMetadata(relayAddress);
103
+
104
+ address result = harness.resolve(payer, beneficiary, projectId, metadata, registry);
105
+ assertEq(result, relayAddress, "Should return relay beneficiary from metadata");
106
+ }
107
+
108
+ function test_resolve_returnsOriginalIfDataTooShort() public {
109
+ _mockIsSucker(true);
110
+
111
+ // Manually craft metadata where the relay key exists but the data segment is shorter than 32 bytes.
112
+ //
113
+ // JBMetadataResolver format:
114
+ // [0..31] : 32-byte reserved header
115
+ // [32..36] : bytes4 id + uint8 offset (lookup table entry)
116
+ // [37] : 0x00 terminator (no more entries)
117
+ // [offset*32 ..] : data
118
+ //
119
+ // We place the ID at byte 32, with offset = 2 (data starts at byte 64).
120
+ // Then we only write 16 bytes of data instead of 32, making data.length < 32.
121
+ bytes memory metadata = new bytes(64 + 16); // 80 bytes total
122
+
123
+ // Write the relay ID at position 32 (first lookup entry).
124
+ bytes4 id = JBRelayBeneficiary.ID;
125
+ metadata[32] = id[0];
126
+ metadata[33] = id[1];
127
+ metadata[34] = id[2];
128
+ metadata[35] = id[3];
129
+ // Offset = 2 means data starts at byte 64.
130
+ metadata[36] = bytes1(uint8(2));
131
+ // Terminator at position 37 (next entry ID byte is 0).
132
+
133
+ // Write 16 bytes of non-zero data starting at byte 64 (less than 32 bytes).
134
+ for (uint256 i = 64; i < 80; i++) {
135
+ metadata[i] = bytes1(uint8(0xFF));
136
+ }
137
+
138
+ address result = harness.resolve(payer, beneficiary, projectId, metadata, registry);
139
+ assertEq(result, beneficiary, "Should return original beneficiary when relay data is too short");
140
+ }
141
+ }