@bananapus/721-hook-v6 0.0.41 → 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.
- package/foundry.lock +1 -7
- package/foundry.toml +1 -1
- package/package.json +20 -9
- package/script/Deploy.s.sol +2 -2
- package/src/JB721Checkpoints.sol +60 -18
- package/src/JB721CheckpointsDeployer.sol +10 -5
- package/src/JB721TiersHook.sol +4 -1
- package/src/JB721TiersHookProjectDeployer.sol +68 -30
- package/src/JB721TiersHookStore.sol +1 -4
- package/src/interfaces/IJB721Checkpoints.sol +21 -14
- package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
- package/test/utils/AccessJBLib.sol +49 -0
- package/test/utils/ForTest_JB721TiersHook.sol +246 -0
- package/test/utils/TestBaseWorkflow.sol +213 -0
- package/test/utils/UnitTestSetup.sol +805 -0
- package/.gas-snapshot +0 -152
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -98
- package/AUDIT_INSTRUCTIONS.md +0 -77
- package/RISKS.md +0 -118
- package/SKILLS.md +0 -43
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -121
- package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
- package/slither-ci.config.json +0 -10
- package/test/721HookAttacks.t.sol +0 -408
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
- package/test/Fork.t.sol +0 -2346
- package/test/TestAuditGaps.sol +0 -1075
- package/test/TestCheckpoints.t.sol +0 -341
- package/test/TestSafeTransferReentrancy.t.sol +0 -305
- package/test/TestVotingUnitsLifecycle.t.sol +0 -313
- package/test/audit/AuditRegressions.t.sol +0 -83
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
- package/test/audit/FreshAudit.t.sol +0 -197
- package/test/audit/FutureTierPoC.t.sol +0 -39
- package/test/audit/FutureTierRemoval.t.sol +0 -47
- package/test/audit/Pass12L18.t.sol +0 -80
- package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
- package/test/audit/ProjectDeployerAuth.t.sol +0 -266
- package/test/audit/RepoFindings.t.sol +0 -195
- package/test/audit/ReserveActivation.t.sol +0 -87
- package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
- package/test/audit/SplitCreditsMismatch.t.sol +0 -219
- package/test/audit/SplitFailureRedistribution.t.sol +0 -143
- package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
- package/test/fork/ERC20CashOutFork.t.sol +0 -633
- package/test/fork/ERC20TierSplitFork.t.sol +0 -596
- package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
- package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
- package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
- package/test/invariants/handlers/TierStoreHandler.sol +0 -165
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
- package/test/regression/CacheTierLookup.t.sol +0 -190
- package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
- package/test/regression/SplitDistributionBugs.t.sol +0 -751
- package/test/regression/SplitNoBeneficiary.t.sol +0 -140
- package/test/unit/AuditFixes_Unit.t.sol +0 -624
- package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
- package/test/unit/JBBitmap.t.sol +0 -170
- package/test/unit/JBIpfsDecoder.t.sol +0 -136
- package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
- package/test/unit/adjustTier_Unit.t.sol +0 -1942
- package/test/unit/deployer_Unit.t.sol +0 -114
- package/test/unit/getters_constructor_Unit.t.sol +0 -593
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
- package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
- package/test/unit/pay_Unit.t.sol +0 -1661
- package/test/unit/redeem_Unit.t.sol +0 -473
- package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
- package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
- package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
package/foundry.lock
CHANGED
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
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": "
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
22
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
23
|
-
"@bananapus/permission-ids-v6": "
|
|
24
|
-
"@openzeppelin/contracts": "
|
|
25
|
-
"@prb/math": "
|
|
26
|
-
"solady": "
|
|
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": "
|
|
40
|
+
"@sphinx-labs/plugins": "0.33.3"
|
|
30
41
|
}
|
|
31
42
|
}
|
package/script/Deploy.s.sol
CHANGED
|
@@ -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
|
|
package/src/JB721Checkpoints.sol
CHANGED
|
@@ -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
|
-
//
|
|
30
|
+
// --------------- public immutable stored properties ---------------- //
|
|
26
31
|
//*********************************************************************//
|
|
27
32
|
|
|
28
|
-
/// @notice
|
|
29
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
46
|
-
///
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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(
|
|
50
|
+
module.initialize(hook);
|
|
46
51
|
}
|
|
47
52
|
}
|
package/src/JB721TiersHook.sol
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
//
|
|
86
|
-
|
|
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
|
-
|
|
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:
|
|
144
|
+
account: projectOwner, projectId: projectId, permissionId: JBPermissionIds.LAUNCH_RULESETS
|
|
132
145
|
});
|
|
133
146
|
|
|
134
147
|
_requirePermissionFrom({
|
|
135
|
-
account:
|
|
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,
|
|
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
|
-
//
|
|
222
|
+
// ----------------------- external views ---------------------------- //
|
|
200
223
|
//*********************************************************************//
|
|
201
224
|
|
|
202
|
-
/// @
|
|
203
|
-
function
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
285
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -725,9 +725,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
725
725
|
|| reserveBeneficiaryOf({hook: hook, tierId: tierId}) == address(0)
|
|
726
726
|
) return 0;
|
|
727
727
|
|
|
728
|
-
// A sold-out tier cannot have mintable pending reserves — minting would underflow remainingSupply.
|
|
729
|
-
if (storedTier.remainingSupply == 0) return 0;
|
|
730
|
-
|
|
731
728
|
// The number of reserve NFTs which have already been minted from the tier.
|
|
732
729
|
uint256 numberOfReserveMints = numberOfReservesMintedFor[hook][tierId];
|
|
733
730
|
|
|
@@ -1232,7 +1229,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1232
1229
|
if (storedTier.remainingSupply == 0) revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
|
|
1233
1230
|
|
|
1234
1231
|
// Mint the 721 — decrement remaining supply first so the reserve check below
|
|
1235
|
-
// sees the post-mint
|
|
1232
|
+
// sees the correct post-mint non-reserve-mint count.
|
|
1236
1233
|
unchecked {
|
|
1237
1234
|
// Keep a reference to its token ID.
|
|
1238
1235
|
tokenIds[i] = _generateTokenId({
|
|
@@ -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
|
|
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
|
)
|