@bananapus/721-hook-v6 0.0.5 → 0.0.7
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/JB721TiersHook.sol
CHANGED
|
@@ -31,7 +31,6 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
|
31
31
|
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
32
32
|
import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
|
|
33
33
|
import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
34
|
-
import {JBIpfsDecoder} from "./libraries/JBIpfsDecoder.sol";
|
|
35
34
|
import {JB721Tier} from "./structs/JB721Tier.sol";
|
|
36
35
|
import {JB721TierConfig} from "./structs/JB721TierConfig.sol";
|
|
37
36
|
import {JB721TiersSetDiscountPercentConfig} from "./structs/JB721TiersSetDiscountPercentConfig.sol";
|
|
@@ -350,16 +349,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
350
349
|
/// @return The token URI from the `tokenUriResolver` if it is set. If it isn't set, the token URI for the NFT's
|
|
351
350
|
/// tier.
|
|
352
351
|
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
|
|
353
|
-
|
|
354
|
-
IJB721TokenUriResolver resolver = STORE.tokenUriResolverOf(address(this));
|
|
355
|
-
|
|
356
|
-
// If a `tokenUriResolver` is set, use it to resolve the token URI.
|
|
357
|
-
if (address(resolver) != address(0)) return resolver.tokenUriOf({nft: address(this), tokenId: tokenId});
|
|
358
|
-
|
|
359
|
-
// Otherwise, return the token URI corresponding with the NFT's tier.
|
|
360
|
-
return JBIpfsDecoder.decode({
|
|
361
|
-
baseUri: baseURI, hexString: STORE.encodedTierIPFSUriOf({hook: address(this), tokenId: tokenId})
|
|
362
|
-
});
|
|
352
|
+
return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
|
|
363
353
|
}
|
|
364
354
|
|
|
365
355
|
//*********************************************************************//
|
|
@@ -721,9 +711,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
721
711
|
// If the payer is the beneficiary, combine their NFT credits with the amount paid.
|
|
722
712
|
uint256 unusedPayCredits;
|
|
723
713
|
if (context.payer == context.beneficiary) {
|
|
724
|
-
|
|
725
|
-
leftoverAmount += payCredits;
|
|
726
|
-
}
|
|
714
|
+
leftoverAmount += payCredits;
|
|
727
715
|
} else {
|
|
728
716
|
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
729
717
|
unusedPayCredits = payCredits;
|
|
@@ -765,28 +753,26 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
765
753
|
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
766
754
|
|
|
767
755
|
// Update NFT credits if they changed.
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if (newPayCredits
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
756
|
+
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
757
|
+
|
|
758
|
+
if (newPayCredits != payCredits) {
|
|
759
|
+
if (newPayCredits > payCredits) {
|
|
760
|
+
emit AddPayCredits({
|
|
761
|
+
amount: newPayCredits - payCredits,
|
|
762
|
+
newTotalCredits: newPayCredits,
|
|
763
|
+
account: context.beneficiary,
|
|
764
|
+
caller: _msgSender()
|
|
765
|
+
});
|
|
766
|
+
} else {
|
|
767
|
+
emit UsePayCredits({
|
|
768
|
+
amount: payCredits - newPayCredits,
|
|
769
|
+
newTotalCredits: newPayCredits,
|
|
770
|
+
account: context.beneficiary,
|
|
771
|
+
caller: _msgSender()
|
|
772
|
+
});
|
|
789
773
|
}
|
|
774
|
+
|
|
775
|
+
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
790
776
|
}
|
|
791
777
|
|
|
792
778
|
// Distribute any forwarded funds to tier split groups.
|
|
@@ -16,7 +16,9 @@ import {mulDiv} from "@prb/math/src/Common.sol";
|
|
|
16
16
|
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
17
17
|
|
|
18
18
|
import {IJB721TiersHookStore} from "../interfaces/IJB721TiersHookStore.sol";
|
|
19
|
+
import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol";
|
|
19
20
|
import {JB721TierConfig} from "../structs/JB721TierConfig.sol";
|
|
21
|
+
import {JBIpfsDecoder} from "./JBIpfsDecoder.sol";
|
|
20
22
|
|
|
21
23
|
/// @notice External library for JB721TiersHook operations extracted to stay within the EIP-170 contract size limit.
|
|
22
24
|
/// @dev Handles tier adjustments, split calculations, price normalization, and split fund distribution.
|
|
@@ -333,4 +335,34 @@ library JB721TiersHookLib {
|
|
|
333
335
|
terminal.pay(projectId, token, amount, beneficiary, 0, "", bytes(""));
|
|
334
336
|
}
|
|
335
337
|
}
|
|
338
|
+
|
|
339
|
+
/// @notice Resolves the token URI for a given NFT token ID.
|
|
340
|
+
/// @dev Extracted to the library to keep JBIpfsDecoder bytecode out of the hook contract (EIP-170 compliance).
|
|
341
|
+
/// @param store The 721 tiers hook store.
|
|
342
|
+
/// @param hook The hook address.
|
|
343
|
+
/// @param baseUri The base URI for IPFS-based token URIs.
|
|
344
|
+
/// @param tokenId The token ID to resolve the URI for.
|
|
345
|
+
/// @return The resolved token URI string.
|
|
346
|
+
function resolveTokenURI(
|
|
347
|
+
IJB721TiersHookStore store,
|
|
348
|
+
address hook,
|
|
349
|
+
string memory baseUri,
|
|
350
|
+
uint256 tokenId
|
|
351
|
+
)
|
|
352
|
+
external
|
|
353
|
+
view
|
|
354
|
+
returns (string memory)
|
|
355
|
+
{
|
|
356
|
+
// Get a reference to the `tokenUriResolver`.
|
|
357
|
+
IJB721TokenUriResolver resolver = store.tokenUriResolverOf(hook);
|
|
358
|
+
|
|
359
|
+
// If a `tokenUriResolver` is set, use it to resolve the token URI.
|
|
360
|
+
if (address(resolver) != address(0)) return resolver.tokenUriOf({nft: hook, tokenId: tokenId});
|
|
361
|
+
|
|
362
|
+
// Otherwise, return the token URI corresponding with the NFT's tier.
|
|
363
|
+
return
|
|
364
|
+
JBIpfsDecoder.decode({
|
|
365
|
+
baseUri: baseUri, hexString: store.encodedTierIPFSUriOf({hook: hook, tokenId: tokenId})
|
|
366
|
+
});
|
|
367
|
+
}
|
|
336
368
|
}
|
package/test/unit/pay_Unit.t.sol
CHANGED
|
@@ -1530,4 +1530,88 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup {
|
|
|
1530
1530
|
// Check: has the holder's balance returned to 0?
|
|
1531
1531
|
assertEq(hook.balanceOf(holder), 0);
|
|
1532
1532
|
}
|
|
1533
|
+
|
|
1534
|
+
function test_afterPayRecorded_revertOnCreditOverflow_samePayerBeneficiary() public {
|
|
1535
|
+
// Mock the directory call.
|
|
1536
|
+
mockAndExpect(
|
|
1537
|
+
address(mockJBDirectory),
|
|
1538
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
1539
|
+
abi.encode(true)
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
// Set the beneficiary's pay credits to max uint256.
|
|
1543
|
+
stdstore.target(address(hook)).sig("payCreditsOf(address)").with_key(beneficiary)
|
|
1544
|
+
.checked_write(type(uint256).max);
|
|
1545
|
+
|
|
1546
|
+
// Pay 1 wei where payer == beneficiary. No metadata → no NFT mints.
|
|
1547
|
+
// `leftoverAmount += payCredits` overflows: 1 + type(uint256).max.
|
|
1548
|
+
vm.expectRevert(stdError.arithmeticError);
|
|
1549
|
+
vm.prank(mockTerminalAddress);
|
|
1550
|
+
hook.afterPayRecordedWith(
|
|
1551
|
+
JBAfterPayRecordedContext({
|
|
1552
|
+
payer: beneficiary,
|
|
1553
|
+
projectId: projectId,
|
|
1554
|
+
rulesetId: 0,
|
|
1555
|
+
amount: JBTokenAmount({
|
|
1556
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1557
|
+
value: 1,
|
|
1558
|
+
decimals: 18,
|
|
1559
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1560
|
+
}),
|
|
1561
|
+
forwardedAmount: JBTokenAmount({
|
|
1562
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1563
|
+
value: 0,
|
|
1564
|
+
decimals: 18,
|
|
1565
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1566
|
+
}),
|
|
1567
|
+
weight: 10 ** 18,
|
|
1568
|
+
newlyIssuedTokenCount: 0,
|
|
1569
|
+
beneficiary: beneficiary,
|
|
1570
|
+
hookMetadata: new bytes(0),
|
|
1571
|
+
payerMetadata: new bytes(0)
|
|
1572
|
+
})
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function test_afterPayRecorded_revertOnCreditOverflow_differentPayerBeneficiary() public {
|
|
1577
|
+
// Mock the directory call.
|
|
1578
|
+
mockAndExpect(
|
|
1579
|
+
address(mockJBDirectory),
|
|
1580
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
1581
|
+
abi.encode(true)
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
// Set the beneficiary's pay credits to max uint256.
|
|
1585
|
+
stdstore.target(address(hook)).sig("payCreditsOf(address)").with_key(beneficiary)
|
|
1586
|
+
.checked_write(type(uint256).max);
|
|
1587
|
+
|
|
1588
|
+
// Pay 1 wei where payer != beneficiary. No metadata → no NFT mints, overspending allowed.
|
|
1589
|
+
// leftoverAmount=1, unusedPayCredits=type(uint256).max → overflow in `leftoverAmount + unusedPayCredits`.
|
|
1590
|
+
vm.expectRevert(stdError.arithmeticError);
|
|
1591
|
+
vm.prank(mockTerminalAddress);
|
|
1592
|
+
hook.afterPayRecordedWith(
|
|
1593
|
+
JBAfterPayRecordedContext({
|
|
1594
|
+
payer: address(0xdead),
|
|
1595
|
+
projectId: projectId,
|
|
1596
|
+
rulesetId: 0,
|
|
1597
|
+
amount: JBTokenAmount({
|
|
1598
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1599
|
+
value: 1,
|
|
1600
|
+
decimals: 18,
|
|
1601
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1602
|
+
}),
|
|
1603
|
+
forwardedAmount: JBTokenAmount({
|
|
1604
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1605
|
+
value: 0,
|
|
1606
|
+
decimals: 18,
|
|
1607
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1608
|
+
}),
|
|
1609
|
+
weight: 10 ** 18,
|
|
1610
|
+
newlyIssuedTokenCount: 0,
|
|
1611
|
+
beneficiary: beneficiary,
|
|
1612
|
+
hookMetadata: new bytes(0),
|
|
1613
|
+
payerMetadata: new bytes(0)
|
|
1614
|
+
})
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1533
1617
|
}
|