@bananapus/721-hook-v6 0.0.47 → 0.0.50
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/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,15 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
|
|
|
12
12
|
- `JB721TiersHookProjectDeployer`
|
|
13
13
|
- `JB721TiersHookLib`
|
|
14
14
|
|
|
15
|
+
## 0.0.50 — Bump nana-core-v6 to 0.0.53
|
|
16
|
+
|
|
17
|
+
`@bananapus/core-v6@0.0.53` ([nana-core-v6 PR #145](https://github.com/Bananapus/nana-core-v6/pull/145)) drops the `via_ir` requirement on `JBCashOutHookSpecsLib`, which lets this package consume the cross-project cashout work (`payAfterCashOutTokensOf` / `addToBalanceAfterCashOutTokensOf`) without needing `via_ir = true` in its own foundry profile. `JB721TiersHookStore.tiersOf` is not via-ir-tolerant under solc 0.8.28 (its category loop trips the Yul stack ceiling), so this dep release is what makes the bump possible at all.
|
|
18
|
+
|
|
19
|
+
- `JBPayDataHookRulesetMetadata` mirrors the new core `pauseCrossProjectFeeFreeInflows` field (bit 80 in the packed metadata word). Forwarded into the canonical `JBRulesetMetadata` at the three call sites in `JB721TiersHookProjectDeployer` (`_launchProjectFor`, `_launchRulesetsFor`, `_queueRulesetsFor`).
|
|
20
|
+
- All `JBRulesetMetadata` test literals patched to include `pauseCrossProjectFeeFreeInflows: false`.
|
|
21
|
+
|
|
22
|
+
`package.json`: version `0.0.49 → 0.0.50`, core dep `^0.0.48 → ^0.0.53`.
|
|
23
|
+
|
|
15
24
|
## Summary
|
|
16
25
|
|
|
17
26
|
- v6 adds tier-level split routing. `JB721TierConfig` and the surrounding minting logic now support `splitPercent` and `splits`.
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@bananapus/address-registry-v6": "0.0.25",
|
|
32
|
-
"@bananapus/core-v6": "0.0.
|
|
31
|
+
"@bananapus/address-registry-v6": "^0.0.25",
|
|
32
|
+
"@bananapus/core-v6": "^0.0.53",
|
|
33
33
|
"@bananapus/ownable-v6": "^0.0.24",
|
|
34
|
-
"@bananapus/permission-ids-v6": "0.0.22",
|
|
34
|
+
"@bananapus/permission-ids-v6": "^0.0.22",
|
|
35
35
|
"@openzeppelin/contracts": "5.6.1",
|
|
36
36
|
"@prb/math": "4.1.1",
|
|
37
37
|
"solady": "0.1.26"
|
|
@@ -277,6 +277,7 @@ contract JB721TiersHookProjectDeployer is
|
|
|
277
277
|
ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts,
|
|
278
278
|
holdFees: payDataRulesetConfig.metadata.holdFees,
|
|
279
279
|
scopeCashOutsToLocalBalances: payDataRulesetConfig.metadata.scopeCashOutsToLocalBalances,
|
|
280
|
+
pauseCrossProjectFeeFreeInflows: payDataRulesetConfig.metadata.pauseCrossProjectFeeFreeInflows,
|
|
280
281
|
useDataHookForPay: true,
|
|
281
282
|
useDataHookForCashOut: payDataRulesetConfig.metadata.useDataHookForCashOut,
|
|
282
283
|
dataHook: address(dataHook),
|
|
@@ -351,6 +352,7 @@ contract JB721TiersHookProjectDeployer is
|
|
|
351
352
|
ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts,
|
|
352
353
|
holdFees: payDataRulesetConfig.metadata.holdFees,
|
|
353
354
|
scopeCashOutsToLocalBalances: payDataRulesetConfig.metadata.scopeCashOutsToLocalBalances,
|
|
355
|
+
pauseCrossProjectFeeFreeInflows: payDataRulesetConfig.metadata.pauseCrossProjectFeeFreeInflows,
|
|
354
356
|
useDataHookForPay: true,
|
|
355
357
|
useDataHookForCashOut: payDataRulesetConfig.metadata.useDataHookForCashOut,
|
|
356
358
|
dataHook: address(dataHook),
|
|
@@ -426,6 +428,7 @@ contract JB721TiersHookProjectDeployer is
|
|
|
426
428
|
ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts,
|
|
427
429
|
holdFees: payDataRulesetConfig.metadata.holdFees,
|
|
428
430
|
scopeCashOutsToLocalBalances: payDataRulesetConfig.metadata.scopeCashOutsToLocalBalances,
|
|
431
|
+
pauseCrossProjectFeeFreeInflows: payDataRulesetConfig.metadata.pauseCrossProjectFeeFreeInflows,
|
|
429
432
|
useDataHookForPay: true,
|
|
430
433
|
useDataHookForCashOut: payDataRulesetConfig.metadata.useDataHookForCashOut,
|
|
431
434
|
dataHook: address(dataHook),
|
|
@@ -691,12 +691,19 @@ library JB721TiersHookLib {
|
|
|
691
691
|
//*********************************************************************//
|
|
692
692
|
|
|
693
693
|
/// @notice Distributes funds for a single tier's split group.
|
|
694
|
-
/// @dev
|
|
695
|
-
///
|
|
696
|
-
/// hook
|
|
697
|
-
///
|
|
698
|
-
///
|
|
699
|
-
///
|
|
694
|
+
/// @dev `_sendPayoutToSplit` returns the actual amount that left this contract — `payoutAmount` on full
|
|
695
|
+
/// success, `0` when the split fully fails (revert or missing recipient), or a partial value when an ERC-20
|
|
696
|
+
/// split hook pulled some but not all of its allowance. The unsent portion accumulates into `amount` and is
|
|
697
|
+
/// routed to the project's primary terminal via `addToBalanceOf` after the loop. If that fallback also
|
|
698
|
+
/// reverts and the token is native ETH, the unsent ETH is stuck in this contract with no recovery path.
|
|
699
|
+
/// For ERC-20 the approval is reset on `addToBalanceOf` failure so the terminal cannot pull later.
|
|
700
|
+
/// @param directory The directory used to resolve the primary terminal for the leftover fallback.
|
|
701
|
+
/// @param splitsContract The splits contract used to look up the tier's split group.
|
|
702
|
+
/// @param projectId The project ID whose primary terminal receives the leftover.
|
|
703
|
+
/// @param token The token being distributed. Use `JBConstants.NATIVE_TOKEN` for ETH.
|
|
704
|
+
/// @param groupId The split group ID identifying the tier whose splits to read.
|
|
705
|
+
/// @param amount The total amount available to distribute across the tier's splits.
|
|
706
|
+
/// @param decimals The decimals of `token`, forwarded into each split hook context.
|
|
700
707
|
function _distributeSingleSplit(
|
|
701
708
|
IJBDirectory directory,
|
|
702
709
|
IJBSplits splitsContract,
|
|
@@ -722,23 +729,22 @@ library JB721TiersHookLib {
|
|
|
722
729
|
unchecked {
|
|
723
730
|
leftoverAmount -= payoutAmount;
|
|
724
731
|
}
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
}
|
|
732
|
+
// `_sendPayoutToSplit` returns the actual amount that left this contract (0 on full failure,
|
|
733
|
+
// a partial value when a split hook pulled some but not all of its allowance, or `payoutAmount`
|
|
734
|
+
// on full success). Add the unsent portion to `amount` so it routes to the project's balance
|
|
735
|
+
// after the loop. The subtraction is safe — the function invariantly returns at most
|
|
736
|
+
// `payoutAmount` (allowance bounded for ERC-20, value bounded for ETH, else all-or-nothing).
|
|
737
|
+
unchecked {
|
|
738
|
+
amount += payoutAmount
|
|
739
|
+
- _sendPayoutToSplit({
|
|
740
|
+
directory: directory,
|
|
741
|
+
split: tierSplits[j],
|
|
742
|
+
token: token,
|
|
743
|
+
amount: payoutAmount,
|
|
744
|
+
projectId: projectId,
|
|
745
|
+
groupId: groupId,
|
|
746
|
+
decimals: decimals
|
|
747
|
+
});
|
|
742
748
|
}
|
|
743
749
|
}
|
|
744
750
|
unchecked {
|
|
@@ -793,9 +799,22 @@ library JB721TiersHookLib {
|
|
|
793
799
|
}
|
|
794
800
|
}
|
|
795
801
|
|
|
796
|
-
/// @notice Sends a payout to a split recipient.
|
|
797
|
-
/// @
|
|
798
|
-
///
|
|
802
|
+
/// @notice Sends a payout to a single split recipient.
|
|
803
|
+
/// @dev Recipient resolution order: (1) `split.hook` if set; (2) `split.projectId` (uses
|
|
804
|
+
/// `split.preferAddToBalance` to choose between `addToBalanceOf` and `pay` on the primary terminal);
|
|
805
|
+
/// (3) `split.beneficiary` for a direct transfer. All-or-nothing for terminal and beneficiary paths;
|
|
806
|
+
/// the split-hook path supports partial pulls via allowance for ERC-20 and balance-delta for ETH.
|
|
807
|
+
/// @param directory The directory used to resolve the primary terminal when the split targets a project.
|
|
808
|
+
/// @param split The split definition (recipient + percent + flags).
|
|
809
|
+
/// @param token The token being distributed. Use `JBConstants.NATIVE_TOKEN` for ETH.
|
|
810
|
+
/// @param amount The amount to send to this recipient.
|
|
811
|
+
/// @param projectId The project ID emitting the payout (used in events and the hook context).
|
|
812
|
+
/// @param groupId The split group ID forwarded into the split hook context.
|
|
813
|
+
/// @param decimals The decimals of `token`, forwarded into the split hook context.
|
|
814
|
+
/// @return sent The amount that actually left this contract. Equals `amount` on a fully successful payout,
|
|
815
|
+
/// `0` on full failure (revert, missing recipient, transfer rejected), and a partial value when a split
|
|
816
|
+
/// hook pulled some but not all of its ERC-20 allowance (or returned some native ETH back to this
|
|
817
|
+
/// contract). The caller routes `amount - sent` to the project's leftover balance.
|
|
799
818
|
function _sendPayoutToSplit(
|
|
800
819
|
IJBDirectory directory,
|
|
801
820
|
JBSplit memory split,
|
|
@@ -806,7 +825,7 @@ library JB721TiersHookLib {
|
|
|
806
825
|
uint256 decimals
|
|
807
826
|
)
|
|
808
827
|
private
|
|
809
|
-
returns (
|
|
828
|
+
returns (uint256 sent)
|
|
810
829
|
{
|
|
811
830
|
bool isNativeToken = token == JBConstants.NATIVE_TOKEN;
|
|
812
831
|
|
|
@@ -817,36 +836,47 @@ library JB721TiersHookLib {
|
|
|
817
836
|
});
|
|
818
837
|
|
|
819
838
|
if (isNativeToken) {
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
839
|
+
// ETH is pushed via `{value: amount}`. On revert, the value transfer is rolled back along with
|
|
840
|
+
// the hook's frame so ETH stays here (balance unchanged → return 0). On success the hook usually
|
|
841
|
+
// keeps the full amount, but if it sends some ETH back via low-level call the balance-delta
|
|
842
|
+
// reports the actual amount that left this contract — the unsent portion routes to the project.
|
|
843
|
+
uint256 ethBefore = address(this).balance;
|
|
844
|
+
try split.hook.processSplitWith{value: amount}(context) {}
|
|
845
|
+
catch (bytes memory reason) {
|
|
825
846
|
emit SplitPayoutReverted({
|
|
826
847
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
827
848
|
});
|
|
828
|
-
return false;
|
|
829
849
|
}
|
|
850
|
+
return ethBefore - address(this).balance;
|
|
830
851
|
} else {
|
|
831
|
-
// ERC20:
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
|
|
852
|
+
// ERC20: grant the hook an allowance, then call the hook callback. The hook pulls tokens via
|
|
853
|
+
// `transferFrom` from inside `processSplitWith`. After the call we revoke any unconsumed
|
|
854
|
+
// allowance and report the actual amount pulled via balance-delta. Mirrors the controller's
|
|
855
|
+
// allowance pattern for reserved-token splits.
|
|
856
|
+
//
|
|
857
|
+
// Three outcomes:
|
|
858
|
+
// - Hook reverts (catch): balance unchanged → return 0. Caller routes the full `amount` to
|
|
859
|
+
// the project's balance.
|
|
860
|
+
// - Hook pulls partial `X < amount` and returns successfully: balance dropped by `X` →
|
|
861
|
+
// return `X`. Caller routes `amount - X` to the project's balance. The hook keeps `X`.
|
|
862
|
+
// - Hook pulls the full amount: return `amount`. Caller treats the split as fully distributed.
|
|
863
|
+
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
|
864
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(split.hook), value: amount});
|
|
837
865
|
try split.hook.processSplitWith(context) {}
|
|
838
866
|
catch (bytes memory reason) {
|
|
839
867
|
emit SplitPayoutReverted({
|
|
840
868
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
841
869
|
});
|
|
842
870
|
}
|
|
843
|
-
|
|
871
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(split.hook), value: 0});
|
|
872
|
+
return balanceBefore - IERC20(token).balanceOf(address(this));
|
|
844
873
|
}
|
|
845
874
|
} else if (split.projectId != 0) {
|
|
846
875
|
IJBTerminal terminal = directory.primaryTerminalOf({projectId: split.projectId, token: token});
|
|
847
|
-
if (address(terminal) == address(0)) return
|
|
876
|
+
if (address(terminal) == address(0)) return 0;
|
|
848
877
|
|
|
849
|
-
//
|
|
878
|
+
// Terminal calls are all-or-nothing: the terminal pulls the full amount or the call reverts.
|
|
879
|
+
// Wrap in try-catch so a failing terminal does not brick the whole payment.
|
|
850
880
|
if (split.preferAddToBalance) {
|
|
851
881
|
if (isNativeToken) {
|
|
852
882
|
try terminal.addToBalanceOf{value: amount}({
|
|
@@ -857,12 +887,12 @@ library JB721TiersHookLib {
|
|
|
857
887
|
memo: "",
|
|
858
888
|
metadata: bytes("")
|
|
859
889
|
}) {
|
|
860
|
-
return
|
|
890
|
+
return amount;
|
|
861
891
|
} catch (bytes memory reason) {
|
|
862
892
|
emit SplitPayoutReverted({
|
|
863
893
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
864
894
|
});
|
|
865
|
-
return
|
|
895
|
+
return 0;
|
|
866
896
|
}
|
|
867
897
|
} else {
|
|
868
898
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
@@ -874,14 +904,14 @@ library JB721TiersHookLib {
|
|
|
874
904
|
memo: "",
|
|
875
905
|
metadata: bytes("")
|
|
876
906
|
}) {
|
|
877
|
-
return
|
|
907
|
+
return amount;
|
|
878
908
|
} catch (bytes memory reason) {
|
|
879
909
|
// Reset approval on failure so tokens aren't left approved to the terminal.
|
|
880
910
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
881
911
|
emit SplitPayoutReverted({
|
|
882
912
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
883
913
|
});
|
|
884
|
-
return
|
|
914
|
+
return 0;
|
|
885
915
|
}
|
|
886
916
|
}
|
|
887
917
|
} else {
|
|
@@ -895,12 +925,12 @@ library JB721TiersHookLib {
|
|
|
895
925
|
memo: "",
|
|
896
926
|
metadata: bytes("")
|
|
897
927
|
}) {
|
|
898
|
-
return
|
|
928
|
+
return amount;
|
|
899
929
|
} catch (bytes memory reason) {
|
|
900
930
|
emit SplitPayoutReverted({
|
|
901
931
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
902
932
|
});
|
|
903
|
-
return
|
|
933
|
+
return 0;
|
|
904
934
|
}
|
|
905
935
|
} else {
|
|
906
936
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
@@ -913,37 +943,45 @@ library JB721TiersHookLib {
|
|
|
913
943
|
memo: "",
|
|
914
944
|
metadata: bytes("")
|
|
915
945
|
}) {
|
|
916
|
-
return
|
|
946
|
+
return amount;
|
|
917
947
|
} catch (bytes memory reason) {
|
|
918
948
|
// Reset approval on failure so tokens aren't left approved to the terminal.
|
|
919
949
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
920
950
|
emit SplitPayoutReverted({
|
|
921
951
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
922
952
|
});
|
|
923
|
-
return
|
|
953
|
+
return 0;
|
|
924
954
|
}
|
|
925
955
|
}
|
|
926
956
|
}
|
|
927
957
|
} else if (split.beneficiary != address(0)) {
|
|
928
958
|
if (isNativeToken) {
|
|
929
959
|
(bool success,) = split.beneficiary.call{value: amount}("");
|
|
930
|
-
if (!success) return
|
|
960
|
+
if (!success) return 0;
|
|
931
961
|
} else {
|
|
932
962
|
// Use the same low-level call + returndata check as SafeERC20.safeTransfer, but return
|
|
933
|
-
//
|
|
963
|
+
// 0 on failure instead of reverting. This handles non-standard tokens (e.g. USDT)
|
|
934
964
|
// that return void, while routing failed transfers to the project's balance instead
|
|
935
965
|
// of bricking all payments.
|
|
936
966
|
(bool callSuccess, bytes memory returndata) =
|
|
937
967
|
address(token).call(abi.encodeCall(IERC20.transfer, (split.beneficiary, amount)));
|
|
938
|
-
if (!callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)))) return
|
|
968
|
+
if (!callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)))) return 0;
|
|
939
969
|
}
|
|
940
|
-
return
|
|
970
|
+
return amount;
|
|
941
971
|
}
|
|
942
|
-
// No projectId and no beneficiary — return
|
|
943
|
-
return
|
|
972
|
+
// No projectId and no beneficiary — return 0 so the funds go to the project's balance.
|
|
973
|
+
return 0;
|
|
944
974
|
}
|
|
945
975
|
|
|
946
976
|
/// @notice Sets split groups in JBSplits for tiers that have splits configured.
|
|
977
|
+
/// @dev Walks `tiersToAdd` in parallel with `tierIdsAdded`. Tiers with an empty `splits` array are skipped,
|
|
978
|
+
/// so only tiers that opt into split distribution allocate a group. The group ID is packed as
|
|
979
|
+
/// `hookAddress | (tierId << 160)` so each tier owns a distinct group within the hook's namespace.
|
|
980
|
+
/// @param splits The splits contract that stores the per-tier groups.
|
|
981
|
+
/// @param projectId The project ID owning the splits.
|
|
982
|
+
/// @param hookAddress The 721 hook address; forms the low 160 bits of each group ID.
|
|
983
|
+
/// @param tiersToAdd The tier configurations being recorded; each may carry its own `splits` array.
|
|
984
|
+
/// @param tierIdsAdded The tier IDs assigned at recording time, in the same order as `tiersToAdd`.
|
|
947
985
|
function _setSplitGroupsFor(
|
|
948
986
|
IJBSplits splits,
|
|
949
987
|
uint256 projectId,
|
|
@@ -24,6 +24,8 @@ pragma solidity ^0.8.0;
|
|
|
24
24
|
/// @custom:member holdFees A flag indicating if fees should be held during this ruleset.
|
|
25
25
|
/// @custom:member scopeCashOutsToLocalBalances A flag indicating if omnichain cash-out calculations should use only
|
|
26
26
|
/// the local chain's terminal balance instead of the project's balance held in all terminals.
|
|
27
|
+
/// @custom:member pauseCrossProjectFeeFreeInflows If `true`, the project cannot be targeted by
|
|
28
|
+
/// `payAfterCashOutTokensOf` / `addToBalanceAfterCashOutTokensOf` calls during this ruleset.
|
|
27
29
|
/// @custom:member useDataHookForCashOuts A flag indicating if the data hook should be used for cash out transactions
|
|
28
30
|
/// during
|
|
29
31
|
/// this ruleset.
|
|
@@ -44,6 +46,7 @@ struct JBPayDataHookRulesetMetadata {
|
|
|
44
46
|
bool ownerMustSendPayouts;
|
|
45
47
|
bool holdFees;
|
|
46
48
|
bool scopeCashOutsToLocalBalances;
|
|
49
|
+
bool pauseCrossProjectFeeFreeInflows;
|
|
47
50
|
bool useDataHookForCashOut;
|
|
48
51
|
uint16 metadata;
|
|
49
52
|
}
|
|
@@ -224,6 +224,7 @@ contract UnitTestSetup is Test {
|
|
|
224
224
|
ownerMustSendPayouts: false,
|
|
225
225
|
holdFees: false,
|
|
226
226
|
scopeCashOutsToLocalBalances: true,
|
|
227
|
+
pauseCrossProjectFeeFreeInflows: false,
|
|
227
228
|
useDataHookForPay: true,
|
|
228
229
|
useDataHookForCashOut: true,
|
|
229
230
|
dataHook: address(0),
|
|
@@ -773,6 +774,7 @@ contract UnitTestSetup is Test {
|
|
|
773
774
|
allowAddPriceFeed: false,
|
|
774
775
|
holdFees: false,
|
|
775
776
|
scopeCashOutsToLocalBalances: true,
|
|
777
|
+
pauseCrossProjectFeeFreeInflows: false,
|
|
776
778
|
useDataHookForCashOut: false,
|
|
777
779
|
metadata: 0x00
|
|
778
780
|
});
|