@bananapus/721-hook-v6 0.0.46 → 0.0.49
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/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
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.48",
|
|
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"
|
package/src/JB721Checkpoints.sol
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
4
5
|
import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
|
|
5
6
|
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
6
7
|
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
|
|
@@ -24,6 +25,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
24
25
|
//*********************************************************************//
|
|
25
26
|
|
|
26
27
|
error JB721Checkpoints_AlreadyInitialized(address hook);
|
|
28
|
+
error JB721Checkpoints_NotOwner(uint256 tokenId, address caller);
|
|
27
29
|
error JB721Checkpoints_Unauthorized(address caller, address hook);
|
|
28
30
|
|
|
29
31
|
//*********************************************************************//
|
|
@@ -44,7 +46,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
44
46
|
// -------------------- internal stored properties ------------------- //
|
|
45
47
|
//*********************************************************************//
|
|
46
48
|
|
|
47
|
-
/// @notice Checkpointed token owners for historical reward eligibility
|
|
49
|
+
/// @notice Checkpointed token owners for historical reward eligibility. Written on enrollment or transfer.
|
|
48
50
|
/// @custom:param tokenId The token ID to get historical owner checkpoints for.
|
|
49
51
|
mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
|
|
50
52
|
|
|
@@ -64,6 +66,37 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
64
66
|
// ---------------------- external transactions ---------------------- //
|
|
65
67
|
//*********************************************************************//
|
|
66
68
|
|
|
69
|
+
/// @notice Delegates voting power and enrolls tokens for distribution eligibility.
|
|
70
|
+
/// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
|
|
71
|
+
/// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
|
|
72
|
+
/// distribution. The existing `delegate(address)` from OZ Votes still works for pure delegation without enrollment.
|
|
73
|
+
/// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
|
|
74
|
+
/// @param tokenIds The token IDs to enroll for distribution eligibility.
|
|
75
|
+
function delegate(address delegatee, uint256[] calldata tokenIds) external override {
|
|
76
|
+
// Delegate voting power (reuses OZ Votes internals).
|
|
77
|
+
_delegate({account: msg.sender, delegatee: delegatee});
|
|
78
|
+
|
|
79
|
+
// Write per-token owner checkpoints for distribution eligibility.
|
|
80
|
+
for (uint256 i; i < tokenIds.length;) {
|
|
81
|
+
uint256 tokenId = tokenIds[i];
|
|
82
|
+
|
|
83
|
+
// Only the current owner can enroll their tokens.
|
|
84
|
+
if (IERC721(HOOK).ownerOf(tokenId) != msg.sender) {
|
|
85
|
+
revert JB721Checkpoints_NotOwner({tokenId: tokenId, caller: msg.sender});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Write an owner checkpoint if the token has none yet.
|
|
89
|
+
if (_ownerCheckpointsOf[tokenId].length() == 0) {
|
|
90
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
91
|
+
_ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(msg.sender)});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
unchecked {
|
|
95
|
+
++i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
/// @notice Initializes a cloned module with its hook reference.
|
|
68
101
|
/// @dev Can only be called once. Called by the deployer after cloning.
|
|
69
102
|
/// @param hook The hook this module serves.
|
|
@@ -98,11 +131,11 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
98
131
|
//*********************************************************************//
|
|
99
132
|
|
|
100
133
|
/// @notice The owner of an NFT at a past block.
|
|
101
|
-
/// @dev
|
|
102
|
-
///
|
|
134
|
+
/// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
|
|
135
|
+
/// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
|
|
103
136
|
/// @param tokenId The token ID of the NFT to get the historical owner of.
|
|
104
137
|
/// @param blockNumber The block number to look up.
|
|
105
|
-
/// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
|
|
138
|
+
/// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
|
|
106
139
|
function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
|
|
107
140
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
108
141
|
uint96 blockNumber96 = uint96(blockNumber);
|
|
@@ -110,10 +143,11 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
110
143
|
Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
|
|
111
144
|
uint256 checkpointCount = checkpoints.length();
|
|
112
145
|
|
|
113
|
-
//
|
|
114
|
-
if (checkpointCount == 0
|
|
115
|
-
|
|
116
|
-
|
|
146
|
+
// No checkpoints = not enrolled and never transferred. Not eligible.
|
|
147
|
+
if (checkpointCount == 0) return address(0);
|
|
148
|
+
|
|
149
|
+
// Query is before the first checkpoint — token not yet enrolled/transferred at this block.
|
|
150
|
+
if (checkpoints.at(0)._key > blockNumber96) return address(0);
|
|
117
151
|
|
|
118
152
|
return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
|
|
119
153
|
}
|
|
@@ -14,11 +14,11 @@ interface IJB721Checkpoints is IERC5805 {
|
|
|
14
14
|
function HOOK() external view returns (address);
|
|
15
15
|
|
|
16
16
|
/// @notice The owner of an NFT at a past block.
|
|
17
|
-
/// @dev
|
|
18
|
-
///
|
|
17
|
+
/// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
|
|
18
|
+
/// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
|
|
19
19
|
/// @param tokenId The token ID of the NFT to get the historical owner of.
|
|
20
20
|
/// @param blockNumber The block number to look up.
|
|
21
|
-
/// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
|
|
21
|
+
/// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
|
|
22
22
|
function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address);
|
|
23
23
|
|
|
24
24
|
/// @notice The store that holds tier and voting data for the hook's NFTs.
|
|
@@ -26,6 +26,14 @@ interface IJB721Checkpoints is IERC5805 {
|
|
|
26
26
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
27
27
|
function STORE() external view returns (IJB721TiersHookStore);
|
|
28
28
|
|
|
29
|
+
/// @notice Delegates voting power and enrolls tokens for distribution eligibility.
|
|
30
|
+
/// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
|
|
31
|
+
/// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
|
|
32
|
+
/// distribution.
|
|
33
|
+
/// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
|
|
34
|
+
/// @param tokenIds The token IDs to enroll for distribution eligibility.
|
|
35
|
+
function delegate(address delegatee, uint256[] calldata tokenIds) external;
|
|
36
|
+
|
|
29
37
|
/// @notice Initializes a cloned module with its hook reference.
|
|
30
38
|
/// @dev Can only be called once. Called by the deployer after cloning.
|
|
31
39
|
/// @param hook The hook this module serves.
|
|
@@ -33,7 +41,6 @@ interface IJB721Checkpoints is IERC5805 {
|
|
|
33
41
|
|
|
34
42
|
/// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
|
|
35
43
|
/// @dev Looks up the token's tier voting units from the store internally.
|
|
36
|
-
/// Auto-self-delegates on first receive so checkpoints work without manual delegation.
|
|
37
44
|
/// @param from The previous owner (address(0) on mint).
|
|
38
45
|
/// @param to The new owner (address(0) on burn).
|
|
39
46
|
/// @param tokenId The token ID to transfer (used to look up tier voting units).
|
|
@@ -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,
|