@bananapus/721-hook-v6 0.0.13 → 0.0.15
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/ARCHITECTURE.md +7 -2
- package/RISKS.md +6 -6
- package/SKILLS.md +5 -4
- package/STYLE_GUIDE.md +131 -43
- package/foundry.toml +3 -3
- package/package.json +5 -5
- package/remappings.txt +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +1 -1
- package/src/JB721TiersHook.sol +23 -0
- package/src/JB721TiersHookProjectDeployer.sol +3 -3
- package/src/libraries/JB721TiersHookLib.sol +44 -0
- package/src/structs/JBPayDataHookRulesetMetadata.sol +3 -0
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +2 -0
- package/test/Fork.t.sol +14 -18
- package/test/fork/ERC20TierSplitFork.t.sol +539 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -2
- package/test/unit/pay_CrossCurrency_Unit.t.sol +498 -0
- package/test/unit/tierSplitRouting_Unit.t.sol +257 -0
- /package/test/regression/{L35_CacheTierLookup.t.sol → CacheTierLookup.t.sol} +0 -0
- /package/test/regression/{L34_ReserveBeneficiaryOverwrite.t.sol → ReserveBeneficiaryOverwrite.t.sol} +0 -0
- /package/test/regression/{L36_SplitNoBeneficiary.t.sol → SplitNoBeneficiary.t.sol} +0 -0
- /package/test/unit/{M6_TierSupplyCheck.t.sol → TierSupplyReserveCheck.t.sol} +0 -0
|
@@ -6,7 +6,18 @@ import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.so
|
|
|
6
6
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
7
7
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
8
8
|
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
9
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
10
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
9
11
|
import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
|
|
12
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
13
|
+
|
|
14
|
+
contract MockERC20 is ERC20 {
|
|
15
|
+
constructor() ERC20("Mock Token", "MOCK") {}
|
|
16
|
+
|
|
17
|
+
function mint(address to, uint256 amount) external {
|
|
18
|
+
_mint(to, amount);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
contract Test_TierSplitRouting is UnitTestSetup {
|
|
12
23
|
using stdStorage for StdStorage;
|
|
@@ -486,4 +497,250 @@ contract Test_TierSplitRouting is UnitTestSetup {
|
|
|
486
497
|
// Flag set — weight should be full despite 100% split consuming entire payment.
|
|
487
498
|
assertEq(weight, 10e18);
|
|
488
499
|
}
|
|
500
|
+
|
|
501
|
+
// ──────────────────────────────────────────────
|
|
502
|
+
// ERC20 Tests: afterPayRecordedWith with ERC20 tokens
|
|
503
|
+
// ──────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
/// @notice Helper: set up an ERC20 tier split test. Returns (hook, tierIds, mockToken).
|
|
506
|
+
function _setupERC20TierSplit()
|
|
507
|
+
internal
|
|
508
|
+
returns (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token)
|
|
509
|
+
{
|
|
510
|
+
// Deploy mock ERC20.
|
|
511
|
+
token = new MockERC20();
|
|
512
|
+
|
|
513
|
+
// Initialize hook with ERC20 currency (0 default tiers).
|
|
514
|
+
testHook = _initHookDefaultTiers(0, false, uint32(uint160(address(token))), 18, address(0));
|
|
515
|
+
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
516
|
+
|
|
517
|
+
// Add a tier with 50% split, priced at 100 tokens.
|
|
518
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
519
|
+
tierConfigs[0] = _tierConfigWithSplit(100, 500_000_000); // 50%
|
|
520
|
+
vm.prank(address(testHook));
|
|
521
|
+
tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
522
|
+
|
|
523
|
+
// Mock directory checks.
|
|
524
|
+
mockAndExpect(
|
|
525
|
+
address(mockJBDirectory),
|
|
526
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
527
|
+
abi.encode(true)
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// @notice Helper: build afterPayRecordedWith context for ERC20 payments.
|
|
532
|
+
function _buildERC20PayContext(
|
|
533
|
+
JB721TiersHook testHook,
|
|
534
|
+
uint256[] memory tierIds,
|
|
535
|
+
address token,
|
|
536
|
+
uint256 payAmount,
|
|
537
|
+
uint256 forwardedAmount
|
|
538
|
+
)
|
|
539
|
+
internal
|
|
540
|
+
view
|
|
541
|
+
returns (JBAfterPayRecordedContext memory)
|
|
542
|
+
{
|
|
543
|
+
uint16[] memory mintIds = new uint16[](1);
|
|
544
|
+
mintIds[0] = uint16(tierIds[0]);
|
|
545
|
+
// Use METADATA_ID_TARGET (the original hook address) for metadata resolution.
|
|
546
|
+
bytes memory payerMetadata = _buildPayerMetadata(testHook.METADATA_ID_TARGET(), mintIds);
|
|
547
|
+
|
|
548
|
+
uint16[] memory splitTierIds = new uint16[](1);
|
|
549
|
+
splitTierIds[0] = uint16(tierIds[0]);
|
|
550
|
+
uint256[] memory splitAmounts = new uint256[](1);
|
|
551
|
+
splitAmounts[0] = forwardedAmount;
|
|
552
|
+
|
|
553
|
+
return JBAfterPayRecordedContext({
|
|
554
|
+
payer: beneficiary,
|
|
555
|
+
projectId: projectId,
|
|
556
|
+
rulesetId: 0,
|
|
557
|
+
amount: JBTokenAmount({token: token, value: payAmount, decimals: 18, currency: uint32(uint160(token))}),
|
|
558
|
+
forwardedAmount: JBTokenAmount({
|
|
559
|
+
token: token, value: forwardedAmount, decimals: 18, currency: uint32(uint160(token))
|
|
560
|
+
}),
|
|
561
|
+
weight: 10e18,
|
|
562
|
+
newlyIssuedTokenCount: 0,
|
|
563
|
+
beneficiary: beneficiary,
|
|
564
|
+
hookMetadata: abi.encode(splitTierIds, splitAmounts),
|
|
565
|
+
payerMetadata: payerMetadata
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function test_afterPayRecorded_erc20_distributesToBeneficiary() public {
|
|
570
|
+
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
571
|
+
|
|
572
|
+
// Mock splits: alice gets 100%.
|
|
573
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
574
|
+
splits[0] = JBSplit({
|
|
575
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
576
|
+
projectId: 0,
|
|
577
|
+
beneficiary: payable(alice),
|
|
578
|
+
preferAddToBalance: false,
|
|
579
|
+
lockedUntil: 0,
|
|
580
|
+
hook: IJBSplitHook(address(0))
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
584
|
+
mockAndExpect(
|
|
585
|
+
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Give terminal the ERC20 tokens and approve the hook.
|
|
589
|
+
token.mint(mockTerminalAddress, 100);
|
|
590
|
+
vm.prank(mockTerminalAddress);
|
|
591
|
+
token.approve(address(testHook), 50);
|
|
592
|
+
|
|
593
|
+
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
594
|
+
|
|
595
|
+
vm.prank(mockTerminalAddress);
|
|
596
|
+
testHook.afterPayRecordedWith(payContext);
|
|
597
|
+
|
|
598
|
+
// Alice should have received 50 tokens.
|
|
599
|
+
assertEq(token.balanceOf(alice), 50);
|
|
600
|
+
// NFT should have been minted.
|
|
601
|
+
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function test_afterPayRecorded_erc20_splitToProject_addToBalance() public {
|
|
605
|
+
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
606
|
+
|
|
607
|
+
// Target project for split.
|
|
608
|
+
uint256 targetProjectId = 99;
|
|
609
|
+
address targetTerminal = makeAddr("targetTerminal");
|
|
610
|
+
vm.etch(targetTerminal, new bytes(0x69));
|
|
611
|
+
|
|
612
|
+
// Mock splits: 100% to target project with preferAddToBalance.
|
|
613
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
614
|
+
splits[0] = JBSplit({
|
|
615
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
616
|
+
projectId: uint56(targetProjectId),
|
|
617
|
+
beneficiary: payable(address(0)),
|
|
618
|
+
preferAddToBalance: true,
|
|
619
|
+
lockedUntil: 0,
|
|
620
|
+
hook: IJBSplitHook(address(0))
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
624
|
+
mockAndExpect(
|
|
625
|
+
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Mock directory: target project's primary terminal.
|
|
629
|
+
mockAndExpect(
|
|
630
|
+
address(mockJBDirectory),
|
|
631
|
+
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, address(token)),
|
|
632
|
+
abi.encode(targetTerminal)
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
// Mock the addToBalanceOf call on the target terminal.
|
|
636
|
+
vm.mockCall(targetTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
637
|
+
|
|
638
|
+
// Give terminal the ERC20 tokens and approve the hook.
|
|
639
|
+
token.mint(mockTerminalAddress, 100);
|
|
640
|
+
vm.prank(mockTerminalAddress);
|
|
641
|
+
token.approve(address(testHook), 50);
|
|
642
|
+
|
|
643
|
+
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
644
|
+
|
|
645
|
+
vm.prank(mockTerminalAddress);
|
|
646
|
+
testHook.afterPayRecordedWith(payContext);
|
|
647
|
+
|
|
648
|
+
// Hook approved the target terminal (library calls forceApprove before addToBalanceOf).
|
|
649
|
+
// Mock terminal doesn't pull, so hook still holds the tokens. Verify approval was set.
|
|
650
|
+
assertGe(token.allowance(address(testHook), targetTerminal), 50);
|
|
651
|
+
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function test_afterPayRecorded_erc20_splitToProject_pay() public {
|
|
655
|
+
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
656
|
+
|
|
657
|
+
uint256 targetProjectId = 99;
|
|
658
|
+
address targetTerminal = makeAddr("targetTerminal");
|
|
659
|
+
vm.etch(targetTerminal, new bytes(0x69));
|
|
660
|
+
|
|
661
|
+
// Mock splits: 100% to target project with preferAddToBalance = false (pay).
|
|
662
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
663
|
+
splits[0] = JBSplit({
|
|
664
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
665
|
+
projectId: uint56(targetProjectId),
|
|
666
|
+
beneficiary: payable(alice),
|
|
667
|
+
preferAddToBalance: false,
|
|
668
|
+
lockedUntil: 0,
|
|
669
|
+
hook: IJBSplitHook(address(0))
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
673
|
+
mockAndExpect(
|
|
674
|
+
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
// Mock directory: target project's primary terminal.
|
|
678
|
+
mockAndExpect(
|
|
679
|
+
address(mockJBDirectory),
|
|
680
|
+
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, address(token)),
|
|
681
|
+
abi.encode(targetTerminal)
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// Mock the pay call on the target terminal.
|
|
685
|
+
vm.mockCall(targetTerminal, abi.encodeWithSelector(IJBTerminal.pay.selector), abi.encode(0));
|
|
686
|
+
|
|
687
|
+
// Give terminal the ERC20 tokens and approve the hook.
|
|
688
|
+
token.mint(mockTerminalAddress, 100);
|
|
689
|
+
vm.prank(mockTerminalAddress);
|
|
690
|
+
token.approve(address(testHook), 50);
|
|
691
|
+
|
|
692
|
+
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
693
|
+
|
|
694
|
+
vm.prank(mockTerminalAddress);
|
|
695
|
+
testHook.afterPayRecordedWith(payContext);
|
|
696
|
+
|
|
697
|
+
// Hook approved the target terminal (library calls forceApprove before pay).
|
|
698
|
+
assertGe(token.allowance(address(testHook), targetTerminal), 50);
|
|
699
|
+
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function test_afterPayRecorded_erc20_noBeneficiary_routesToProjectBalance() public {
|
|
703
|
+
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
704
|
+
|
|
705
|
+
// Mock splits: no beneficiary and no projectId — leftover goes to project balance.
|
|
706
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
707
|
+
splits[0] = JBSplit({
|
|
708
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
709
|
+
projectId: 0,
|
|
710
|
+
beneficiary: payable(address(0)),
|
|
711
|
+
preferAddToBalance: false,
|
|
712
|
+
lockedUntil: 0,
|
|
713
|
+
hook: IJBSplitHook(address(0))
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
717
|
+
mockAndExpect(
|
|
718
|
+
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Mock the project's primary terminal for the leftover addToBalance.
|
|
722
|
+
address projectTerminal = makeAddr("projectTerminal");
|
|
723
|
+
vm.etch(projectTerminal, new bytes(0x69));
|
|
724
|
+
mockAndExpect(
|
|
725
|
+
address(mockJBDirectory),
|
|
726
|
+
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, address(token)),
|
|
727
|
+
abi.encode(projectTerminal)
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
vm.mockCall(projectTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
731
|
+
|
|
732
|
+
// Give terminal the ERC20 tokens and approve the hook.
|
|
733
|
+
token.mint(mockTerminalAddress, 100);
|
|
734
|
+
vm.prank(mockTerminalAddress);
|
|
735
|
+
token.approve(address(testHook), 50);
|
|
736
|
+
|
|
737
|
+
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
738
|
+
|
|
739
|
+
vm.prank(mockTerminalAddress);
|
|
740
|
+
testHook.afterPayRecordedWith(payContext);
|
|
741
|
+
|
|
742
|
+
// Hook approved the project terminal (library calls forceApprove before addToBalanceOf).
|
|
743
|
+
assertGe(token.allowance(address(testHook), projectTerminal), 50);
|
|
744
|
+
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
745
|
+
}
|
|
489
746
|
}
|
|
File without changes
|
/package/test/regression/{L34_ReserveBeneficiaryOverwrite.t.sol → ReserveBeneficiaryOverwrite.t.sol}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|