@bananapus/721-hook-v6 0.0.32 → 0.0.33
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 +68 -32
- 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/JB721TiersHookLib.sol +53 -2
- 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/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.33",
|
|
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,6 +19,8 @@ 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";
|
|
@@ -42,7 +44,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
42
44
|
|
|
43
45
|
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
44
46
|
error JB721TiersHook_CantBuyWithCredits();
|
|
45
|
-
error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
|
|
46
47
|
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
47
48
|
error JB721TiersHook_MintReserveNftsPaused();
|
|
48
49
|
error JB721TiersHook_NoProjectId();
|
|
@@ -65,6 +66,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
65
66
|
/// @notice The contract that stores and manages splits.
|
|
66
67
|
IJBSplits public immutable override SPLITS;
|
|
67
68
|
|
|
69
|
+
/// @notice The deployer used to deploy checkpoint module clones during initialization.
|
|
70
|
+
IJB721CheckpointsDeployer internal immutable CHECKPOINTS_DEPLOYER;
|
|
71
|
+
|
|
68
72
|
//*********************************************************************//
|
|
69
73
|
// --------------------- private stored properties ------------------ //
|
|
70
74
|
//*********************************************************************//
|
|
@@ -104,6 +108,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
104
108
|
/// - pricing decimals in bits 32-39 (8 bits).
|
|
105
109
|
uint256 internal _packedPricingContext;
|
|
106
110
|
|
|
111
|
+
/// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
|
|
112
|
+
/// @dev Set once during `initialize()`. Pass this to JBTokenDistributor as the IVotes token.
|
|
113
|
+
IJB721Checkpoints public override CHECKPOINTS;
|
|
114
|
+
|
|
107
115
|
//*********************************************************************//
|
|
108
116
|
// -------------------------- constructor ---------------------------- //
|
|
109
117
|
//*********************************************************************//
|
|
@@ -114,6 +122,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
114
122
|
/// @param rulesets A contract storing and managing project rulesets.
|
|
115
123
|
/// @param store The contract which stores the NFT's data.
|
|
116
124
|
/// @param splits The contract that stores and manages splits.
|
|
125
|
+
/// @param checkpointsDeployer The deployer used to deploy checkpoint module clones during initialization.
|
|
117
126
|
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
118
127
|
constructor(
|
|
119
128
|
IJBDirectory directory,
|
|
@@ -122,6 +131,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
122
131
|
IJBRulesets rulesets,
|
|
123
132
|
IJB721TiersHookStore store,
|
|
124
133
|
IJBSplits splits,
|
|
134
|
+
IJB721CheckpointsDeployer checkpointsDeployer,
|
|
125
135
|
address trustedForwarder
|
|
126
136
|
)
|
|
127
137
|
JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
|
|
@@ -132,6 +142,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
132
142
|
RULESETS = rulesets;
|
|
133
143
|
STORE = store;
|
|
134
144
|
SPLITS = splits;
|
|
145
|
+
CHECKPOINTS_DEPLOYER = checkpointsDeployer;
|
|
135
146
|
|
|
136
147
|
// Prevent the implementation contract from being initialized.
|
|
137
148
|
_initialized = true;
|
|
@@ -146,14 +157,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
146
157
|
/// @param tokenId The token ID of the NFT to get the first owner of.
|
|
147
158
|
/// @return The address of the NFT's first owner.
|
|
148
159
|
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);
|
|
160
|
+
address first = _firstOwnerOf[tokenId];
|
|
161
|
+
return first != address(0) ? first : _ownerOf(tokenId);
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
/// @notice Context for the pricing of this hook's tiers.
|
|
@@ -225,8 +230,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
225
230
|
hook: address(this)
|
|
226
231
|
});
|
|
227
232
|
|
|
228
|
-
hookSpecifications[0] =
|
|
229
|
-
|
|
233
|
+
hookSpecifications[0] = JBPayHookSpecification({
|
|
234
|
+
hook: this,
|
|
235
|
+
noop: false,
|
|
236
|
+
amount: totalSplitAmount,
|
|
237
|
+
metadata: abi.encode(context.beneficiary, context.payer, splitMetadata)
|
|
238
|
+
});
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
/// @notice The combined cash out weight of the NFTs with the specified token IDs.
|
|
@@ -314,6 +323,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
314
323
|
|| flags.preventOverspending || flags.issueTokensForSplits
|
|
315
324
|
) STORE.recordFlags(flags);
|
|
316
325
|
|
|
326
|
+
// Deploy the checkpoint module for IVotes-compatible voting power.
|
|
327
|
+
CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
|
|
328
|
+
|
|
317
329
|
// Transfer ownership to the initializer.
|
|
318
330
|
_transferOwnership(_msgSender());
|
|
319
331
|
}
|
|
@@ -321,7 +333,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
321
333
|
/// @notice Indicates if this contract adheres to the specified interface.
|
|
322
334
|
/// @dev See {IERC165-supportsInterface}.
|
|
323
335
|
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
324
|
-
function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
|
|
336
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, JB721Hook) returns (bool) {
|
|
325
337
|
return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
|
|
326
338
|
}
|
|
327
339
|
|
|
@@ -579,13 +591,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
579
591
|
|
|
580
592
|
/// @notice Returns the calldata, preferred to use over `msg.data`
|
|
581
593
|
/// @return calldata the `msg.data` of this call
|
|
582
|
-
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
594
|
+
function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) {
|
|
583
595
|
return ERC2771Context._msgData();
|
|
584
596
|
}
|
|
585
597
|
|
|
586
598
|
/// @notice Returns the sender, preferred to use over `msg.sender`
|
|
587
599
|
/// @return sender the sender address of this call.
|
|
588
|
-
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
600
|
+
function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) {
|
|
589
601
|
return ERC2771Context._msgSender();
|
|
590
602
|
}
|
|
591
603
|
|
|
@@ -636,17 +648,26 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
636
648
|
|
|
637
649
|
/// @notice Mint NFTs from the specified tiers and update the beneficiary's pay credits.
|
|
638
650
|
/// @param value The normalized payment value.
|
|
639
|
-
/// @param
|
|
640
|
-
|
|
651
|
+
/// @param payer The address that initiated the payment.
|
|
652
|
+
/// @param payerMetadata The metadata provided by the payer.
|
|
653
|
+
/// @param beneficiary The address to mint NFTs to and track credits for.
|
|
654
|
+
function _mintAndUpdateCredits(
|
|
655
|
+
uint256 value,
|
|
656
|
+
address payer,
|
|
657
|
+
bytes calldata payerMetadata,
|
|
658
|
+
address beneficiary
|
|
659
|
+
)
|
|
660
|
+
internal
|
|
661
|
+
{
|
|
641
662
|
// Keep a reference to the number of NFT credits the beneficiary already has.
|
|
642
|
-
uint256 payCredits = payCreditsOf[
|
|
663
|
+
uint256 payCredits = payCreditsOf[beneficiary];
|
|
643
664
|
|
|
644
665
|
// Set the leftover amount as the initial value.
|
|
645
666
|
uint256 leftoverAmount = value;
|
|
646
667
|
|
|
647
|
-
// If the payer is the beneficiary, combine their NFT credits with the amount paid.
|
|
668
|
+
// If the payer is the effective beneficiary, combine their NFT credits with the amount paid.
|
|
648
669
|
uint256 unusedPayCredits;
|
|
649
|
-
if (
|
|
670
|
+
if (payer == beneficiary) {
|
|
650
671
|
leftoverAmount += payCredits;
|
|
651
672
|
} else {
|
|
652
673
|
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
@@ -659,7 +680,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
659
680
|
|
|
660
681
|
// Resolve the metadata.
|
|
661
682
|
(bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
662
|
-
id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata:
|
|
683
|
+
id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata: payerMetadata
|
|
663
684
|
});
|
|
664
685
|
|
|
665
686
|
if (found) {
|
|
@@ -693,11 +714,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
693
714
|
// deployers must set that flag in tier configuration when they need that invariant.
|
|
694
715
|
if (restrictedCost > value) revert JB721TiersHook_CantBuyWithCredits();
|
|
695
716
|
|
|
696
|
-
// Mint each token.
|
|
717
|
+
// Mint each token to the effective beneficiary.
|
|
697
718
|
_mintTokens({
|
|
698
719
|
tokenIds: tokenIds,
|
|
699
720
|
tierIds: tierIdsToMint,
|
|
700
|
-
beneficiary:
|
|
721
|
+
beneficiary: beneficiary,
|
|
701
722
|
totalAmountPaid: totalAmountPaid
|
|
702
723
|
});
|
|
703
724
|
}
|
|
@@ -714,19 +735,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
714
735
|
emit AddPayCredits({
|
|
715
736
|
amount: newPayCredits - payCredits,
|
|
716
737
|
newTotalCredits: newPayCredits,
|
|
717
|
-
account:
|
|
738
|
+
account: beneficiary,
|
|
718
739
|
caller: _msgSender()
|
|
719
740
|
});
|
|
720
741
|
} else {
|
|
721
742
|
emit UsePayCredits({
|
|
722
743
|
amount: payCredits - newPayCredits,
|
|
723
744
|
newTotalCredits: newPayCredits,
|
|
724
|
-
account:
|
|
745
|
+
account: beneficiary,
|
|
725
746
|
caller: _msgSender()
|
|
726
747
|
});
|
|
727
748
|
}
|
|
728
749
|
|
|
729
|
-
payCreditsOf[
|
|
750
|
+
payCreditsOf[beneficiary] = newPayCredits;
|
|
730
751
|
}
|
|
731
752
|
}
|
|
732
753
|
|
|
@@ -749,11 +770,24 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
749
770
|
});
|
|
750
771
|
if (!valid) return;
|
|
751
772
|
|
|
773
|
+
// Decode the beneficiary and payer forwarded from beforePayRecordedWith.
|
|
774
|
+
address beneficiary;
|
|
775
|
+
address payer;
|
|
776
|
+
bytes memory splitData;
|
|
777
|
+
if (context.hookMetadata.length != 0) {
|
|
778
|
+
(beneficiary, payer, splitData) = abi.decode(context.hookMetadata, (address, address, bytes));
|
|
779
|
+
}
|
|
780
|
+
// Fall back to context values if none were forwarded.
|
|
781
|
+
if (beneficiary == address(0)) beneficiary = context.beneficiary;
|
|
782
|
+
if (payer == address(0)) payer = context.payer;
|
|
783
|
+
|
|
752
784
|
// Mint NFTs from the specified tiers and update the beneficiary's pay credits.
|
|
753
|
-
_mintAndUpdateCredits({
|
|
785
|
+
_mintAndUpdateCredits({
|
|
786
|
+
value: value, payer: payer, payerMetadata: context.payerMetadata, beneficiary: beneficiary
|
|
787
|
+
});
|
|
754
788
|
|
|
755
789
|
// Distribute any forwarded funds to tier split groups.
|
|
756
|
-
if (
|
|
790
|
+
if (splitData.length != 0 && context.forwardedAmount.value != 0) {
|
|
757
791
|
JB721TiersHookLib.distributeAll({
|
|
758
792
|
directory: DIRECTORY,
|
|
759
793
|
splits: SPLITS,
|
|
@@ -762,7 +796,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
762
796
|
token: context.forwardedAmount.token,
|
|
763
797
|
amount: context.forwardedAmount.value,
|
|
764
798
|
decimals: context.forwardedAmount.decimals,
|
|
765
|
-
encodedSplitData:
|
|
799
|
+
encodedSplitData: splitData
|
|
766
800
|
});
|
|
767
801
|
}
|
|
768
802
|
}
|
|
@@ -779,11 +813,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
779
813
|
/// @param tierId The ID of the tier to set the discount percent for.
|
|
780
814
|
/// @param discountPercent The discount percent to set for the tier.
|
|
781
815
|
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
816
|
// slither-disable-next-line calls-loop
|
|
786
|
-
|
|
817
|
+
JB721TiersHookLib.setDiscountPercentOf({
|
|
818
|
+
store: STORE, tierId: tierId, discountPercent: discountPercent, caller: _msgSender()
|
|
819
|
+
});
|
|
787
820
|
}
|
|
788
821
|
|
|
789
822
|
/// @notice Before transferring an NFT, register its first owner (if necessary).
|
|
@@ -822,5 +855,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
822
855
|
// Record the transfer.
|
|
823
856
|
// slither-disable-next-line reentrency-events,calls-loop
|
|
824
857
|
STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
|
|
858
|
+
|
|
859
|
+
// Notify the checkpoint module to update checkpointed voting power.
|
|
860
|
+
CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
|
|
825
861
|
}
|
|
826
862
|
}
|
|
@@ -77,6 +77,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
77
77
|
/// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
|
|
78
78
|
/// @return cashOutCount The amount of tokens that should be considered cashed out.
|
|
79
79
|
/// @return totalSupply The total amount of tokens that are considered to be existing.
|
|
80
|
+
/// @return effectiveSurplusValue The surplus to use for reclaim calculation.
|
|
80
81
|
/// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
|
|
81
82
|
/// the beneficiary.
|
|
82
83
|
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
@@ -88,6 +89,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
88
89
|
uint256 cashOutTaxRate,
|
|
89
90
|
uint256 cashOutCount,
|
|
90
91
|
uint256 totalSupply,
|
|
92
|
+
uint256 effectiveSurplusValue,
|
|
91
93
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
92
94
|
)
|
|
93
95
|
{
|
|
@@ -114,6 +116,9 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
114
116
|
// Use the total cash out weight of the 721s.
|
|
115
117
|
totalSupply = totalCashOutWeight();
|
|
116
118
|
|
|
119
|
+
// Use the surplus from the context.
|
|
120
|
+
effectiveSurplusValue = context.surplus.value;
|
|
121
|
+
|
|
117
122
|
// Use the cash out tax rate from the context.
|
|
118
123
|
cashOutTaxRate = context.cashOutTaxRate;
|
|
119
124
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol";
|
|
5
|
+
import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
6
|
+
|
|
7
|
+
/// @notice A checkpoint module that provides IVotes-compatible checkpointed voting power for a JB721TiersHook.
|
|
8
|
+
/// @dev Deployed as a clone via JB721CheckpointsDeployer during hook initialization. One module per hook.
|
|
9
|
+
/// Pass this address to JBTokenDistributor as the IVotes token.
|
|
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
|
+
/// @notice The hook that this module tracks voting power for.
|
|
26
|
+
/// @return The hook address.
|
|
27
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
28
|
+
function HOOK() external view returns (address);
|
|
29
|
+
|
|
30
|
+
/// @notice The store that holds tier and voting data for the hook's NFTs.
|
|
31
|
+
/// @return The store contract.
|
|
32
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
33
|
+
function STORE() external view returns (IJB721TiersHookStore);
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {IJB721Checkpoints} from "./IJB721Checkpoints.sol";
|
|
5
|
+
import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
6
|
+
|
|
7
|
+
/// @notice Deploys JB721Checkpoints clones for JB721TiersHook instances.
|
|
8
|
+
interface IJB721CheckpointsDeployer {
|
|
9
|
+
/// @notice The implementation contract that clones are based on.
|
|
10
|
+
/// @return The implementation address.
|
|
11
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
12
|
+
function IMPLEMENTATION() external view returns (address);
|
|
13
|
+
|
|
14
|
+
/// @notice Deploys a new deterministic checkpoint clone for the given hook.
|
|
15
|
+
/// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
|
|
16
|
+
/// @param hook The hook address the module will serve.
|
|
17
|
+
/// @param store The store that holds tier data for the hook's NFTs.
|
|
18
|
+
/// @return module The newly deployed and initialized checkpoint module.
|
|
19
|
+
function deploy(address hook, IJB721TiersHookStore store) external returns (IJB721Checkpoints module);
|
|
20
|
+
}
|