@bananapus/721-hook-v6 0.0.42 → 0.0.43

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.
Files changed (78) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +60 -18
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +4 -1
  8. package/src/JB721TiersHookProjectDeployer.sol +68 -30
  9. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  10. package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
  11. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
  12. package/test/utils/AccessJBLib.sol +49 -0
  13. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  14. package/test/utils/TestBaseWorkflow.sol +213 -0
  15. package/test/utils/UnitTestSetup.sol +805 -0
  16. package/.gas-snapshot +0 -152
  17. package/ADMINISTRATION.md +0 -87
  18. package/ARCHITECTURE.md +0 -98
  19. package/AUDIT_INSTRUCTIONS.md +0 -77
  20. package/RISKS.md +0 -118
  21. package/SKILLS.md +0 -43
  22. package/STYLE_GUIDE.md +0 -610
  23. package/USER_JOURNEYS.md +0 -121
  24. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  25. package/slither-ci.config.json +0 -10
  26. package/test/721HookAttacks.t.sol +0 -408
  27. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  28. package/test/Fork.t.sol +0 -2346
  29. package/test/TestAuditGaps.sol +0 -1075
  30. package/test/TestCheckpoints.t.sol +0 -341
  31. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  32. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  33. package/test/audit/AuditRegressions.t.sol +0 -83
  34. package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
  35. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  36. package/test/audit/FreshAudit.t.sol +0 -197
  37. package/test/audit/FutureTierPoC.t.sol +0 -39
  38. package/test/audit/FutureTierRemoval.t.sol +0 -47
  39. package/test/audit/Pass12L18.t.sol +0 -80
  40. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  41. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  42. package/test/audit/RepoFindings.t.sol +0 -195
  43. package/test/audit/ReserveActivation.t.sol +0 -87
  44. package/test/audit/ReserveSlotProtection.t.sol +0 -273
  45. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  46. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  47. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  48. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  49. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  50. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  51. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  52. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  53. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  54. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  55. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  56. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  57. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  58. package/test/regression/CacheTierLookup.t.sol +0 -190
  59. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  60. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  61. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  62. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  63. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  64. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  65. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  66. package/test/unit/JBBitmap.t.sol +0 -170
  67. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  68. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  69. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  70. package/test/unit/deployer_Unit.t.sol +0 -114
  71. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  72. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  73. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  74. package/test/unit/pay_Unit.t.sol +0 -1661
  75. package/test/unit/redeem_Unit.t.sol +0 -473
  76. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  77. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  78. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
package/foundry.lock CHANGED
@@ -1,11 +1,5 @@
1
1
  {
2
2
  "lib/forge-std": {
3
3
  "rev": "77876f8a5b44b770a935621bb331660c90ac928e"
4
- },
5
- "lib/sphinx": {
6
- "branch": {
7
- "name": "v0.23.0",
8
- "rev": "5fb24a825f46bd6ae0b5359fe0da1d2346126b09"
9
- }
10
4
  }
11
- }
5
+ }
package/foundry.toml CHANGED
@@ -14,7 +14,7 @@ depth = 100
14
14
  fail_on_revert = false
15
15
 
16
16
  [lint]
17
- exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
17
+ exclude_lints = ["mixed-case-variable", "pascal-case-struct"]
18
18
  [fmt]
19
19
  number_underscore = "thousands"
20
20
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,11 +1,22 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Bananapus/nana-721-hook-v6"
8
8
  },
9
+ "files": [
10
+ "CHANGELOG.md",
11
+ "foundry.lock",
12
+ "foundry.toml",
13
+ "references/",
14
+ "remappings.txt",
15
+ "script/",
16
+ "sphinx.lock",
17
+ "src/",
18
+ "test/utils/"
19
+ ],
9
20
  "engines": {
10
21
  "node": ">=20.0.0"
11
22
  },
@@ -17,15 +28,15 @@
17
28
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
18
29
  },
19
30
  "dependencies": {
20
- "@bananapus/address-registry-v6": "^0.0.20",
21
- "@bananapus/core-v6": "^0.0.36",
22
- "@bananapus/ownable-v6": "^0.0.20",
23
- "@bananapus/permission-ids-v6": "^0.0.19",
24
- "@openzeppelin/contracts": "^5.6.1",
25
- "@prb/math": "^4.1.0",
26
- "solady": "^0.1.8"
31
+ "@bananapus/address-registry-v6": "0.0.25",
32
+ "@bananapus/core-v6": "^0.0.39",
33
+ "@bananapus/ownable-v6": "^0.0.24",
34
+ "@bananapus/permission-ids-v6": "0.0.22",
35
+ "@openzeppelin/contracts": "5.6.1",
36
+ "@prb/math": "4.1.1",
37
+ "solady": "0.1.26"
27
38
  },
28
39
  "devDependencies": {
29
- "@sphinx-labs/plugins": "^0.33.2"
40
+ "@sphinx-labs/plugins": "0.33.3"
30
41
  }
31
42
  }
@@ -87,12 +87,12 @@ contract DeployScript is Script, Sphinx {
87
87
  (address _deployer, bool _deployerIsDeployed) = _isDeployed({
88
88
  salt: CHECKPOINTS_DEPLOYER_SALT,
89
89
  creationCode: type(JB721CheckpointsDeployer).creationCode,
90
- arguments: ""
90
+ arguments: abi.encode(store)
91
91
  });
92
92
 
93
93
  // Deploy it if it has not been deployed yet.
94
94
  checkpointsDeployer = !_deployerIsDeployed
95
- ? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}()
95
+ ? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}(store)
96
96
  : JB721CheckpointsDeployer(_deployer);
97
97
  }
98
98
 
@@ -1,9 +1,12 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
- import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
5
4
  import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
5
+ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6
+ import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
7
+
6
8
  import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
9
+ import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
7
10
  import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
8
11
 
9
12
  /// @title JB721Checkpoints
@@ -14,6 +17,8 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
14
17
  /// (`_cachedThis`) is uninitialized on clones, so `domainSeparatorV4()` always rebuilds using the clone's
15
18
  /// `address(this)` — correct behavior, tiny gas overhead.
16
19
  contract JB721Checkpoints is Votes, IJB721Checkpoints {
20
+ using Checkpoints for Checkpoints.Trace160;
21
+
17
22
  //*********************************************************************//
18
23
  // --------------------------- custom errors ------------------------- //
19
24
  //*********************************************************************//
@@ -22,45 +27,51 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
22
27
  error JB721Checkpoints_Unauthorized();
23
28
 
24
29
  //*********************************************************************//
25
- // --------------------- private stored properties ------------------ //
30
+ // --------------- public immutable stored properties ---------------- //
26
31
  //*********************************************************************//
27
32
 
28
- /// @notice Whether this contract has been initialized.
29
- bool private _initialized;
33
+ /// @notice The store that holds tier and voting data for the hook's NFTs.
34
+ IJB721TiersHookStore public immutable override STORE;
30
35
 
31
36
  //*********************************************************************//
32
- // ---------------------- public stored properties ------------------- //
37
+ // --------------------- public stored properties -------------------- //
33
38
  //*********************************************************************//
34
39
 
35
40
  /// @notice The hook that this module tracks voting power for.
36
41
  address public override HOOK;
37
42
 
38
- /// @notice The store that holds tier and voting data for the hook's NFTs.
39
- IJB721TiersHookStore public override STORE;
43
+ //*********************************************************************//
44
+ // -------------------- internal stored properties ------------------- //
45
+ //*********************************************************************//
46
+
47
+ /// @notice Checkpointed token owners for historical reward eligibility after first transfer.
48
+ /// @custom:param tokenId The token ID to get historical owner checkpoints for.
49
+ mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
40
50
 
41
51
  //*********************************************************************//
42
52
  // -------------------------- constructor ---------------------------- //
43
53
  //*********************************************************************//
44
54
 
45
- /// @dev Parameterless. The implementation contract is initialized in the constructor to prevent direct use.
46
- /// Clones are initialized via `initialize()`.
47
- constructor() EIP712("JB721Checkpoints", "1") {
48
- _initialized = true;
55
+ /// @dev The implementation contract is initialized in the constructor to prevent direct use. Clones are initialized
56
+ /// via `initialize()`.
57
+ /// @param store The store that holds tier data for each hook's NFTs.
58
+ constructor(IJB721TiersHookStore store) EIP712("JB721Checkpoints", "1") {
59
+ STORE = store;
60
+ HOOK = address(1);
49
61
  }
50
62
 
51
63
  //*********************************************************************//
52
64
  // ---------------------- external transactions ---------------------- //
53
65
  //*********************************************************************//
54
66
 
55
- /// @notice Initializes a cloned module with its hook and store references.
67
+ /// @notice Initializes a cloned module with its hook reference.
56
68
  /// @dev Can only be called once. Called by the deployer after cloning.
57
69
  /// @param hook The hook this module serves.
58
- /// @param store The store that holds tier data for the hook's NFTs.
59
- function initialize(address hook, IJB721TiersHookStore store) external override {
60
- if (_initialized) revert JB721Checkpoints_AlreadyInitialized();
61
- _initialized = true;
70
+ function initialize(address hook) external override {
71
+ if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized();
72
+ // `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
73
+ // slither-disable-next-line missing-zero-check
62
74
  HOOK = hook;
63
- STORE = store;
64
75
  }
65
76
 
66
77
  /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
@@ -71,6 +82,12 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
71
82
  function onTransfer(address from, address to, uint256 tokenId) external override {
72
83
  if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
73
84
 
85
+ if (from != address(0)) {
86
+ // forge-lint: disable-next-line(unsafe-typecast)
87
+ // slither-disable-next-line unused-return
88
+ _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(to)});
89
+ }
90
+
74
91
  // Look up this token's tier to get its voting units.
75
92
  uint256 votingUnits = STORE.tierOfTokenId({hook: HOOK, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
76
93
 
@@ -79,7 +96,32 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
79
96
  }
80
97
 
81
98
  //*********************************************************************//
82
- // ------------------------ internal functions ----------------------- //
99
+ // ----------------------- external views ---------------------------- //
100
+ //*********************************************************************//
101
+
102
+ /// @notice The owner of an NFT at a past block.
103
+ /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
104
+ /// inferred from the hook's `firstOwnerOf`.
105
+ /// @param tokenId The token ID of the NFT to get the historical owner of.
106
+ /// @param blockNumber The block number to look up.
107
+ /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
108
+ function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
109
+ // forge-lint: disable-next-line(unsafe-typecast)
110
+ uint96 blockNumber96 = uint96(blockNumber);
111
+
112
+ Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
113
+ uint256 checkpointCount = checkpoints.length();
114
+
115
+ // Before the first transfer/burn checkpoint, the mint owner is implicit in the hook's first-owner tracking.
116
+ if (checkpointCount == 0 || checkpoints.at(0)._key > blockNumber96) {
117
+ return IJB721TiersHook(HOOK).firstOwnerOf(tokenId);
118
+ }
119
+
120
+ return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
121
+ }
122
+
123
+ //*********************************************************************//
124
+ // ----------------------- internal views ---------------------------- //
83
125
  //*********************************************************************//
84
126
 
85
127
  /// @notice Returns the total voting units held by an account (across all tiers).
@@ -2,6 +2,7 @@
2
2
  pragma solidity 0.8.28;
3
3
 
4
4
  import {LibClone} from "solady/src/utils/LibClone.sol";
5
+
5
6
  import {JB721Checkpoints} from "./JB721Checkpoints.sol";
6
7
  import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
7
8
  import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.sol";
@@ -19,12 +20,17 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
19
20
  /// @notice The checkpoint module implementation that clones delegate to.
20
21
  address public immutable override IMPLEMENTATION;
21
22
 
23
+ /// @notice The store that holds tier and voting data for each hook's NFTs.
24
+ IJB721TiersHookStore public immutable override STORE;
25
+
22
26
  //*********************************************************************//
23
27
  // -------------------------- constructor ---------------------------- //
24
28
  //*********************************************************************//
25
29
 
26
- constructor() {
27
- IMPLEMENTATION = address(new JB721Checkpoints());
30
+ /// @param store The store that holds tier data for each hook's NFTs.
31
+ constructor(IJB721TiersHookStore store) {
32
+ STORE = store;
33
+ IMPLEMENTATION = address(new JB721Checkpoints(store));
28
34
  }
29
35
 
30
36
  //*********************************************************************//
@@ -34,14 +40,13 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
34
40
  /// @notice Deploys a new deterministic checkpoint clone for the given hook.
35
41
  /// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
36
42
  /// @param hook The hook address the module will serve.
37
- /// @param store The store that holds tier data for the hook's NFTs.
38
43
  /// @return module The newly deployed and initialized checkpoint module.
39
- function deploy(address hook, IJB721TiersHookStore store) external override returns (IJB721Checkpoints module) {
44
+ function deploy(address hook) external override returns (IJB721Checkpoints module) {
40
45
  if (msg.sender != hook) revert JB721CheckpointsDeployer_Unauthorized();
41
46
 
42
47
  module = IJB721Checkpoints(
43
48
  LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
44
49
  );
45
- module.initialize({hook: hook, store: store});
50
+ module.initialize(hook);
46
51
  }
47
52
  }
@@ -656,6 +656,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
656
656
  if (tokenIds.length != 0) {
657
657
  // totalAmountPaid is the full amount available before recordMint deducted tier prices.
658
658
  uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
659
+ // slither-disable-next-line reentrancy-events
659
660
  _mintTokens({
660
661
  tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
661
662
  });
@@ -791,10 +792,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
791
792
 
792
793
  // Deploy the checkpoint module lazily on the first transfer.
793
794
  if (address(CHECKPOINTS) == address(0)) {
794
- CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
795
+ // slither-disable-next-line calls-loop,reentrancy-events
796
+ CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
795
797
  }
796
798
 
797
799
  // Notify the checkpoint module to update checkpointed voting power.
800
+ // slither-disable-next-line calls-loop,reentrancy-events
798
801
  CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
799
802
  }
800
803
  }
@@ -11,6 +11,7 @@ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadat
11
11
  import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
12
12
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
13
13
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
14
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
14
15
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
15
16
 
16
17
  import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
@@ -25,7 +26,12 @@ import {JBQueueRulesetsConfig} from "./structs/JBQueueRulesetsConfig.sol";
25
26
  /// @title JB721TiersHookProjectDeployer
26
27
  /// @notice Deploys a project and a 721 tiers hook for it. Can be used to queue rulesets for the project if given
27
28
  /// `JBPermissionIds.QUEUE_RULESETS` or `JBPermissionIds.LAUNCH_RULESETS`.
28
- contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721TiersHookProjectDeployer {
29
+ contract JB721TiersHookProjectDeployer is
30
+ ERC2771Context,
31
+ JBPermissioned,
32
+ IERC721Receiver,
33
+ IJB721TiersHookProjectDeployer
34
+ {
29
35
  //*********************************************************************//
30
36
  // --------------- public immutable stored properties ---------------- //
31
37
  //*********************************************************************//
@@ -82,8 +88,9 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
82
88
  override
83
89
  returns (uint256 projectId, IJB721TiersHook hook)
84
90
  {
85
- // Get the project's ID, optimistically knowing it will be one greater than the current number of projects.
86
- projectId = DIRECTORY.PROJECTS().count() + 1;
91
+ // Reserve the project ID up front so permissionless project creations cannot invalidate hook deployment.
92
+ IJBProjects PROJECTS = DIRECTORY.PROJECTS();
93
+ projectId = PROJECTS.createFor(address(this));
87
94
 
88
95
  // Deploy the hook.
89
96
  hook = HOOK_DEPLOYER.deployHookFor({
@@ -92,13 +99,16 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
92
99
  salt: salt == bytes32(0) ? bytes32(0) : keccak256(abi.encode(_msgSender(), salt))
93
100
  });
94
101
 
95
- // Launch the project.
102
+ // Launch the rulesets for the reserved project.
96
103
  _launchProjectFor({
97
- owner: owner, launchProjectConfig: launchProjectConfig, dataHook: hook, controller: controller
104
+ projectId: projectId, launchProjectConfig: launchProjectConfig, dataHook: hook, controller: controller
98
105
  });
99
106
 
100
107
  // Transfer the hook's ownership to the project.
101
108
  JBOwnable(address(hook)).transferOwnershipToProject(projectId);
109
+
110
+ // Transfer the project NFT to its intended owner.
111
+ PROJECTS.safeTransferFrom({from: address(this), to: owner, tokenId: projectId});
102
112
  }
103
113
 
104
114
  /// @notice Launches rulesets for a project with an attached 721 tiers hook.
@@ -108,6 +118,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
108
118
  /// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook which is being
109
119
  /// deployed.
110
120
  /// @param launchRulesetsConfig Configuration which dictates the project's new rulesets.
121
+ /// @param projectUri Metadata URI to associate with the project. Pass an empty string to leave it unchanged.
111
122
  /// @param controller The controller that the project's rulesets will be queued with.
112
123
  /// @param salt A salt to use for the deterministic deployment.
113
124
  /// @return rulesetId The ID of the successfully created ruleset.
@@ -116,6 +127,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
116
127
  uint256 projectId,
117
128
  JBDeploy721TiersHookConfig calldata deployTiersHookConfig,
118
129
  JBLaunchRulesetsConfig calldata launchRulesetsConfig,
130
+ string calldata projectUri,
119
131
  IJBController controller,
120
132
  bytes32 salt
121
133
  )
@@ -125,16 +137,23 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
125
137
  {
126
138
  // Get the project's projects contract.
127
139
  IJBProjects PROJECTS = DIRECTORY.PROJECTS();
140
+ address projectOwner = PROJECTS.ownerOf(projectId);
128
141
 
129
142
  // Enforce permissions.
130
143
  _requirePermissionFrom({
131
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.LAUNCH_RULESETS
144
+ account: projectOwner, projectId: projectId, permissionId: JBPermissionIds.LAUNCH_RULESETS
132
145
  });
133
146
 
134
147
  _requirePermissionFrom({
135
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TERMINALS
148
+ account: projectOwner, projectId: projectId, permissionId: JBPermissionIds.SET_TERMINALS
136
149
  });
137
150
 
151
+ if (bytes(projectUri).length != 0) {
152
+ _requirePermissionFrom({
153
+ account: projectOwner, projectId: projectId, permissionId: JBPermissionIds.SET_PROJECT_URI
154
+ });
155
+ }
156
+
138
157
  // Deploy the hook.
139
158
  hook = HOOK_DEPLOYER.deployHookFor({
140
159
  projectId: projectId,
@@ -147,7 +166,11 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
147
166
 
148
167
  // Launch the rulesets.
149
168
  rulesetId = _launchRulesetsFor({
150
- projectId: projectId, launchRulesetsConfig: launchRulesetsConfig, dataHook: hook, controller: controller
169
+ projectId: projectId,
170
+ launchRulesetsConfig: launchRulesetsConfig,
171
+ projectUri: projectUri,
172
+ dataHook: hook,
173
+ controller: controller
151
174
  });
152
175
  }
153
176
 
@@ -196,24 +219,13 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
196
219
  }
197
220
 
198
221
  //*********************************************************************//
199
- // -------------------------- internal views ------------------------- //
222
+ // ----------------------- external views ---------------------------- //
200
223
  //*********************************************************************//
201
224
 
202
- /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
203
- function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
204
- return ERC2771Context._contextSuffixLength();
205
- }
206
-
207
- /// @notice The calldata. Preferred to use over `msg.data`.
208
- /// @return calldata The `msg.data` of this call.
209
- function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
210
- return ERC2771Context._msgData();
211
- }
212
-
213
- /// @notice The message's sender. Preferred to use over `msg.sender`.
214
- /// @return sender The address which sent this call.
215
- function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
216
- return ERC2771Context._msgSender();
225
+ /// @notice Accepts project NFT reservations minted by `JBProjects.createFor`.
226
+ function onERC721Received(address, address from, uint256, bytes calldata) external view returns (bytes4) {
227
+ if (msg.sender != address(DIRECTORY.PROJECTS()) || from != address(0)) revert();
228
+ return IERC721Receiver.onERC721Received.selector;
217
229
  }
218
230
 
219
231
  //*********************************************************************//
@@ -221,12 +233,12 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
221
233
  //*********************************************************************//
222
234
 
223
235
  /// @notice Launches a project.
224
- /// @param owner The address that will own the project.
236
+ /// @param projectId The ID of the reserved project.
225
237
  /// @param launchProjectConfig Configuration which dictates the behavior of the project which is being launched.
226
238
  /// @param dataHook The data hook to use for the project.
227
239
  /// @param controller The controller that the project's rulesets will be queued with.
228
240
  function _launchProjectFor(
229
- address owner,
241
+ uint256 projectId,
230
242
  JBLaunchProjectConfig memory launchProjectConfig,
231
243
  IJB721TiersHook dataHook,
232
244
  IJBController controller
@@ -279,10 +291,10 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
279
291
  }
280
292
  }
281
293
 
282
- // Launch the project.
294
+ // Launch the rulesets for the reserved project.
283
295
  // slither-disable-next-line unused-return
284
- controller.launchProjectFor({
285
- owner: owner,
296
+ controller.launchRulesetsFor({
297
+ projectId: projectId,
286
298
  projectUri: launchProjectConfig.projectUri,
287
299
  rulesetConfigurations: rulesetConfigurations,
288
300
  terminalConfigurations: launchProjectConfig.terminalConfigurations,
@@ -293,12 +305,14 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
293
305
  /// @notice Launches rulesets for a project.
294
306
  /// @param projectId The ID of the project to launch rulesets for.
295
307
  /// @param launchRulesetsConfig Configuration which dictates the behavior of the project's rulesets.
308
+ /// @param projectUri Metadata URI to associate with the project. Pass an empty string to leave it unchanged.
296
309
  /// @param dataHook The data hook to use for the project.
297
310
  /// @param controller The controller that the project's rulesets will be queued with.
298
311
  /// @return rulesetId The ID of the successfully created ruleset.
299
312
  function _launchRulesetsFor(
300
313
  uint256 projectId,
301
314
  JBLaunchRulesetsConfig memory launchRulesetsConfig,
315
+ string memory projectUri,
302
316
  IJB721TiersHook dataHook,
303
317
  IJBController controller
304
318
  )
@@ -352,12 +366,15 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
352
366
  }
353
367
 
354
368
  // Launch the rulesets.
355
- return controller.launchRulesetsFor({
369
+ uint256 rulesetId = controller.launchRulesetsFor({
356
370
  projectId: projectId,
371
+ projectUri: projectUri,
357
372
  rulesetConfigurations: rulesetConfigurations,
358
373
  terminalConfigurations: launchRulesetsConfig.terminalConfigurations,
359
374
  memo: launchRulesetsConfig.memo
360
375
  });
376
+
377
+ return rulesetId;
361
378
  }
362
379
 
363
380
  /// @notice Queues rulesets for a project.
@@ -426,4 +443,25 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
426
443
  projectId: projectId, rulesetConfigurations: rulesetConfigurations, memo: queueRulesetsConfig.memo
427
444
  });
428
445
  }
446
+
447
+ //*********************************************************************//
448
+ // -------------------------- internal views ------------------------- //
449
+ //*********************************************************************//
450
+
451
+ /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
452
+ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
453
+ return ERC2771Context._contextSuffixLength();
454
+ }
455
+
456
+ /// @notice The calldata. Preferred to use over `msg.data`.
457
+ /// @return calldata The `msg.data` of this call.
458
+ function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
459
+ return ERC2771Context._msgData();
460
+ }
461
+
462
+ /// @notice The message's sender. Preferred to use over `msg.sender`.
463
+ /// @return sender The address which sent this call.
464
+ function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
465
+ return ERC2771Context._msgSender();
466
+ }
429
467
  }
@@ -8,27 +8,34 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
8
8
  /// @dev Deployed as a clone via JB721CheckpointsDeployer during hook initialization. One module per hook.
9
9
  /// Pass this address to JBTokenDistributor as the IVotes token.
10
10
  interface IJB721Checkpoints is IERC5805 {
11
- /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
12
- /// @dev Looks up the token's tier voting units from the store internally.
13
- /// Auto-self-delegates on first receive so checkpoints work without manual delegation.
14
- /// @param from The previous owner (address(0) on mint).
15
- /// @param to The new owner (address(0) on burn).
16
- /// @param tokenId The token ID being transferred (used to look up tier voting units).
17
- function onTransfer(address from, address to, uint256 tokenId) external;
18
-
19
- /// @notice Initializes a cloned module with its hook and store references.
20
- /// @dev Can only be called once. Called by the deployer after cloning.
21
- /// @param hook The hook this module serves.
22
- /// @param store The store that holds tier data for the hook's NFTs.
23
- function initialize(address hook, IJB721TiersHookStore store) external;
24
-
25
11
  /// @notice The hook that this module tracks voting power for.
26
12
  /// @return The hook address.
27
13
  // forge-lint: disable-next-line(mixed-case-function)
28
14
  function HOOK() external view returns (address);
29
15
 
16
+ /// @notice The owner of an NFT at a past block.
17
+ /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
18
+ /// inferred from the hook's `firstOwnerOf`.
19
+ /// @param tokenId The token ID of the NFT to get the historical owner of.
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.
22
+ function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address);
23
+
30
24
  /// @notice The store that holds tier and voting data for the hook's NFTs.
31
25
  /// @return The store contract.
32
26
  // forge-lint: disable-next-line(mixed-case-function)
33
27
  function STORE() external view returns (IJB721TiersHookStore);
28
+
29
+ /// @notice Initializes a cloned module with its hook reference.
30
+ /// @dev Can only be called once. Called by the deployer after cloning.
31
+ /// @param hook The hook this module serves.
32
+ function initialize(address hook) external;
33
+
34
+ /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
35
+ /// @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
+ /// @param from The previous owner (address(0) on mint).
38
+ /// @param to The new owner (address(0) on burn).
39
+ /// @param tokenId The token ID being transferred (used to look up tier voting units).
40
+ function onTransfer(address from, address to, uint256 tokenId) external;
34
41
  }
@@ -14,10 +14,14 @@ interface IJB721CheckpointsDeployer {
14
14
  // forge-lint: disable-next-line(mixed-case-function)
15
15
  function IMPLEMENTATION() external view returns (address);
16
16
 
17
+ /// @notice The store that holds tier and voting data for each hook's NFTs.
18
+ /// @return The store contract.
19
+ // forge-lint: disable-next-line(mixed-case-function)
20
+ function STORE() external view returns (IJB721TiersHookStore);
21
+
17
22
  /// @notice Deploys a new deterministic checkpoint clone for the given hook.
18
23
  /// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
19
24
  /// @param hook The hook address the module will serve.
20
- /// @param store The store that holds tier data for the hook's NFTs.
21
25
  /// @return module The newly deployed and initialized checkpoint module.
22
- function deploy(address hook, IJB721TiersHookStore store) external returns (IJB721Checkpoints module);
26
+ function deploy(address hook) external returns (IJB721Checkpoints module);
23
27
  }
@@ -43,6 +43,7 @@ interface IJB721TiersHookProjectDeployer {
43
43
  /// @param projectId The ID of the project that rulesets are being launched for.
44
44
  /// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook.
45
45
  /// @param launchRulesetsConfig Configuration which dictates the project's new rulesets.
46
+ /// @param projectUri Metadata URI to associate with the project. Pass an empty string to leave it unchanged.
46
47
  /// @param controller The controller that the project's rulesets will be queued with.
47
48
  /// @param salt A salt to use for the deterministic deployment.
48
49
  /// @return rulesetId The ID of the successfully created ruleset.
@@ -51,6 +52,7 @@ interface IJB721TiersHookProjectDeployer {
51
52
  uint256 projectId,
52
53
  JBDeploy721TiersHookConfig memory deployTiersHookConfig,
53
54
  JBLaunchRulesetsConfig memory launchRulesetsConfig,
55
+ string memory projectUri,
54
56
  IJBController controller,
55
57
  bytes32 salt
56
58
  )
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
+ import "@bananapus/core-v6/src/libraries/JBConstants.sol";
8
+
9
+ contract AccessJBLib {
10
+ // forge-lint: disable-next-line(mixed-case-function)
11
+ function NATIVE() external pure returns (uint256) {
12
+ return uint32(uint160(JBConstants.NATIVE_TOKEN));
13
+ }
14
+
15
+ // forge-lint: disable-next-line(mixed-case-function)
16
+ function USD() external pure returns (uint256) {
17
+ return JBCurrencyIds.USD;
18
+ }
19
+
20
+ // forge-lint: disable-next-line(mixed-case-function)
21
+ function NATIVE_TOKEN() external pure returns (address) {
22
+ return JBConstants.NATIVE_TOKEN;
23
+ }
24
+
25
+ // forge-lint: disable-next-line(mixed-case-function)
26
+ function MAX_FEE() external pure returns (uint256) {
27
+ return JBConstants.MAX_FEE;
28
+ }
29
+
30
+ // forge-lint: disable-next-line(mixed-case-function)
31
+ function MAX_RESERVED_PERCENT() external pure returns (uint256) {
32
+ return JBConstants.MAX_RESERVED_PERCENT;
33
+ }
34
+
35
+ // forge-lint: disable-next-line(mixed-case-function)
36
+ function MAX_CASH_OUT_TAX_RATE() external pure returns (uint256) {
37
+ return JBConstants.MAX_CASH_OUT_TAX_RATE;
38
+ }
39
+
40
+ // forge-lint: disable-next-line(mixed-case-function)
41
+ function MAX_WEIGHT_CUT_PERCENT() external pure returns (uint256) {
42
+ return JBConstants.MAX_WEIGHT_CUT_PERCENT;
43
+ }
44
+
45
+ // forge-lint: disable-next-line(mixed-case-function)
46
+ function SPLITS_TOTAL_PERCENT() external pure returns (uint256) {
47
+ return JBConstants.SPLITS_TOTAL_PERCENT;
48
+ }
49
+ }