@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- // Get a reference to the `tokenUriResolver`.
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
- unchecked {
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
- unchecked {
769
- uint256 newPayCredits = leftoverAmount + unusedPayCredits;
770
-
771
- if (newPayCredits != payCredits) {
772
- if (newPayCredits > payCredits) {
773
- emit AddPayCredits({
774
- amount: newPayCredits - payCredits,
775
- newTotalCredits: newPayCredits,
776
- account: context.beneficiary,
777
- caller: _msgSender()
778
- });
779
- } else {
780
- emit UsePayCredits({
781
- amount: payCredits - newPayCredits,
782
- newTotalCredits: newPayCredits,
783
- account: context.beneficiary,
784
- caller: _msgSender()
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
  }
@@ -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
  }