@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
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -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
|
+
}
|
package/test/unit/deployer.t.sol
CHANGED
|
@@ -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
|
+
}
|