@bananapus/721-hook-v6 0.0.50 → 0.0.52
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 +4 -4
- package/script/Deploy.s.sol +36 -39
- package/script/helpers/Hook721DeploymentLib.sol +18 -24
- package/src/JB721Checkpoints.sol +11 -11
- package/src/JB721TiersHook.sol +33 -36
- package/src/JB721TiersHookDeployer.sol +1 -1
- package/src/JB721TiersHookProjectDeployer.sol +5 -8
- package/src/JB721TiersHookStore.sol +9 -9
- package/src/abstract/ERC721.sol +3 -3
- package/src/abstract/JB721Hook.sol +15 -13
- package/src/interfaces/IJB721Checkpoints.sol +3 -3
- package/src/interfaces/IJB721Hook.sol +1 -1
- package/src/interfaces/IJB721TiersHook.sol +8 -9
- package/src/interfaces/IJB721TiersHookStore.sol +3 -3
- package/src/libraries/JB721TiersHookLib.sol +182 -43
- package/src/structs/JB721Tier.sol +2 -2
- package/src/structs/JB721TierConfig.sol +2 -2
- package/src/structs/JBPayDataHookRulesetMetadata.sol +0 -3
- package/test/utils/ForTest_JB721TiersHook.sol +1 -1
- package/test/utils/UnitTestSetup.sol +8 -10
|
@@ -71,7 +71,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
71
71
|
/// @custom:param hook The 721 contract that the tier belongs to.
|
|
72
72
|
/// @custom:param tierId The ID of the tier to get the encoded IPFS URI of.
|
|
73
73
|
/// @custom:returns The encoded IPFS URI.
|
|
74
|
-
mapping(address hook => mapping(uint256 tierId => bytes32)) public override
|
|
74
|
+
mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIpfsUriOf;
|
|
75
75
|
|
|
76
76
|
/// @notice Returns the largest tier ID currently used on the provided 721 contract.
|
|
77
77
|
/// @dev This may not include the last tier ID if it has been removed.
|
|
@@ -166,7 +166,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
166
166
|
/// @return The encoded IPFS URI for the NFT's tier.
|
|
167
167
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
168
168
|
function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) {
|
|
169
|
-
return
|
|
169
|
+
return encodedIpfsUriOf[hook][tierIdOfToken(tokenId)];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/// @notice Get the behavioral flags for a hook — such as whether transfers are pausable, whether NFT holders can
|
|
@@ -628,7 +628,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
628
628
|
// No reserve frequency if there is no reserve beneficiary.
|
|
629
629
|
reserveFrequency: reserveBeneficiary == address(0) ? 0 : storedTier.reserveFrequency,
|
|
630
630
|
reserveBeneficiary: reserveBeneficiary,
|
|
631
|
-
|
|
631
|
+
encodedIpfsUri: encodedIpfsUriOf[hook][tierId],
|
|
632
632
|
category: storedTier.category,
|
|
633
633
|
discountPercent: storedTier.discountPercent,
|
|
634
634
|
flags: JB721TierFlags({
|
|
@@ -1051,9 +1051,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1051
1051
|
}
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
-
// Set the `
|
|
1055
|
-
if (tierToAdd.
|
|
1056
|
-
|
|
1054
|
+
// Set the `encodedIpfsUri` if needed.
|
|
1055
|
+
if (tierToAdd.encodedIpfsUri != bytes32(0)) {
|
|
1056
|
+
encodedIpfsUriOf[msg.sender][tierId] = tierToAdd.encodedIpfsUri;
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
1059
|
if (startSortedTierId != 0) {
|
|
@@ -1392,10 +1392,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1392
1392
|
|
|
1393
1393
|
/// @notice Record a new encoded IPFS URI for a tier.
|
|
1394
1394
|
/// @param tierId The ID of the tier to set the encoded IPFS URI of.
|
|
1395
|
-
/// @param
|
|
1395
|
+
/// @param encodedIpfsUri The encoded IPFS URI to set for the tier.
|
|
1396
1396
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
1397
|
-
function
|
|
1398
|
-
|
|
1397
|
+
function recordSetEncodedIpfsUriOf(uint256 tierId, bytes32 encodedIpfsUri) external override {
|
|
1398
|
+
encodedIpfsUriOf[msg.sender][tierId] = encodedIpfsUri;
|
|
1399
1399
|
}
|
|
1400
1400
|
|
|
1401
1401
|
/// @notice Record a newly set token URI resolver.
|
package/src/abstract/ERC721.sol
CHANGED
|
@@ -40,9 +40,9 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
40
40
|
/**
|
|
41
41
|
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
|
|
42
42
|
*/
|
|
43
|
-
function _initialize(string memory
|
|
44
|
-
_name =
|
|
45
|
-
_symbol =
|
|
43
|
+
function _initialize(string memory collectionName, string memory collectionSymbol) internal {
|
|
44
|
+
_name = collectionName;
|
|
45
|
+
_symbol = collectionSymbol;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -50,7 +50,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
50
50
|
//*********************************************************************//
|
|
51
51
|
|
|
52
52
|
/// @notice The ID of the project that this contract is associated with.
|
|
53
|
-
uint256 public override
|
|
53
|
+
uint256 public override projectId;
|
|
54
54
|
|
|
55
55
|
//*********************************************************************//
|
|
56
56
|
// -------------------------- constructor ---------------------------- //
|
|
@@ -194,16 +194,16 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
194
194
|
override
|
|
195
195
|
{
|
|
196
196
|
// Keep a reference to the project ID.
|
|
197
|
-
uint256
|
|
197
|
+
uint256 localProjectId = projectId;
|
|
198
198
|
|
|
199
199
|
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
200
200
|
// interaction with the correct project.
|
|
201
201
|
if (
|
|
202
|
-
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId:
|
|
203
|
-
|| context.projectId !=
|
|
202
|
+
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: localProjectId, terminal: IJBTerminal(msg.sender)})
|
|
203
|
+
|| context.projectId != localProjectId
|
|
204
204
|
) {
|
|
205
205
|
revert JB721Hook_InvalidCashOut({
|
|
206
|
-
caller: msg.sender, contextProjectId: context.projectId, projectId:
|
|
206
|
+
caller: msg.sender, contextProjectId: context.projectId, projectId: localProjectId, msgValue: msg.value
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -243,15 +243,17 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
243
243
|
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
244
244
|
/// @param context The payment context passed in by the terminal.
|
|
245
245
|
function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
|
|
246
|
-
uint256
|
|
246
|
+
uint256 localProjectId = projectId;
|
|
247
247
|
|
|
248
248
|
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
249
249
|
// interaction with the correct project.
|
|
250
250
|
if (
|
|
251
|
-
!DIRECTORY.isTerminalOf({projectId:
|
|
252
|
-
|| context.projectId !=
|
|
251
|
+
!DIRECTORY.isTerminalOf({projectId: localProjectId, terminal: IJBTerminal(msg.sender)})
|
|
252
|
+
|| context.projectId != localProjectId
|
|
253
253
|
) {
|
|
254
|
-
revert JB721Hook_InvalidPay({
|
|
254
|
+
revert JB721Hook_InvalidPay({
|
|
255
|
+
caller: msg.sender, contextProjectId: context.projectId, projectId: localProjectId
|
|
256
|
+
});
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
// Process the payment.
|
|
@@ -267,12 +269,12 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
267
269
|
function _didBurn(uint256[] memory tokenIds) internal virtual;
|
|
268
270
|
|
|
269
271
|
/// @notice Initializes the contract by associating it with a project and adding ERC721 details.
|
|
270
|
-
/// @param
|
|
272
|
+
/// @param initialProjectId The ID of the project that this contract is associated with.
|
|
271
273
|
/// @param name The name of the NFT collection.
|
|
272
274
|
/// @param symbol The symbol representing the NFT collection.
|
|
273
|
-
function _initialize(uint256
|
|
274
|
-
ERC721._initialize({
|
|
275
|
-
|
|
275
|
+
function _initialize(uint256 initialProjectId, string memory name, string memory symbol) internal {
|
|
276
|
+
ERC721._initialize({collectionName: name, collectionSymbol: symbol});
|
|
277
|
+
projectId = initialProjectId;
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
/// @notice Process a received payment by minting NFTs and/or updating credits. Subclasses implement the
|
|
@@ -11,7 +11,7 @@ interface IJB721Checkpoints is IERC5805 {
|
|
|
11
11
|
/// @notice The hook that this module tracks voting power for.
|
|
12
12
|
/// @return The hook address.
|
|
13
13
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
14
|
-
function
|
|
14
|
+
function hook() external view returns (address);
|
|
15
15
|
|
|
16
16
|
/// @notice The owner of an NFT at a past block.
|
|
17
17
|
/// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
|
|
@@ -36,8 +36,8 @@ interface IJB721Checkpoints is IERC5805 {
|
|
|
36
36
|
|
|
37
37
|
/// @notice Initializes a cloned module with its hook reference.
|
|
38
38
|
/// @dev Can only be called once. Called by the deployer after cloning.
|
|
39
|
-
/// @param
|
|
40
|
-
function initialize(address
|
|
39
|
+
/// @param hookAddress The hook this module serves.
|
|
40
|
+
function initialize(address hookAddress) external;
|
|
41
41
|
|
|
42
42
|
/// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
|
|
43
43
|
/// @dev Looks up the token's tier voting units from the store internally.
|
|
@@ -18,5 +18,5 @@ interface IJB721Hook is IJBRulesetDataHook, IJBPayHook, IJBCashOutHook {
|
|
|
18
18
|
|
|
19
19
|
/// @notice The ID of the project that this contract is associated with.
|
|
20
20
|
/// @return The project ID.
|
|
21
|
-
function
|
|
21
|
+
function projectId() external view returns (uint256);
|
|
22
22
|
}
|
|
@@ -89,7 +89,7 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
89
89
|
/// @param tierId The ID of the tier whose encoded IPFS URI was set.
|
|
90
90
|
/// @param encodedUri The new encoded IPFS URI.
|
|
91
91
|
/// @param caller The address that called the function.
|
|
92
|
-
event
|
|
92
|
+
event SetEncodedIpfsUri(uint256 indexed tierId, bytes32 encodedUri, address caller);
|
|
93
93
|
|
|
94
94
|
/// @notice Emitted when the token URI resolver is set.
|
|
95
95
|
/// @param resolver The new token URI resolver.
|
|
@@ -140,8 +140,7 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
140
140
|
/// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
|
|
141
141
|
/// @dev Deployed lazily on first mint. Pass this to JBTokenDistributor as the IVotes token.
|
|
142
142
|
/// @return The checkpoint module.
|
|
143
|
-
|
|
144
|
-
function CHECKPOINTS() external view returns (IJB721Checkpoints);
|
|
143
|
+
function checkpoints() external view returns (IJB721Checkpoints);
|
|
145
144
|
|
|
146
145
|
/// @notice The contract that exposes price feeds for currency conversions.
|
|
147
146
|
/// @return The prices contract.
|
|
@@ -165,7 +164,7 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
165
164
|
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external;
|
|
166
165
|
|
|
167
166
|
/// @notice Initializes a cloned copy of the original `JB721TiersHook` contract.
|
|
168
|
-
/// @param
|
|
167
|
+
/// @param initialProjectId The ID of the project this hook is associated with.
|
|
169
168
|
/// @param name The name of the NFT collection.
|
|
170
169
|
/// @param symbol The symbol representing the NFT collection.
|
|
171
170
|
/// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
|
|
@@ -174,7 +173,7 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
174
173
|
/// @param tiersConfig The NFT tiers and pricing context to initialize the hook with.
|
|
175
174
|
/// @param flags A set of additional options which dictate how the hook behaves.
|
|
176
175
|
function initialize(
|
|
177
|
-
uint256
|
|
176
|
+
uint256 initialProjectId,
|
|
178
177
|
string memory name,
|
|
179
178
|
string memory symbol,
|
|
180
179
|
string memory baseUri,
|
|
@@ -217,16 +216,16 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
217
216
|
/// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value
|
|
218
217
|
/// to leave unchanged. `address(this)` is used instead of `address(0)` because `address(0)` is a valid value that
|
|
219
218
|
/// clears the resolver.
|
|
220
|
-
/// @param
|
|
221
|
-
/// @param
|
|
219
|
+
/// @param encodedIpfsUriTierId The ID of the tier to set the encoded IPFS URI of.
|
|
220
|
+
/// @param encodedIpfsUri The encoded IPFS URI to set.
|
|
222
221
|
function setMetadata(
|
|
223
222
|
string calldata name,
|
|
224
223
|
string calldata symbol,
|
|
225
224
|
string calldata baseUri,
|
|
226
225
|
string calldata contractUri,
|
|
227
226
|
IJB721TokenUriResolver tokenUriResolver,
|
|
228
|
-
uint256
|
|
229
|
-
bytes32
|
|
227
|
+
uint256 encodedIpfsUriTierId,
|
|
228
|
+
bytes32 encodedIpfsUri
|
|
230
229
|
)
|
|
231
230
|
external;
|
|
232
231
|
}
|
|
@@ -42,7 +42,7 @@ interface IJB721TiersHookStore {
|
|
|
42
42
|
/// @param tierId The ID of the tier to get the encoded IPFS URI of.
|
|
43
43
|
/// @return The encoded IPFS URI.
|
|
44
44
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
45
|
-
function
|
|
45
|
+
function encodedIpfsUriOf(address hook, uint256 tierId) external view returns (bytes32);
|
|
46
46
|
|
|
47
47
|
/// @notice The encoded IPFS URI for the tier of the 721 with the provided token ID.
|
|
48
48
|
/// @param hook The 721 contract that the encoded IPFS URI belongs to.
|
|
@@ -246,9 +246,9 @@ interface IJB721TiersHookStore {
|
|
|
246
246
|
|
|
247
247
|
/// @notice Record a new encoded IPFS URI for a tier.
|
|
248
248
|
/// @param tierId The ID of the tier to set the encoded IPFS URI of.
|
|
249
|
-
/// @param
|
|
249
|
+
/// @param encodedIpfsUri The encoded IPFS URI to set for the tier.
|
|
250
250
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
251
|
-
function
|
|
251
|
+
function recordSetEncodedIpfsUriOf(uint256 tierId, bytes32 encodedIpfsUri) external;
|
|
252
252
|
|
|
253
253
|
/// @notice Record a newly set token URI resolver.
|
|
254
254
|
/// @param resolver The resolver to set.
|
|
@@ -360,7 +360,7 @@ library JB721TiersHookLib {
|
|
|
360
360
|
// attributable to tier splits. Downstream compositors (e.g. JBOmnichainDeployer) use this
|
|
361
361
|
// to preserve split credit when an extra hook (buyback) returns weight=0.
|
|
362
362
|
if (totalSplitAmount != 0 && context.amount.value != 0 && store.flagsOf(address(this)).issueTokensForSplits) {
|
|
363
|
-
splitCreditWeight = mulDiv(context.weight, totalSplitAmount, context.amount.value);
|
|
363
|
+
splitCreditWeight = mulDiv({x: context.weight, y: totalSplitAmount, denominator: context.amount.value});
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
// Resolve the effective beneficiary from payment metadata.
|
|
@@ -690,6 +690,149 @@ library JB721TiersHookLib {
|
|
|
690
690
|
// ----------------------- private helpers --------------------------- //
|
|
691
691
|
//*********************************************************************//
|
|
692
692
|
|
|
693
|
+
/// @notice Approves `terminal` to spend `amount` of `token`, calls `addToBalanceOf`, then revokes any
|
|
694
|
+
/// unconsumed allowance and reports the actual amount consumed via the allowance delta.
|
|
695
|
+
/// @dev Allowance-delta consumption protects against terminals that return successfully without pulling
|
|
696
|
+
/// the full granted amount, and the unconditional final `forceApprove(..., 0)` ensures no stale allowance
|
|
697
|
+
/// is left behind on either the success or failure path.
|
|
698
|
+
/// @param terminal The terminal to call.
|
|
699
|
+
/// @param token The ERC-20 token. Native must not be passed here.
|
|
700
|
+
/// @param projectId The destination project ID.
|
|
701
|
+
/// @param amount The amount to grant as allowance and request the terminal to pull.
|
|
702
|
+
/// @return consumed The amount actually consumed by the terminal (0 on revert; otherwise
|
|
703
|
+
/// `amount - remainingAllowance` after the call).
|
|
704
|
+
/// @return failureReason The revert reason from the terminal call, or empty bytes if the call succeeded.
|
|
705
|
+
function _approveAndAddToBalance(
|
|
706
|
+
IJBTerminal terminal,
|
|
707
|
+
address token,
|
|
708
|
+
uint256 projectId,
|
|
709
|
+
uint256 amount
|
|
710
|
+
)
|
|
711
|
+
private
|
|
712
|
+
returns (uint256 consumed, bytes memory failureReason)
|
|
713
|
+
{
|
|
714
|
+
// Grant the terminal allowance to pull up to `amount` of `token` from this contract. `forceApprove`
|
|
715
|
+
// sets the allowance to exactly `amount` regardless of any prior allowance, so a leftover non-zero
|
|
716
|
+
// allowance from a previous failed call cannot inflate this one.
|
|
717
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
718
|
+
|
|
719
|
+
// Track whether the external call returned successfully. Initialized to `false` so a revert path
|
|
720
|
+
// leaves it unset and the consumption math below is skipped.
|
|
721
|
+
bool succeeded;
|
|
722
|
+
|
|
723
|
+
// Call the terminal's `addToBalanceOf` inside try/catch so a reverting terminal can't brick the
|
|
724
|
+
// surrounding payment. The terminal is expected to `transferFrom` against the allowance set above.
|
|
725
|
+
try terminal.addToBalanceOf({
|
|
726
|
+
projectId: projectId,
|
|
727
|
+
token: token,
|
|
728
|
+
amount: amount,
|
|
729
|
+
// Don't release any held fees on this call — fee returns are handled separately.
|
|
730
|
+
shouldReturnHeldFees: false,
|
|
731
|
+
// No memo or metadata is forwarded; the leftover routing is opaque to off-chain consumers.
|
|
732
|
+
memo: "",
|
|
733
|
+
metadata: bytes("")
|
|
734
|
+
}) {
|
|
735
|
+
// The terminal returned without reverting. The actual amount it consumed is measured below
|
|
736
|
+
// via the allowance delta — `succeeded` only records that there was no revert to report.
|
|
737
|
+
succeeded = true;
|
|
738
|
+
} catch (bytes memory reason) {
|
|
739
|
+
// The terminal reverted. Capture the reason so the caller can surface it via the
|
|
740
|
+
// `SplitPayoutReverted` event (or, for the leftover fallback, the
|
|
741
|
+
// `JB721TiersHookLib_SplitFallbackFailed` error) without re-throwing here.
|
|
742
|
+
failureReason = reason;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// On success, compute how much the terminal actually pulled. The post-call allowance equals
|
|
746
|
+
// `amount - actuallyConsumed` (since `forceApprove` set the pre-call allowance to `amount`), so
|
|
747
|
+
// `consumed = amount - postAllowance`. On failure, `succeeded` is `false` and `consumed` stays at
|
|
748
|
+
// its default `0`, which lets the caller route the full `amount` to the project's leftover balance.
|
|
749
|
+
if (succeeded) {
|
|
750
|
+
// Safe to use `unchecked`: `postAllowance` cannot exceed `amount` because the terminal can
|
|
751
|
+
// only decrement (via `transferFrom`) the allowance we just set to `amount`.
|
|
752
|
+
unchecked {
|
|
753
|
+
consumed = amount - IERC20(token).allowance({owner: address(this), spender: address(terminal)});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Unconditionally revoke any remaining allowance. This is the load-bearing safety step — without
|
|
758
|
+
// it, a terminal that returns successfully without pulling the full `amount` would leave a stale
|
|
759
|
+
// non-zero allowance that the terminal could drain later. Running on both the success and failure
|
|
760
|
+
// paths means no allowance ever leaks out of this function.
|
|
761
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/// @notice Approves `terminal` to spend `amount` of `token`, calls `pay`, then revokes any unconsumed
|
|
765
|
+
/// allowance and reports the actual amount consumed via the allowance delta.
|
|
766
|
+
/// @dev Mirrors `_approveAndAddToBalance` for the `pay` entry point.
|
|
767
|
+
/// @param terminal The terminal to call.
|
|
768
|
+
/// @param token The ERC-20 token. Native must not be passed here.
|
|
769
|
+
/// @param projectId The destination project ID.
|
|
770
|
+
/// @param amount The amount to grant as allowance and request the terminal to pull.
|
|
771
|
+
/// @param beneficiary The beneficiary for the `pay` call.
|
|
772
|
+
/// @return consumed The amount actually consumed by the terminal.
|
|
773
|
+
/// @return failureReason The revert reason from the terminal call, or empty bytes on success.
|
|
774
|
+
function _approveAndPay(
|
|
775
|
+
IJBTerminal terminal,
|
|
776
|
+
address token,
|
|
777
|
+
uint256 projectId,
|
|
778
|
+
uint256 amount,
|
|
779
|
+
address payable beneficiary
|
|
780
|
+
)
|
|
781
|
+
private
|
|
782
|
+
returns (uint256 consumed, bytes memory failureReason)
|
|
783
|
+
{
|
|
784
|
+
// Grant the terminal allowance to pull up to `amount` of `token` from this contract. `forceApprove`
|
|
785
|
+
// sets the allowance to exactly `amount` regardless of any prior allowance, so a leftover non-zero
|
|
786
|
+
// allowance from a previous failed call cannot inflate this one.
|
|
787
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
788
|
+
|
|
789
|
+
// Track whether the external call returned successfully. Initialized to `false` so a revert path
|
|
790
|
+
// leaves it unset and the consumption math below is skipped.
|
|
791
|
+
bool succeeded;
|
|
792
|
+
|
|
793
|
+
// Call the terminal's `pay` inside try/catch so a reverting terminal can't brick the surrounding
|
|
794
|
+
// payment. The terminal is expected to `transferFrom` against the allowance set above and mint
|
|
795
|
+
// project tokens to `beneficiary` in return.
|
|
796
|
+
try terminal.pay({
|
|
797
|
+
projectId: projectId,
|
|
798
|
+
token: token,
|
|
799
|
+
amount: amount,
|
|
800
|
+
beneficiary: beneficiary,
|
|
801
|
+
// No minimum is enforced on the terminal-side mint — the split-level minimum (if any) is
|
|
802
|
+
// enforced by the caller before reaching this helper.
|
|
803
|
+
minReturnedTokens: 0,
|
|
804
|
+
// No memo or metadata is forwarded; the leftover routing is opaque to off-chain consumers.
|
|
805
|
+
memo: "",
|
|
806
|
+
metadata: bytes("")
|
|
807
|
+
}) {
|
|
808
|
+
// The terminal returned without reverting. The actual amount it consumed is measured below
|
|
809
|
+
// via the allowance delta — `succeeded` only records that there was no revert to report.
|
|
810
|
+
succeeded = true;
|
|
811
|
+
} catch (bytes memory reason) {
|
|
812
|
+
// The terminal reverted. Capture the reason so the caller can surface it via the
|
|
813
|
+
// `SplitPayoutReverted` event without re-throwing here.
|
|
814
|
+
failureReason = reason;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// On success, compute how much the terminal actually pulled. The post-call allowance equals
|
|
818
|
+
// `amount - actuallyConsumed` (since `forceApprove` set the pre-call allowance to `amount`), so
|
|
819
|
+
// `consumed = amount - postAllowance`. On failure, `succeeded` is `false` and `consumed` stays at
|
|
820
|
+
// its default `0`, which lets the caller route the full `amount` to the project's leftover balance.
|
|
821
|
+
if (succeeded) {
|
|
822
|
+
// Safe to use `unchecked`: `postAllowance` cannot exceed `amount` because the terminal can
|
|
823
|
+
// only decrement (via `transferFrom`) the allowance we just set to `amount`.
|
|
824
|
+
unchecked {
|
|
825
|
+
consumed = amount - IERC20(token).allowance({owner: address(this), spender: address(terminal)});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Unconditionally revoke any remaining allowance. This is the load-bearing safety step — without
|
|
830
|
+
// it, a terminal that returns successfully without pulling the full `amount` would leave a stale
|
|
831
|
+
// non-zero allowance that the terminal could drain later. Running on both the success and failure
|
|
832
|
+
// paths means no allowance ever leaks out of this function.
|
|
833
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
834
|
+
}
|
|
835
|
+
|
|
693
836
|
/// @notice Distributes funds for a single tier's split group.
|
|
694
837
|
/// @dev `_sendPayoutToSplit` returns the actual amount that left this contract — `payoutAmount` on full
|
|
695
838
|
/// success, `0` when the split fully fails (revert or missing recipient), or a partial value when an ERC-20
|
|
@@ -779,20 +922,16 @@ library JB721TiersHookLib {
|
|
|
779
922
|
});
|
|
780
923
|
}
|
|
781
924
|
} else {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}) {}
|
|
791
|
-
catch (bytes memory reason) {
|
|
792
|
-
// Reset approval on failure.
|
|
793
|
-
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
925
|
+
// Allowance-delta + unconditional cleanup: a terminal that returns successfully without pulling
|
|
926
|
+
// the full allowance would otherwise leave funds stranded in this library/hook and leak a
|
|
927
|
+
// non-zero allowance to the terminal. Fail closed: revert if either the call reverts OR the
|
|
928
|
+
// terminal under-consumed the granted allowance.
|
|
929
|
+
(uint256 consumed, bytes memory failureReason) = _approveAndAddToBalance({
|
|
930
|
+
terminal: terminal, token: token, projectId: projectId, amount: leftoverAmount
|
|
931
|
+
});
|
|
932
|
+
if (failureReason.length != 0 || consumed != leftoverAmount) {
|
|
794
933
|
revert JB721TiersHookLib_SplitFallbackFailed({
|
|
795
|
-
projectId: projectId, token: token, amount: leftoverAmount, reason:
|
|
934
|
+
projectId: projectId, token: token, amount: leftoverAmount, reason: failureReason
|
|
796
935
|
});
|
|
797
936
|
}
|
|
798
937
|
}
|
|
@@ -895,24 +1034,24 @@ library JB721TiersHookLib {
|
|
|
895
1034
|
return 0;
|
|
896
1035
|
}
|
|
897
1036
|
} else {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
return amount;
|
|
908
|
-
} catch (bytes memory reason) {
|
|
909
|
-
// Reset approval on failure so tokens aren't left approved to the terminal.
|
|
910
|
-
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
1037
|
+
// Allowance-delta + unconditional cleanup. `sent` reflects the amount the terminal actually
|
|
1038
|
+
// pulled; the unconsumed remainder (if any) stays in this contract and is routed back to the
|
|
1039
|
+
// project's leftover balance by the caller. On revert, `sent` stays 0 and the SplitPayoutReverted
|
|
1040
|
+
// event records the failure reason.
|
|
1041
|
+
bytes memory failureReason;
|
|
1042
|
+
(sent, failureReason) = _approveAndAddToBalance({
|
|
1043
|
+
terminal: terminal, token: token, projectId: split.projectId, amount: amount
|
|
1044
|
+
});
|
|
1045
|
+
if (failureReason.length != 0) {
|
|
911
1046
|
emit SplitPayoutReverted({
|
|
912
|
-
projectId: projectId,
|
|
1047
|
+
projectId: projectId,
|
|
1048
|
+
split: split,
|
|
1049
|
+
amount: amount,
|
|
1050
|
+
reason: failureReason,
|
|
1051
|
+
caller: msg.sender
|
|
913
1052
|
});
|
|
914
|
-
return 0;
|
|
915
1053
|
}
|
|
1054
|
+
return sent;
|
|
916
1055
|
}
|
|
917
1056
|
} else {
|
|
918
1057
|
if (isNativeToken) {
|
|
@@ -933,25 +1072,25 @@ library JB721TiersHookLib {
|
|
|
933
1072
|
return 0;
|
|
934
1073
|
}
|
|
935
1074
|
} else {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1075
|
+
// Same allowance-delta pattern as the `preferAddToBalance` branch, for the `pay` entry point.
|
|
1076
|
+
bytes memory failureReason;
|
|
1077
|
+
(sent, failureReason) = _approveAndPay({
|
|
1078
|
+
terminal: terminal,
|
|
939
1079
|
token: token,
|
|
1080
|
+
projectId: split.projectId,
|
|
940
1081
|
amount: amount,
|
|
941
|
-
beneficiary: split.beneficiary
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
metadata: bytes("")
|
|
945
|
-
}) {
|
|
946
|
-
return amount;
|
|
947
|
-
} catch (bytes memory reason) {
|
|
948
|
-
// Reset approval on failure so tokens aren't left approved to the terminal.
|
|
949
|
-
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
1082
|
+
beneficiary: split.beneficiary
|
|
1083
|
+
});
|
|
1084
|
+
if (failureReason.length != 0) {
|
|
950
1085
|
emit SplitPayoutReverted({
|
|
951
|
-
projectId: projectId,
|
|
1086
|
+
projectId: projectId,
|
|
1087
|
+
split: split,
|
|
1088
|
+
amount: amount,
|
|
1089
|
+
reason: failureReason,
|
|
1090
|
+
caller: msg.sender
|
|
952
1091
|
});
|
|
953
|
-
return 0;
|
|
954
1092
|
}
|
|
1093
|
+
return sent;
|
|
955
1094
|
}
|
|
956
1095
|
}
|
|
957
1096
|
} else if (split.beneficiary != address(0)) {
|
|
@@ -12,7 +12,7 @@ import {JB721TierFlags} from "./JB721TierFlags.sol";
|
|
|
12
12
|
/// tier. With a `reserveFrequency` of 5, an extra NFT will be minted for the `reserveBeneficiary` for every 5 NFTs
|
|
13
13
|
/// purchased.
|
|
14
14
|
/// @custom:member reserveBeneficiary The address which receives any reserve NFTs from this tier.
|
|
15
|
-
/// @custom:member
|
|
15
|
+
/// @custom:member encodedIpfsUri The IPFS URI to use for each NFT in this tier.
|
|
16
16
|
/// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
|
|
17
17
|
/// @custom:member discountPercent The discount that should be applied to the tier.
|
|
18
18
|
/// @custom:member flags Boolean flags for this tier (allowOwnerMint, transfersPausable, cantBeRemoved,
|
|
@@ -29,7 +29,7 @@ struct JB721Tier {
|
|
|
29
29
|
uint104 votingUnits;
|
|
30
30
|
uint16 reserveFrequency;
|
|
31
31
|
address reserveBeneficiary;
|
|
32
|
-
bytes32
|
|
32
|
+
bytes32 encodedIpfsUri;
|
|
33
33
|
uint24 category;
|
|
34
34
|
uint8 discountPercent;
|
|
35
35
|
JB721TierFlags flags;
|
|
@@ -14,7 +14,7 @@ import {JB721TierConfigFlags} from "./JB721TierConfigFlags.sol";
|
|
|
14
14
|
/// purchased.
|
|
15
15
|
/// @custom:member reserveBeneficiary The address which receives any reserve NFTs from this tier. Overrides the default
|
|
16
16
|
/// reserve beneficiary if one is set.
|
|
17
|
-
/// @custom:member
|
|
17
|
+
/// @custom:member encodedIpfsUri The IPFS URI to use for each NFT in this tier.
|
|
18
18
|
/// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
|
|
19
19
|
/// @custom:member discountPercent The discount that should be applied to the tier.
|
|
20
20
|
/// @custom:member flags Boolean flags for this tier config (allowOwnerMint, useReserveBeneficiaryAsDefault,
|
|
@@ -29,7 +29,7 @@ struct JB721TierConfig {
|
|
|
29
29
|
uint32 votingUnits;
|
|
30
30
|
uint16 reserveFrequency;
|
|
31
31
|
address reserveBeneficiary;
|
|
32
|
-
bytes32
|
|
32
|
+
bytes32 encodedIpfsUri;
|
|
33
33
|
uint24 category;
|
|
34
34
|
uint8 discountPercent;
|
|
35
35
|
JB721TierConfigFlags flags;
|
|
@@ -24,8 +24,6 @@ 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.
|
|
29
27
|
/// @custom:member useDataHookForCashOuts A flag indicating if the data hook should be used for cash out transactions
|
|
30
28
|
/// during
|
|
31
29
|
/// this ruleset.
|
|
@@ -46,7 +44,6 @@ struct JBPayDataHookRulesetMetadata {
|
|
|
46
44
|
bool ownerMustSendPayouts;
|
|
47
45
|
bool holdFees;
|
|
48
46
|
bool scopeCashOutsToLocalBalances;
|
|
49
|
-
bool pauseCrossProjectFeeFreeInflows;
|
|
50
47
|
bool useDataHookForCashOut;
|
|
51
48
|
uint16 metadata;
|
|
52
49
|
}
|
|
@@ -162,7 +162,7 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor
|
|
|
162
162
|
votingUnits: storedTier.price,
|
|
163
163
|
reserveFrequency: storedTier.reserveFrequency,
|
|
164
164
|
reserveBeneficiary: reserveBeneficiaryOf(nft, currentSortIndex),
|
|
165
|
-
|
|
165
|
+
encodedIpfsUri: encodedIpfsUriOf[nft][currentSortIndex],
|
|
166
166
|
category: storedTier.category,
|
|
167
167
|
discountPercent: storedTier.discountPercent,
|
|
168
168
|
flags: JB721TierFlags({
|