@bananapus/721-hook-v6 0.0.32 → 0.0.34
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/USER_JOURNEYS.md +11 -0
- package/package.json +3 -3
- package/script/Deploy.s.sol +53 -19
- package/src/JB721Checkpoints.sol +92 -0
- package/src/JB721CheckpointsDeployer.sol +45 -0
- package/src/JB721TiersHook.sol +90 -116
- package/src/abstract/JB721Hook.sol +5 -0
- package/src/interfaces/IJB721Checkpoints.sol +34 -0
- package/src/interfaces/IJB721CheckpointsDeployer.sol +20 -0
- package/src/interfaces/IJB721TiersHook.sol +8 -0
- package/src/libraries/JB721Constants.sol +6 -0
- package/src/libraries/JB721TiersHookLib.sol +353 -146
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +11 -1
- package/test/Fork.t.sol +11 -2
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestCheckpoints.t.sol +329 -0
- package/test/audit/CodexNemesisRepoFindings.t.sol +270 -0
- package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +161 -0
- package/test/audit/CodexSplitCreditsMismatch.t.sol +2 -1
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +1 -0
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +249 -0
- package/test/audit/SplitFailureRedistribution.t.sol +2 -1
- package/test/fork/ERC20CashOutFork.t.sol +11 -2
- package/test/fork/ERC20TierSplitFork.t.sol +11 -2
- package/test/fork/IssueTokensForSplitsFork.t.sol +11 -2
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +2 -2
- package/test/regression/SplitDistributionBugs.t.sol +5 -5
- package/test/regression/SplitNoBeneficiary.t.sol +1 -1
- package/test/unit/AuditFixes_Unit.t.sol +5 -5
- package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -0
- package/test/unit/pay_Unit.t.sol +1 -0
- package/test/unit/redeem_Unit.t.sol +3 -3
- package/test/unit/relayBeneficiary_Unit.t.sol +182 -0
- package/test/unit/splitHookDistribution_Unit.t.sol +6 -6
- package/test/unit/tierSplitRouting_Unit.t.sol +2 -2
package/USER_JOURNEYS.md
CHANGED
|
@@ -81,6 +81,17 @@
|
|
|
81
81
|
2. Keep collection-specific behavior in the downstream repo while leaving pay, reserve, and cash-out semantics in this repo.
|
|
82
82
|
3. Audit hook-store interactions here first, then audit the downstream resolver or wrapper.
|
|
83
83
|
|
|
84
|
+
## Journey 7: Mint NFTs To The Correct Beneficiary During Cross-Chain Payments
|
|
85
|
+
|
|
86
|
+
**Starting state:** a sucker pays the project on behalf of a remote user via `payRemote`, and the 721 hook needs to mint NFTs and accrue credits to the real user instead of the sucker contract.
|
|
87
|
+
|
|
88
|
+
**Success:** NFTs mint to and pay credits accrue to the real remote user.
|
|
89
|
+
|
|
90
|
+
**Flow**
|
|
91
|
+
1. The sucker calls `terminal.pay()` with itself as both payer and beneficiary, embedding the real user's address in the `JB_RELAY_BENEFICIARY` metadata key.
|
|
92
|
+
2. `_mintAndUpdateCredits` detects that `payer == beneficiary` and finds relay-beneficiary metadata.
|
|
93
|
+
3. All NFT minting and credit accounting uses the resolved relay beneficiary instead of the sucker address.
|
|
94
|
+
|
|
84
95
|
## Hand-Offs
|
|
85
96
|
|
|
86
97
|
- Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for the treasury, ruleset, and permission surfaces the hook plugs into.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@bananapus/address-registry-v6": "^0.0.17",
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
21
|
+
"@bananapus/core-v6": "^0.0.34",
|
|
22
22
|
"@bananapus/ownable-v6": "^0.0.17",
|
|
23
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.17",
|
|
24
24
|
"@openzeppelin/contracts": "^5.6.1",
|
|
25
25
|
"@prb/math": "^4.1.0",
|
|
26
26
|
"solady": "^0.1.8"
|
package/script/Deploy.s.sol
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
|
|
11
11
|
import {Script} from "forge-std/Script.sol";
|
|
12
12
|
|
|
13
|
+
import {JB721CheckpointsDeployer} from "../src/JB721CheckpointsDeployer.sol";
|
|
14
|
+
import {IJB721CheckpointsDeployer} from "../src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
13
15
|
import {JB721TiersHookDeployer} from "../src/JB721TiersHookDeployer.sol";
|
|
14
16
|
import {JB721TiersHookProjectDeployer} from "../src/JB721TiersHookProjectDeployer.sol";
|
|
15
17
|
import {JB721TiersHookStore} from "../src/JB721TiersHookStore.sol";
|
|
@@ -34,6 +36,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
34
36
|
bytes32 HOOK_STORE_SALT = "JB721TiersHookStoreV6_";
|
|
35
37
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
36
38
|
bytes32 PROJECT_DEPLOYER_SALT = "JB721TiersHookProjectDeployerV6";
|
|
39
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
40
|
+
bytes32 CHECKPOINTS_DEPLOYER_SALT = "JB721CheckpointsDeployerV6";
|
|
37
41
|
|
|
38
42
|
function configureSphinx() public override {
|
|
39
43
|
sphinxConfig.projectName = "nana-721-hook-v6";
|
|
@@ -69,28 +73,58 @@ contract DeployScript is Script, Sphinx {
|
|
|
69
73
|
JB721TiersHookStore store;
|
|
70
74
|
{
|
|
71
75
|
// Perform the check for the store.
|
|
72
|
-
(address _store, bool _storeIsDeployed) =
|
|
73
|
-
|
|
76
|
+
(address _store, bool _storeIsDeployed) = _isDeployed({
|
|
77
|
+
salt: HOOK_STORE_SALT, creationCode: type(JB721TiersHookStore).creationCode, arguments: ""
|
|
78
|
+
});
|
|
74
79
|
|
|
75
80
|
// Deploy it if it has not been deployed yet.
|
|
76
81
|
store = !_storeIsDeployed ? new JB721TiersHookStore{salt: HOOK_STORE_SALT}() : JB721TiersHookStore(_store);
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
JB721CheckpointsDeployer checkpointsDeployer;
|
|
85
|
+
{
|
|
86
|
+
// Perform the check for the deployer.
|
|
87
|
+
(address _deployer, bool _deployerIsDeployed) = _isDeployed({
|
|
88
|
+
salt: CHECKPOINTS_DEPLOYER_SALT,
|
|
89
|
+
creationCode: type(JB721CheckpointsDeployer).creationCode,
|
|
90
|
+
arguments: ""
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Deploy it if it has not been deployed yet.
|
|
94
|
+
checkpointsDeployer = !_deployerIsDeployed
|
|
95
|
+
? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}()
|
|
96
|
+
: JB721CheckpointsDeployer(_deployer);
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
JB721TiersHook hook;
|
|
80
100
|
{
|
|
81
101
|
// Perform the check for the registry.
|
|
82
|
-
(address _hook, bool _hookIsDeployed) = _isDeployed(
|
|
83
|
-
HOOK_SALT,
|
|
84
|
-
type(JB721TiersHook).creationCode,
|
|
85
|
-
abi.encode(
|
|
86
|
-
core.directory,
|
|
102
|
+
(address _hook, bool _hookIsDeployed) = _isDeployed({
|
|
103
|
+
salt: HOOK_SALT,
|
|
104
|
+
creationCode: type(JB721TiersHook).creationCode,
|
|
105
|
+
arguments: abi.encode(
|
|
106
|
+
core.directory,
|
|
107
|
+
core.permissions,
|
|
108
|
+
core.prices,
|
|
109
|
+
core.rulesets,
|
|
110
|
+
store,
|
|
111
|
+
core.splits,
|
|
112
|
+
checkpointsDeployer,
|
|
113
|
+
TRUSTED_FORWARDER
|
|
87
114
|
)
|
|
88
|
-
);
|
|
115
|
+
});
|
|
89
116
|
|
|
90
117
|
// Deploy it if it has not been deployed yet.
|
|
91
118
|
hook = !_hookIsDeployed
|
|
92
119
|
? new JB721TiersHook{salt: HOOK_SALT}(
|
|
93
|
-
core.directory,
|
|
120
|
+
core.directory,
|
|
121
|
+
core.permissions,
|
|
122
|
+
core.prices,
|
|
123
|
+
core.rulesets,
|
|
124
|
+
store,
|
|
125
|
+
core.splits,
|
|
126
|
+
IJB721CheckpointsDeployer(address(checkpointsDeployer)),
|
|
127
|
+
TRUSTED_FORWARDER
|
|
94
128
|
)
|
|
95
129
|
: JB721TiersHook(_hook);
|
|
96
130
|
}
|
|
@@ -98,11 +132,11 @@ contract DeployScript is Script, Sphinx {
|
|
|
98
132
|
JB721TiersHookDeployer hookDeployer;
|
|
99
133
|
{
|
|
100
134
|
// Perform the check for the registry.
|
|
101
|
-
(address _hookDeployer, bool _hookDeployerIsDeployed) = _isDeployed(
|
|
102
|
-
HOOK_DEPLOYER_SALT,
|
|
103
|
-
type(JB721TiersHookDeployer).creationCode,
|
|
104
|
-
abi.encode(hook, store, registry.registry, TRUSTED_FORWARDER)
|
|
105
|
-
);
|
|
135
|
+
(address _hookDeployer, bool _hookDeployerIsDeployed) = _isDeployed({
|
|
136
|
+
salt: HOOK_DEPLOYER_SALT,
|
|
137
|
+
creationCode: type(JB721TiersHookDeployer).creationCode,
|
|
138
|
+
arguments: abi.encode(hook, store, registry.registry, TRUSTED_FORWARDER)
|
|
139
|
+
});
|
|
106
140
|
|
|
107
141
|
hookDeployer = !_hookDeployerIsDeployed
|
|
108
142
|
? new JB721TiersHookDeployer{salt: HOOK_DEPLOYER_SALT}(
|
|
@@ -114,11 +148,11 @@ contract DeployScript is Script, Sphinx {
|
|
|
114
148
|
JB721TiersHookProjectDeployer projectDeployer;
|
|
115
149
|
{
|
|
116
150
|
// Perform the check for the registry.
|
|
117
|
-
(address _projectDeployer, bool _projectDeployerIsdeployed) = _isDeployed(
|
|
118
|
-
PROJECT_DEPLOYER_SALT,
|
|
119
|
-
type(JB721TiersHookProjectDeployer).creationCode,
|
|
120
|
-
abi.encode(core.directory, core.permissions, hookDeployer, TRUSTED_FORWARDER)
|
|
121
|
-
);
|
|
151
|
+
(address _projectDeployer, bool _projectDeployerIsdeployed) = _isDeployed({
|
|
152
|
+
salt: PROJECT_DEPLOYER_SALT,
|
|
153
|
+
creationCode: type(JB721TiersHookProjectDeployer).creationCode,
|
|
154
|
+
arguments: abi.encode(core.directory, core.permissions, hookDeployer, TRUSTED_FORWARDER)
|
|
155
|
+
});
|
|
122
156
|
|
|
123
157
|
projectDeployer = !_projectDeployerIsdeployed
|
|
124
158
|
? new JB721TiersHookProjectDeployer{salt: PROJECT_DEPLOYER_SALT}(
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
5
|
+
import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
|
|
6
|
+
import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
|
|
7
|
+
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
8
|
+
|
|
9
|
+
/// @title JB721Checkpoints
|
|
10
|
+
/// @notice Provides IVotes-compatible checkpointed voting power for a JB721TiersHook. Deployed as an EIP-1167 clone
|
|
11
|
+
/// via JB721CheckpointsDeployer — one module per hook. The hook calls `onTransfer` on every NFT transfer to
|
|
12
|
+
/// maintain accurate vote checkpoints.
|
|
13
|
+
/// @dev EIP712 on clones: OZ stores name/version as immutables (accessible via DELEGATECALL). The storage cache
|
|
14
|
+
/// (`_cachedThis`) is uninitialized on clones, so `domainSeparatorV4()` always rebuilds using the clone's
|
|
15
|
+
/// `address(this)` — correct behavior, tiny gas overhead.
|
|
16
|
+
contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
17
|
+
//*********************************************************************//
|
|
18
|
+
// --------------------------- custom errors ------------------------- //
|
|
19
|
+
//*********************************************************************//
|
|
20
|
+
|
|
21
|
+
error JB721Checkpoints_AlreadyInitialized();
|
|
22
|
+
error JB721Checkpoints_Unauthorized();
|
|
23
|
+
|
|
24
|
+
//*********************************************************************//
|
|
25
|
+
// --------------------- private stored properties ------------------ //
|
|
26
|
+
//*********************************************************************//
|
|
27
|
+
|
|
28
|
+
/// @notice Whether this contract has been initialized.
|
|
29
|
+
bool private _initialized;
|
|
30
|
+
|
|
31
|
+
//*********************************************************************//
|
|
32
|
+
// ---------------------- public stored properties ------------------- //
|
|
33
|
+
//*********************************************************************//
|
|
34
|
+
|
|
35
|
+
/// @notice The hook that this module tracks voting power for.
|
|
36
|
+
address public override HOOK;
|
|
37
|
+
|
|
38
|
+
/// @notice The store that holds tier and voting data for the hook's NFTs.
|
|
39
|
+
IJB721TiersHookStore public override STORE;
|
|
40
|
+
|
|
41
|
+
//*********************************************************************//
|
|
42
|
+
// -------------------------- constructor ---------------------------- //
|
|
43
|
+
//*********************************************************************//
|
|
44
|
+
|
|
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;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//*********************************************************************//
|
|
52
|
+
// ---------------------- external transactions ---------------------- //
|
|
53
|
+
//*********************************************************************//
|
|
54
|
+
|
|
55
|
+
/// @notice Initializes a cloned module with its hook and store references.
|
|
56
|
+
/// @dev Can only be called once. Called by the deployer after cloning.
|
|
57
|
+
/// @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;
|
|
62
|
+
HOOK = hook;
|
|
63
|
+
STORE = store;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
|
|
67
|
+
/// @dev Only callable by the HOOK. Looks up the token's tier voting units from the store.
|
|
68
|
+
/// @param from The previous owner (address(0) on mint).
|
|
69
|
+
/// @param to The new owner (address(0) on burn).
|
|
70
|
+
/// @param tokenId The token ID being transferred.
|
|
71
|
+
function onTransfer(address from, address to, uint256 tokenId) external override {
|
|
72
|
+
if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
|
|
73
|
+
|
|
74
|
+
// Look up this token's tier to get its voting units.
|
|
75
|
+
uint256 votingUnits = STORE.tierOfTokenId({hook: HOOK, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
76
|
+
|
|
77
|
+
// Move checkpointed voting power from the previous owner to the new owner.
|
|
78
|
+
_transferVotingUnits({from: from, to: to, amount: votingUnits});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//*********************************************************************//
|
|
82
|
+
// ------------------------ internal functions ----------------------- //
|
|
83
|
+
//*********************************************************************//
|
|
84
|
+
|
|
85
|
+
/// @notice Returns the total voting units held by an account (across all tiers).
|
|
86
|
+
/// @dev Called by OZ Votes when re-delegating to compute the account's total voting units.
|
|
87
|
+
/// @param account The address to get the voting units of.
|
|
88
|
+
/// @return The total voting units the account holds.
|
|
89
|
+
function _getVotingUnits(address account) internal view override returns (uint256) {
|
|
90
|
+
return STORE.votingUnitsOf({hook: HOOK, account: account});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {LibClone} from "solady/src/utils/LibClone.sol";
|
|
5
|
+
import {JB721Checkpoints} from "./JB721Checkpoints.sol";
|
|
6
|
+
import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
|
|
7
|
+
import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.sol";
|
|
8
|
+
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
9
|
+
|
|
10
|
+
/// @title JB721CheckpointsDeployer
|
|
11
|
+
/// @notice Deploys EIP-1167 clones of JB721Checkpoints for each JB721TiersHook instance.
|
|
12
|
+
/// @dev The implementation is deployed once in the constructor. Each `deploy()` call clones it (~45k gas) and
|
|
13
|
+
/// initializes the clone with the hook and store references.
|
|
14
|
+
contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
|
|
15
|
+
//*********************************************************************//
|
|
16
|
+
// --------------- public immutable stored properties ---------------- //
|
|
17
|
+
//*********************************************************************//
|
|
18
|
+
|
|
19
|
+
/// @notice The checkpoint module implementation that clones delegate to.
|
|
20
|
+
address public immutable override IMPLEMENTATION;
|
|
21
|
+
|
|
22
|
+
//*********************************************************************//
|
|
23
|
+
// -------------------------- constructor ---------------------------- //
|
|
24
|
+
//*********************************************************************//
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
IMPLEMENTATION = address(new JB721Checkpoints());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//*********************************************************************//
|
|
31
|
+
// ---------------------- external transactions ---------------------- //
|
|
32
|
+
//*********************************************************************//
|
|
33
|
+
|
|
34
|
+
/// @notice Deploys a new deterministic checkpoint clone for the given hook.
|
|
35
|
+
/// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
|
|
36
|
+
/// @param hook The hook address the module will serve.
|
|
37
|
+
/// @param store The store that holds tier data for the hook's NFTs.
|
|
38
|
+
/// @return module The newly deployed and initialized checkpoint module.
|
|
39
|
+
function deploy(address hook, IJB721TiersHookStore store) external override returns (IJB721Checkpoints module) {
|
|
40
|
+
module = IJB721Checkpoints(
|
|
41
|
+
LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
|
|
42
|
+
);
|
|
43
|
+
module.initialize({hook: hook, store: store});
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/JB721TiersHook.sol
CHANGED
|
@@ -19,9 +19,12 @@ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"
|
|
|
19
19
|
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
20
20
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
21
21
|
import {JB721Hook} from "./abstract/JB721Hook.sol";
|
|
22
|
+
import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
|
|
23
|
+
import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.sol";
|
|
22
24
|
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
23
25
|
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
24
26
|
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
27
|
+
import {JB721Constants} from "./libraries/JB721Constants.sol";
|
|
25
28
|
import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
|
|
26
29
|
import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
27
30
|
import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
|
|
@@ -42,7 +45,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
42
45
|
|
|
43
46
|
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
44
47
|
error JB721TiersHook_CantBuyWithCredits();
|
|
45
|
-
error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
|
|
46
48
|
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
47
49
|
error JB721TiersHook_MintReserveNftsPaused();
|
|
48
50
|
error JB721TiersHook_NoProjectId();
|
|
@@ -65,6 +67,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
65
67
|
/// @notice The contract that stores and manages splits.
|
|
66
68
|
IJBSplits public immutable override SPLITS;
|
|
67
69
|
|
|
70
|
+
/// @notice The deployer used to deploy checkpoint module clones during initialization.
|
|
71
|
+
IJB721CheckpointsDeployer internal immutable CHECKPOINTS_DEPLOYER;
|
|
72
|
+
|
|
68
73
|
//*********************************************************************//
|
|
69
74
|
// --------------------- private stored properties ------------------ //
|
|
70
75
|
//*********************************************************************//
|
|
@@ -104,6 +109,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
104
109
|
/// - pricing decimals in bits 32-39 (8 bits).
|
|
105
110
|
uint256 internal _packedPricingContext;
|
|
106
111
|
|
|
112
|
+
/// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
|
|
113
|
+
/// @dev Set once during `initialize()`. Pass this to JBTokenDistributor as the IVotes token.
|
|
114
|
+
IJB721Checkpoints public override CHECKPOINTS;
|
|
115
|
+
|
|
107
116
|
//*********************************************************************//
|
|
108
117
|
// -------------------------- constructor ---------------------------- //
|
|
109
118
|
//*********************************************************************//
|
|
@@ -114,6 +123,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
114
123
|
/// @param rulesets A contract storing and managing project rulesets.
|
|
115
124
|
/// @param store The contract which stores the NFT's data.
|
|
116
125
|
/// @param splits The contract that stores and manages splits.
|
|
126
|
+
/// @param checkpointsDeployer The deployer used to deploy checkpoint module clones during initialization.
|
|
117
127
|
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
118
128
|
constructor(
|
|
119
129
|
IJBDirectory directory,
|
|
@@ -122,6 +132,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
122
132
|
IJBRulesets rulesets,
|
|
123
133
|
IJB721TiersHookStore store,
|
|
124
134
|
IJBSplits splits,
|
|
135
|
+
IJB721CheckpointsDeployer checkpointsDeployer,
|
|
125
136
|
address trustedForwarder
|
|
126
137
|
)
|
|
127
138
|
JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
|
|
@@ -132,6 +143,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
132
143
|
RULESETS = rulesets;
|
|
133
144
|
STORE = store;
|
|
134
145
|
SPLITS = splits;
|
|
146
|
+
CHECKPOINTS_DEPLOYER = checkpointsDeployer;
|
|
135
147
|
|
|
136
148
|
// Prevent the implementation contract from being initialized.
|
|
137
149
|
_initialized = true;
|
|
@@ -146,14 +158,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
146
158
|
/// @param tokenId The token ID of the NFT to get the first owner of.
|
|
147
159
|
/// @return The address of the NFT's first owner.
|
|
148
160
|
function firstOwnerOf(uint256 tokenId) external view override returns (address) {
|
|
149
|
-
|
|
150
|
-
address
|
|
151
|
-
|
|
152
|
-
// If the stored first owner is set, return it.
|
|
153
|
-
if (storedFirstOwner != address(0)) return storedFirstOwner;
|
|
154
|
-
|
|
155
|
-
// Otherwise, the first owner must be the current owner.
|
|
156
|
-
return _ownerOf(tokenId);
|
|
161
|
+
address first = _firstOwnerOf[tokenId];
|
|
162
|
+
return first != address(0) ? first : _ownerOf(tokenId);
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
/// @notice Context for the pricing of this hook's tiers.
|
|
@@ -196,37 +202,24 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
196
202
|
{
|
|
197
203
|
hookSpecifications = new JBPayHookSpecification[](1);
|
|
198
204
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Convert split amounts from tier pricing to payment token denomination (if currencies differ)
|
|
205
|
-
// and cap at the actual payment value so the terminal never forwards more than was paid.
|
|
206
|
-
if (totalSplitAmount != 0) {
|
|
207
|
-
(totalSplitAmount, splitMetadata) = JB721TiersHookLib.convertAndCapSplitAmounts({
|
|
208
|
-
totalSplitAmount: totalSplitAmount,
|
|
209
|
-
splitMetadata: splitMetadata,
|
|
210
|
-
packedPricingContext: _packedPricingContext,
|
|
211
|
-
prices: PRICES,
|
|
212
|
-
projectId: context.projectId,
|
|
213
|
-
amountCurrency: context.amount.currency,
|
|
214
|
-
amountDecimals: context.amount.decimals,
|
|
215
|
-
amountValue: context.amount.value
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Adjust weight so the terminal mints tokens only for the amount that actually enters the project.
|
|
220
|
-
weight = JB721TiersHookLib.calculateWeight({
|
|
221
|
-
contextWeight: context.weight,
|
|
222
|
-
amountValue: context.amount.value,
|
|
223
|
-
totalSplitAmount: totalSplitAmount,
|
|
205
|
+
// Compute split amounts, adjusted weight, and resolved beneficiary in a single library call.
|
|
206
|
+
uint256 totalSplitAmount;
|
|
207
|
+
bytes memory splitMetadata;
|
|
208
|
+
address beneficiary;
|
|
209
|
+
(weight, totalSplitAmount, splitMetadata, beneficiary) = JB721TiersHookLib.computeSplitsAndWeight({
|
|
224
210
|
store: STORE,
|
|
225
|
-
|
|
211
|
+
metadataIdTarget: METADATA_ID_TARGET,
|
|
212
|
+
packedPricingContext: _packedPricingContext,
|
|
213
|
+
prices: PRICES,
|
|
214
|
+
context: context
|
|
226
215
|
});
|
|
227
216
|
|
|
228
|
-
hookSpecifications[0] =
|
|
229
|
-
|
|
217
|
+
hookSpecifications[0] = JBPayHookSpecification({
|
|
218
|
+
hook: this,
|
|
219
|
+
noop: false,
|
|
220
|
+
amount: totalSplitAmount,
|
|
221
|
+
metadata: abi.encode(beneficiary, context.payer, splitMetadata)
|
|
222
|
+
});
|
|
230
223
|
}
|
|
231
224
|
|
|
232
225
|
/// @notice The combined cash out weight of the NFTs with the specified token IDs.
|
|
@@ -314,6 +307,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
314
307
|
|| flags.preventOverspending || flags.issueTokensForSplits
|
|
315
308
|
) STORE.recordFlags(flags);
|
|
316
309
|
|
|
310
|
+
// Deploy the checkpoint module for IVotes-compatible voting power.
|
|
311
|
+
CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
|
|
312
|
+
|
|
317
313
|
// Transfer ownership to the initializer.
|
|
318
314
|
_transferOwnership(_msgSender());
|
|
319
315
|
}
|
|
@@ -321,7 +317,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
321
317
|
/// @notice Indicates if this contract adheres to the specified interface.
|
|
322
318
|
/// @dev See {IERC165-supportsInterface}.
|
|
323
319
|
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
324
|
-
function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
|
|
320
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, JB721Hook) returns (bool) {
|
|
325
321
|
return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
|
|
326
322
|
}
|
|
327
323
|
|
|
@@ -579,13 +575,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
579
575
|
|
|
580
576
|
/// @notice Returns the calldata, preferred to use over `msg.data`
|
|
581
577
|
/// @return calldata the `msg.data` of this call
|
|
582
|
-
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
578
|
+
function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) {
|
|
583
579
|
return ERC2771Context._msgData();
|
|
584
580
|
}
|
|
585
581
|
|
|
586
582
|
/// @notice Returns the sender, preferred to use over `msg.sender`
|
|
587
583
|
/// @return sender the sender address of this call.
|
|
588
|
-
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
584
|
+
function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) {
|
|
589
585
|
return ERC2771Context._msgSender();
|
|
590
586
|
}
|
|
591
587
|
|
|
@@ -636,97 +632,60 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
636
632
|
|
|
637
633
|
/// @notice Mint NFTs from the specified tiers and update the beneficiary's pay credits.
|
|
638
634
|
/// @param value The normalized payment value.
|
|
639
|
-
/// @param
|
|
640
|
-
|
|
635
|
+
/// @param payer The address that initiated the payment.
|
|
636
|
+
/// @param payerMetadata The metadata provided by the payer.
|
|
637
|
+
/// @param beneficiary The address to mint NFTs to and track credits for.
|
|
638
|
+
function _mintAndUpdateCredits(
|
|
639
|
+
uint256 value,
|
|
640
|
+
address payer,
|
|
641
|
+
bytes calldata payerMetadata,
|
|
642
|
+
address beneficiary
|
|
643
|
+
)
|
|
644
|
+
internal
|
|
645
|
+
{
|
|
641
646
|
// Keep a reference to the number of NFT credits the beneficiary already has.
|
|
642
|
-
uint256 payCredits = payCreditsOf[
|
|
643
|
-
|
|
644
|
-
// Set the leftover amount as the initial value.
|
|
645
|
-
uint256 leftoverAmount = value;
|
|
646
|
-
|
|
647
|
-
// If the payer is the beneficiary, combine their NFT credits with the amount paid.
|
|
648
|
-
uint256 unusedPayCredits;
|
|
649
|
-
if (context.payer == context.beneficiary) {
|
|
650
|
-
leftoverAmount += payCredits;
|
|
651
|
-
} else {
|
|
652
|
-
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
653
|
-
unusedPayCredits = payCredits;
|
|
654
|
-
}
|
|
647
|
+
uint256 payCredits = payCreditsOf[beneficiary];
|
|
655
648
|
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
649
|
+
// Compute the mint: combine credits, decode metadata, record mint, and check overspending.
|
|
650
|
+
(uint256[] memory tokenIds, uint16[] memory tierIdsToMint, uint256 newPayCredits) = JB721TiersHookLib.prepareMint({
|
|
651
|
+
store: STORE,
|
|
652
|
+
metadataIdTarget: METADATA_ID_TARGET,
|
|
653
|
+
value: value,
|
|
654
|
+
payer: payer,
|
|
655
|
+
beneficiary: beneficiary,
|
|
656
|
+
payCredits: payCredits,
|
|
657
|
+
payerMetadata: payerMetadata
|
|
663
658
|
});
|
|
664
659
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
// Decode the metadata.
|
|
673
|
-
(payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
|
|
674
|
-
|
|
675
|
-
// Make sure overspending is allowed if requested.
|
|
676
|
-
if (allowOverspending && !payerAllowsOverspending) {
|
|
677
|
-
allowOverspending = false;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Mint NFTs from the tiers as specified.
|
|
681
|
-
if (tierIdsToMint.length != 0) {
|
|
682
|
-
uint256[] memory tokenIds;
|
|
683
|
-
uint256 restrictedCost;
|
|
684
|
-
uint256 totalAmountPaid = leftoverAmount;
|
|
685
|
-
|
|
686
|
-
// Record the mints.
|
|
687
|
-
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
688
|
-
(tokenIds, leftoverAmount, restrictedCost) =
|
|
689
|
-
STORE.recordMint({amount: leftoverAmount, tierIds: tierIdsToMint, isOwnerMint: false});
|
|
690
|
-
|
|
691
|
-
// Enforce `cantBuyWithCredits`: only tiers explicitly configured as credit-restricted must be fully
|
|
692
|
-
// covered by fresh payment (not stored credits). Split-bearing tiers are not automatically restricted;
|
|
693
|
-
// deployers must set that flag in tier configuration when they need that invariant.
|
|
694
|
-
if (restrictedCost > value) revert JB721TiersHook_CantBuyWithCredits();
|
|
695
|
-
|
|
696
|
-
// Mint each token.
|
|
697
|
-
_mintTokens({
|
|
698
|
-
tokenIds: tokenIds,
|
|
699
|
-
tierIds: tierIdsToMint,
|
|
700
|
-
beneficiary: context.beneficiary,
|
|
701
|
-
totalAmountPaid: totalAmountPaid
|
|
702
|
-
});
|
|
703
|
-
}
|
|
660
|
+
// Mint each token to the effective beneficiary.
|
|
661
|
+
if (tokenIds.length != 0) {
|
|
662
|
+
// totalAmountPaid is the full amount available before recordMint deducted tier prices.
|
|
663
|
+
uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
|
|
664
|
+
_mintTokens({
|
|
665
|
+
tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
|
|
666
|
+
});
|
|
704
667
|
}
|
|
705
668
|
|
|
706
|
-
// If overspending isn't allowed, revert.
|
|
707
|
-
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
708
|
-
|
|
709
669
|
// Update NFT credits if they changed.
|
|
710
|
-
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
711
|
-
|
|
712
670
|
if (newPayCredits != payCredits) {
|
|
713
671
|
if (newPayCredits > payCredits) {
|
|
714
672
|
emit AddPayCredits({
|
|
715
673
|
amount: newPayCredits - payCredits,
|
|
716
674
|
newTotalCredits: newPayCredits,
|
|
717
|
-
account:
|
|
675
|
+
account: beneficiary,
|
|
718
676
|
caller: _msgSender()
|
|
719
677
|
});
|
|
720
678
|
} else {
|
|
721
679
|
emit UsePayCredits({
|
|
722
680
|
amount: payCredits - newPayCredits,
|
|
723
681
|
newTotalCredits: newPayCredits,
|
|
724
|
-
account:
|
|
682
|
+
account: beneficiary,
|
|
725
683
|
caller: _msgSender()
|
|
726
684
|
});
|
|
727
685
|
}
|
|
728
686
|
|
|
729
|
-
|
|
687
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
688
|
+
payCreditsOf[beneficiary] = newPayCredits;
|
|
730
689
|
}
|
|
731
690
|
}
|
|
732
691
|
|
|
@@ -749,11 +708,24 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
749
708
|
});
|
|
750
709
|
if (!valid) return;
|
|
751
710
|
|
|
711
|
+
// Decode the beneficiary and payer forwarded from beforePayRecordedWith.
|
|
712
|
+
address beneficiary;
|
|
713
|
+
address payer;
|
|
714
|
+
bytes memory splitData;
|
|
715
|
+
if (context.hookMetadata.length != 0) {
|
|
716
|
+
(beneficiary, payer, splitData) = abi.decode(context.hookMetadata, (address, address, bytes));
|
|
717
|
+
}
|
|
718
|
+
// Fall back to context values if none were forwarded.
|
|
719
|
+
if (beneficiary == address(0)) beneficiary = context.beneficiary;
|
|
720
|
+
if (payer == address(0)) payer = context.payer;
|
|
721
|
+
|
|
752
722
|
// Mint NFTs from the specified tiers and update the beneficiary's pay credits.
|
|
753
|
-
_mintAndUpdateCredits({
|
|
723
|
+
_mintAndUpdateCredits({
|
|
724
|
+
value: value, payer: payer, payerMetadata: context.payerMetadata, beneficiary: beneficiary
|
|
725
|
+
});
|
|
754
726
|
|
|
755
727
|
// Distribute any forwarded funds to tier split groups.
|
|
756
|
-
if (
|
|
728
|
+
if (splitData.length != 0 && context.forwardedAmount.value != 0) {
|
|
757
729
|
JB721TiersHookLib.distributeAll({
|
|
758
730
|
directory: DIRECTORY,
|
|
759
731
|
splits: SPLITS,
|
|
@@ -762,7 +734,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
762
734
|
token: context.forwardedAmount.token,
|
|
763
735
|
amount: context.forwardedAmount.value,
|
|
764
736
|
decimals: context.forwardedAmount.decimals,
|
|
765
|
-
encodedSplitData:
|
|
737
|
+
encodedSplitData: splitData
|
|
766
738
|
});
|
|
767
739
|
}
|
|
768
740
|
}
|
|
@@ -779,11 +751,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
779
751
|
/// @param tierId The ID of the tier to set the discount percent for.
|
|
780
752
|
/// @param discountPercent The discount percent to set for the tier.
|
|
781
753
|
function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
|
|
782
|
-
emit SetDiscountPercent({tierId: tierId, discountPercent: discountPercent, caller: _msgSender()});
|
|
783
|
-
|
|
784
|
-
// Record the discount percent for the tier.
|
|
785
754
|
// slither-disable-next-line calls-loop
|
|
786
|
-
|
|
755
|
+
JB721TiersHookLib.setDiscountPercentOf({
|
|
756
|
+
store: STORE, tierId: tierId, discountPercent: discountPercent, caller: _msgSender()
|
|
757
|
+
});
|
|
787
758
|
}
|
|
788
759
|
|
|
789
760
|
/// @notice Before transferring an NFT, register its first owner (if necessary).
|
|
@@ -822,5 +793,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
822
793
|
// Record the transfer.
|
|
823
794
|
// slither-disable-next-line reentrency-events,calls-loop
|
|
824
795
|
STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
|
|
796
|
+
|
|
797
|
+
// Notify the checkpoint module to update checkpointed voting power.
|
|
798
|
+
CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
|
|
825
799
|
}
|
|
826
800
|
}
|