@bananapus/distributor-v6 0.0.7 → 0.0.9
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/README.md +2 -2
- package/package.json +16 -7
- package/src/JB721Distributor.sol +46 -12
- package/src/JBDistributor.sol +59 -37
- package/src/JBTokenDistributor.sol +4 -0
- package/src/interfaces/IJBDistributor.sol +3 -1
- package/src/structs/JBTokenSnapshotData.sol +4 -2
- package/src/structs/JBVestingData.sol +6 -3
- package/.github/pull_request_template.md +0 -33
- package/.github/workflows/lint.yml +0 -19
- package/.github/workflows/publish.yml +0 -19
- package/.github/workflows/slither.yml +0 -23
- package/.github/workflows/test.yml +0 -28
- package/.gitmodules +0 -3
- package/ADMINISTRATION.md +0 -65
- package/ARCHITECTURE.md +0 -89
- package/AUDIT_INSTRUCTIONS.md +0 -52
- package/RISKS.md +0 -78
- package/SKILLS.md +0 -36
- package/USER_JOURNEYS.md +0 -122
- package/slither-ci.config.json +0 -10
- package/test/AuditFixes.t.sol +0 -429
- package/test/JB721Distributor.t.sol +0 -2059
- package/test/JBTokenDistributor.t.sol +0 -503
- package/test/audit/CodexNemesisAccountingPoC.t.sol +0 -344
- package/test/audit/CodexNemesisFreshSplitTokenMismatch.t.sol +0 -133
- package/test/audit/CodexNemesisFreshVerification.t.sol +0 -218
- package/test/audit/CodexNemesisPoC.t.sol +0 -191
- package/test/audit/H26VotingPowerCap.t.sol +0 -343
- package/test/audit/Pass12Fixes.t.sol +0 -344
- package/test/audit/PostSnapshotMintTheft.t.sol +0 -413
- package/test/audit/TokenMismatchFix.t.sol +0 -295
- package/test/fork/TokenDistributorFork.t.sol +0 -603
- package/test/invariant/JB721DistributorInvariant.t.sol +0 -414
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/distributor-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Bananapus/nana-distributor-v6"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"foundry.lock",
|
|
12
|
+
"foundry.toml",
|
|
13
|
+
"references/",
|
|
14
|
+
"remappings.txt",
|
|
15
|
+
"script/",
|
|
16
|
+
"src/"
|
|
17
|
+
],
|
|
9
18
|
"engines": {
|
|
10
19
|
"node": ">=20.0.0"
|
|
11
20
|
},
|
|
@@ -17,13 +26,13 @@
|
|
|
17
26
|
"deploy:testnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks testnets"
|
|
18
27
|
},
|
|
19
28
|
"dependencies": {
|
|
20
|
-
"@bananapus/721-hook-v6": "
|
|
21
|
-
"@bananapus/core-v6": "
|
|
22
|
-
"@bananapus/permission-ids-v6": "
|
|
23
|
-
"@openzeppelin/contracts": "
|
|
24
|
-
"@prb/math": "
|
|
29
|
+
"@bananapus/721-hook-v6": "0.0.43",
|
|
30
|
+
"@bananapus/core-v6": "0.0.39",
|
|
31
|
+
"@bananapus/permission-ids-v6": "0.0.22",
|
|
32
|
+
"@openzeppelin/contracts": "5.6.1",
|
|
33
|
+
"@prb/math": "4.1.1"
|
|
25
34
|
},
|
|
26
35
|
"devDependencies": {
|
|
27
|
-
"@sphinx-labs/plugins": "
|
|
36
|
+
"@sphinx-labs/plugins": "0.33.3"
|
|
28
37
|
}
|
|
29
38
|
}
|
package/src/JB721Distributor.sol
CHANGED
|
@@ -256,8 +256,8 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
256
256
|
|
|
257
257
|
/// @notice The stake weight of a given NFT token ID based on its tier's voting units, validated against historical
|
|
258
258
|
/// state.
|
|
259
|
-
/// @dev Returns 0 if the token
|
|
260
|
-
/// preventing late mints from capturing pro-rata rewards within the current round.
|
|
259
|
+
/// @dev Returns 0 if the token was not owned at the round's snapshot block or if its snapshot owner had no
|
|
260
|
+
/// checkpointed voting power, preventing late mints from capturing pro-rata rewards within the current round.
|
|
261
261
|
/// @param hook The hook the token belongs to.
|
|
262
262
|
/// @param tokenId The ID of the token to get the stake weight of.
|
|
263
263
|
/// @return tokenStakeAmount The voting units of the token's tier (or 0 if ineligible).
|
|
@@ -267,12 +267,15 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
267
267
|
.STORE()
|
|
268
268
|
.tierOfTokenId({hook: hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
269
269
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
// Stake eligibility is fixed at the round snapshot block, not the caller's current block.
|
|
271
|
+
uint256 snapshotBlock = roundSnapshotBlock[currentRound()];
|
|
272
|
+
address owner = _snapshotOwnerOf({hook: hook, tokenId: tokenId, snapshotBlock: snapshotBlock});
|
|
273
|
+
if (owner == address(0)) return 0;
|
|
274
|
+
|
|
275
|
+
// Use the checkpoints module to verify the token's snapshot owner had voting power at the round's snapshot
|
|
276
|
+
// block. If the token did not exist then, ownerOfAt returns zero above and the token is not eligible.
|
|
274
277
|
uint256 pastVotes = IVotes(address(IJB721TiersHook(hook).CHECKPOINTS()))
|
|
275
|
-
.getPastVotes({account: owner, timepoint:
|
|
278
|
+
.getPastVotes({account: owner, timepoint: snapshotBlock});
|
|
276
279
|
|
|
277
280
|
// If the owner had no voting power at round start, the token is ineligible.
|
|
278
281
|
// slither-disable-next-line incorrect-equality
|
|
@@ -299,6 +302,35 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
299
302
|
// ----------------------- private helpers --------------------------- //
|
|
300
303
|
//*********************************************************************//
|
|
301
304
|
|
|
305
|
+
/// @notice Returns the token owner at the round snapshot block.
|
|
306
|
+
/// @dev Returns zero if the hook has no checkpoint module, the module does not support historical ownership, the
|
|
307
|
+
/// call fails, or the token was not owned at `snapshotBlock`. Treating all of these as ineligible prevents late
|
|
308
|
+
/// mints and current-owner transfers from claiming rewards for a snapshot they did not participate in.
|
|
309
|
+
/// @param hook The 721 hook whose checkpoint module is queried.
|
|
310
|
+
/// @param tokenId The token ID to query.
|
|
311
|
+
/// @param snapshotBlock The round snapshot block to prove ownership at.
|
|
312
|
+
/// @return owner The historical token owner, or zero if ownership cannot be proven.
|
|
313
|
+
function _snapshotOwnerOf(
|
|
314
|
+
address hook,
|
|
315
|
+
uint256 tokenId,
|
|
316
|
+
uint256 snapshotBlock
|
|
317
|
+
)
|
|
318
|
+
private
|
|
319
|
+
view
|
|
320
|
+
returns (address owner)
|
|
321
|
+
{
|
|
322
|
+
// The 721 hook owns the checkpoint module; the distributor only trusts that module's historical proof.
|
|
323
|
+
IJB721Checkpoints checkpoints = IJB721TiersHook(hook).CHECKPOINTS();
|
|
324
|
+
|
|
325
|
+
// Use staticcall so older hooks without `ownerOfAt` fail closed instead of reverting the whole distribution.
|
|
326
|
+
(bool success, bytes memory data) =
|
|
327
|
+
address(checkpoints).staticcall(abi.encodeCall(IJB721Checkpoints.ownerOfAt, (tokenId, snapshotBlock)));
|
|
328
|
+
if (!success || data.length < 32) return address(0);
|
|
329
|
+
|
|
330
|
+
// A zero owner means the token was not owned at the snapshot block and is not eligible this round.
|
|
331
|
+
owner = abi.decode(data, (address));
|
|
332
|
+
}
|
|
333
|
+
|
|
302
334
|
/// @notice Vest a single NFT token, enforcing a per-owner voting power cap across the batch.
|
|
303
335
|
/// @dev Returns 0 for burned tokens, already-vested tokens, tokens whose owner had no snapshot voting power,
|
|
304
336
|
/// and tokens whose owner has already exhausted their voting power cap within this batch.
|
|
@@ -350,18 +382,20 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
350
382
|
.STORE()
|
|
351
383
|
.tierOfTokenId({hook: ctx.hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
352
384
|
|
|
353
|
-
// Look up the owner, verify snapshot eligibility, and find or create the owner's tracking slot.
|
|
385
|
+
// Look up the snapshot owner, verify snapshot eligibility, and find or create the owner's tracking slot.
|
|
354
386
|
uint256 ownerIndex;
|
|
355
387
|
uint256 pastVotes;
|
|
356
388
|
{
|
|
357
|
-
//
|
|
358
|
-
|
|
389
|
+
// Reuse the same round snapshot block for every token in this vesting batch.
|
|
390
|
+
uint256 snapshotBlock = roundSnapshotBlock[currentRound()];
|
|
391
|
+
address owner = _snapshotOwnerOf({hook: ctx.hook, tokenId: tokenId, snapshotBlock: snapshotBlock});
|
|
392
|
+
if (owner == address(0)) return (0, newUniqueCount);
|
|
359
393
|
|
|
360
394
|
// Query the owner's checkpointed voting power at the round's snapshot block.
|
|
361
395
|
pastVotes = IVotes(address(IJB721TiersHook(ctx.hook).CHECKPOINTS()))
|
|
362
|
-
.getPastVotes({account: owner, timepoint:
|
|
396
|
+
.getPastVotes({account: owner, timepoint: snapshotBlock});
|
|
363
397
|
|
|
364
|
-
// If the owner had no voting power at round start, the token is ineligible for this round.
|
|
398
|
+
// If the snapshot owner had no voting power at round start, the token is ineligible for this round.
|
|
365
399
|
// slither-disable-next-line incorrect-equality
|
|
366
400
|
if (pastVotes == 0) return (0, newUniqueCount);
|
|
367
401
|
|
package/src/JBDistributor.sol
CHANGED
|
@@ -10,7 +10,13 @@ import {IJBDistributor} from "./interfaces/IJBDistributor.sol";
|
|
|
10
10
|
import {JBTokenSnapshotData} from "./structs/JBTokenSnapshotData.sol";
|
|
11
11
|
import {JBVestingData} from "./structs/JBVestingData.sol";
|
|
12
12
|
|
|
13
|
-
/// @notice
|
|
13
|
+
/// @notice Abstract base for reward distributors. Manages round-based distribution of ERC-20 tokens (or native ETH)
|
|
14
|
+
/// to stakers with linear vesting. Each round, a snapshot is taken of the distributable balance, and stakers can
|
|
15
|
+
/// claim their pro-rata share based on their stake weight at the snapshot block. Claimed tokens vest linearly over
|
|
16
|
+
/// `vestingRounds` rounds and can be collected as they unlock.
|
|
17
|
+
/// @dev Subclasses define how stake is measured (`_tokenStake`, `_totalStake`), who can claim (`_canClaim`), and
|
|
18
|
+
/// what "burned" means (`_tokenBurned`). Two concrete implementations exist: `JBTokenDistributor` (IVotes tokens)
|
|
19
|
+
/// and `JB721Distributor` (Juicebox 721 NFTs).
|
|
14
20
|
abstract contract JBDistributor is IJBDistributor {
|
|
15
21
|
using SafeERC20 for IERC20;
|
|
16
22
|
|
|
@@ -122,10 +128,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
122
128
|
// ---------------------- external transactions ---------------------- //
|
|
123
129
|
//*********************************************************************//
|
|
124
130
|
|
|
125
|
-
/// @notice
|
|
126
|
-
///
|
|
127
|
-
///
|
|
128
|
-
/// @param
|
|
131
|
+
/// @notice Snapshot the current round's distributable balance and begin vesting for the specified token IDs.
|
|
132
|
+
/// Each token ID's share is proportional to its stake weight relative to the total stake at the snapshot block.
|
|
133
|
+
/// Vesting completes after `vestingRounds` rounds. Reverts if there's nothing to distribute.
|
|
134
|
+
/// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
|
|
135
|
+
/// @param tokenIds The staker token IDs to claim rewards for.
|
|
136
|
+
/// @param tokens The reward tokens to begin vesting.
|
|
129
137
|
function beginVesting(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) external override {
|
|
130
138
|
// Revert if no token IDs are provided.
|
|
131
139
|
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds();
|
|
@@ -167,11 +175,13 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
167
175
|
}
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
/// @notice
|
|
171
|
-
///
|
|
172
|
-
/// @
|
|
178
|
+
/// @notice Directly fund the distributor for a specific hook by pulling tokens from the caller. An alternative
|
|
179
|
+
/// to split-based funding — useful for one-off deposits or external reward sources.
|
|
180
|
+
/// @dev For native ETH, send `msg.value` and pass `IERC20(JBConstants.NATIVE_TOKEN)` as the token. Uses balance
|
|
181
|
+
/// delta to handle fee-on-transfer tokens correctly.
|
|
182
|
+
/// @param hook The hook to fund (determines which staker pool receives the tokens).
|
|
173
183
|
/// @param token The token to fund with.
|
|
174
|
-
/// @param amount The amount to fund.
|
|
184
|
+
/// @param amount The amount to fund (ignored for native ETH — `msg.value` is used instead).
|
|
175
185
|
function fund(address hook, IERC20 token, uint256 amount) external payable override {
|
|
176
186
|
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
177
187
|
amount = msg.value;
|
|
@@ -186,16 +196,19 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
186
196
|
_accountedBalanceOf[token] += amount;
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
/// @notice Record the snapshot block for the current round. Callable by anyone
|
|
199
|
+
/// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
|
|
200
|
+
/// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
|
|
190
201
|
function poke() external override {
|
|
191
202
|
_ensureSnapshotBlock(currentRound());
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
/// @notice Release
|
|
205
|
+
/// @notice Release unvested rewards tied to burned tokens. When an NFT is burned, its pending vesting entries
|
|
206
|
+
/// become stranded — this function unlocks them and returns them to the hook's distributable pool (they are NOT
|
|
207
|
+
/// sent to the beneficiary). Anyone can call this for burned tokens.
|
|
195
208
|
/// @param hook The hook whose tokens were burned.
|
|
196
|
-
/// @param tokenIds The IDs of the burned tokens.
|
|
197
|
-
/// @param tokens The
|
|
198
|
-
/// @param beneficiary
|
|
209
|
+
/// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
|
|
210
|
+
/// @param tokens The reward tokens to release.
|
|
211
|
+
/// @param beneficiary Unused for forfeiture — tokens return to the pool. Kept for interface compatibility.
|
|
199
212
|
function releaseForfeitedRewards(
|
|
200
213
|
address hook,
|
|
201
214
|
uint256[] calldata tokenIds,
|
|
@@ -228,11 +241,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
228
241
|
return _balanceOf[hook][token];
|
|
229
242
|
}
|
|
230
243
|
|
|
231
|
-
/// @notice Calculate
|
|
244
|
+
/// @notice Calculate the total amount of a reward token that has been claimed (began vesting) for a given
|
|
245
|
+
/// staker token ID but has not yet been collected. Includes both locked (still vesting) and unlocked amounts.
|
|
232
246
|
/// @param hook The hook the tokenId belongs to.
|
|
233
|
-
/// @param tokenId The ID of the token to calculate
|
|
234
|
-
/// @param token The
|
|
235
|
-
/// @return tokenAmount The
|
|
247
|
+
/// @param tokenId The ID of the staker token to calculate for.
|
|
248
|
+
/// @param token The reward token to check.
|
|
249
|
+
/// @return tokenAmount The total uncollected amount (vesting + vested-but-uncollected).
|
|
236
250
|
function claimedFor(
|
|
237
251
|
address hook,
|
|
238
252
|
uint256 tokenId,
|
|
@@ -261,11 +275,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
261
275
|
}
|
|
262
276
|
}
|
|
263
277
|
|
|
264
|
-
/// @notice Calculate how much of
|
|
278
|
+
/// @notice Calculate how much of a reward token is currently unlocked and ready to be collected for a given
|
|
279
|
+
/// staker token ID. Only includes the vested portion — excludes amounts still locked in vesting.
|
|
265
280
|
/// @param hook The hook the tokenId belongs to.
|
|
266
|
-
/// @param tokenId The ID of the token to calculate
|
|
267
|
-
/// @param token The
|
|
268
|
-
/// @return tokenAmount The amount of tokens that can be
|
|
281
|
+
/// @param tokenId The ID of the staker token to calculate for.
|
|
282
|
+
/// @param token The reward token to check.
|
|
283
|
+
/// @return tokenAmount The amount of tokens that can be collected right now via `collectVestedRewards`.
|
|
269
284
|
function collectableFor(
|
|
270
285
|
address hook,
|
|
271
286
|
uint256 tokenId,
|
|
@@ -340,10 +355,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
340
355
|
// ----------------------- public transactions ----------------------- //
|
|
341
356
|
//*********************************************************************//
|
|
342
357
|
|
|
343
|
-
/// @notice Collect vested
|
|
358
|
+
/// @notice Collect tokens that have vested (partially or fully) and transfer them to the beneficiary. Also
|
|
359
|
+
/// auto-vests for the current round if rewards haven't been claimed yet — so callers don't need to separately
|
|
360
|
+
/// call `beginVesting`. Only the token owner (verified via `_canClaim`) can collect.
|
|
344
361
|
/// @param hook The hook whose stakers are collecting.
|
|
345
|
-
/// @param tokenIds The IDs of the tokens to collect for.
|
|
346
|
-
/// @param tokens The
|
|
362
|
+
/// @param tokenIds The IDs of the tokens to collect for (caller must own all of them).
|
|
363
|
+
/// @param tokens The reward tokens to collect vested amounts of.
|
|
347
364
|
/// @param beneficiary The recipient of the collected tokens.
|
|
348
365
|
function collectVestedRewards(
|
|
349
366
|
address hook,
|
|
@@ -633,28 +650,33 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
633
650
|
// ----------------------- internal views ---------------------------- //
|
|
634
651
|
//*********************************************************************//
|
|
635
652
|
|
|
636
|
-
/// @notice
|
|
653
|
+
/// @notice Check whether an account is authorized to collect vested rewards for the given token ID. For 721
|
|
654
|
+
/// distributors this is ownership; for token distributors this is address-encoding match.
|
|
637
655
|
/// @param hook The hook the token belongs to.
|
|
638
656
|
/// @param tokenId The ID of the token to check.
|
|
639
|
-
/// @param account The account to check
|
|
640
|
-
/// @return canClaim
|
|
657
|
+
/// @param account The account to check authorization for.
|
|
658
|
+
/// @return canClaim True if the account can collect rewards for this token ID.
|
|
641
659
|
function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
|
|
642
660
|
|
|
643
|
-
/// @notice
|
|
661
|
+
/// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations
|
|
662
|
+
/// and their unvested rewards can be released via `releaseForfeitedRewards`.
|
|
644
663
|
/// @param hook The hook the token belongs to.
|
|
645
|
-
/// @param tokenId The
|
|
646
|
-
/// @return tokenWasBurned
|
|
664
|
+
/// @param tokenId The token ID to check.
|
|
665
|
+
/// @return tokenWasBurned True if the token has been burned.
|
|
647
666
|
function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
|
|
648
667
|
|
|
649
|
-
/// @notice The
|
|
668
|
+
/// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
|
|
669
|
+
/// For 721 distributors this is the tier's voting units; for token distributors this is delegated voting power.
|
|
650
670
|
/// @param hook The hook the token belongs to.
|
|
651
|
-
/// @param tokenId The ID of the token to get the
|
|
652
|
-
/// @return tokenStakeAmount The
|
|
671
|
+
/// @param tokenId The ID of the token to get the stake weight of.
|
|
672
|
+
/// @return tokenStakeAmount The stake weight represented by this token ID.
|
|
653
673
|
function _tokenStake(address hook, uint256 tokenId) internal view virtual returns (uint256 tokenStakeAmount);
|
|
654
674
|
|
|
655
|
-
/// @notice The total
|
|
675
|
+
/// @notice The total stake across all token IDs at a given block. Used as the denominator when calculating each
|
|
676
|
+
/// token ID's pro-rata share. For 721 distributors this is `getPastTotalSupply` from the checkpoints module;
|
|
677
|
+
/// for token distributors this is `getPastTotalSupply` from the IVotes token.
|
|
656
678
|
/// @param hook The hook to get the total stake for.
|
|
657
|
-
/// @param blockNumber The block number to
|
|
658
|
-
/// @return totalStakedAmount The total
|
|
679
|
+
/// @param blockNumber The block number to query (must be strictly in the past).
|
|
680
|
+
/// @return totalStakedAmount The total stake at the given block.
|
|
659
681
|
function _totalStake(address hook, uint256 blockNumber) internal view virtual returns (uint256 totalStakedAmount);
|
|
660
682
|
}
|
|
@@ -139,6 +139,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
139
139
|
function _canClaim(address hook, uint256 tokenId, address account) internal pure override returns (bool canClaim) {
|
|
140
140
|
hook; // Silence unused variable warning.
|
|
141
141
|
if (tokenId >> 160 != 0) revert JBTokenDistributor_InvalidTokenId();
|
|
142
|
+
// The high bits were checked above, so this cast recovers the encoded address.
|
|
143
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
142
144
|
canClaim = address(uint160(tokenId)) == account;
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -162,6 +164,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
162
164
|
/// @return tokenStakeAmount The delegated voting power at the round's snapshot block.
|
|
163
165
|
function _tokenStake(address hook, uint256 tokenId) internal view override returns (uint256 tokenStakeAmount) {
|
|
164
166
|
if (tokenId >> 160 != 0) revert JBTokenDistributor_InvalidTokenId();
|
|
167
|
+
// The high bits were checked above, so this cast recovers the encoded address.
|
|
168
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
165
169
|
tokenStakeAmount = IVotes(hook).getPastVotes(address(uint160(tokenId)), roundSnapshotBlock[currentRound()]);
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -5,7 +5,9 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
5
5
|
|
|
6
6
|
import {JBTokenSnapshotData} from "../structs/JBTokenSnapshotData.sol";
|
|
7
7
|
|
|
8
|
-
/// @notice
|
|
8
|
+
/// @notice Interface for round-based reward distributors with linear vesting. Stakers claim their share of a
|
|
9
|
+
/// distributable balance each round, and claimed amounts vest linearly over a configurable number of rounds.
|
|
10
|
+
/// Two implementations exist: `JBTokenDistributor` (IVotes token stakers) and `JB721Distributor` (NFT holders).
|
|
9
11
|
interface IJBDistributor {
|
|
10
12
|
//*********************************************************************//
|
|
11
13
|
// -------------------------------- events --------------------------- //
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
/// @
|
|
5
|
-
///
|
|
4
|
+
/// @notice A point-in-time snapshot of a reward token's state for a specific hook and round. The distributable
|
|
5
|
+
/// amount for the round is `balance - vestingAmount`.
|
|
6
|
+
/// @custom:member balance The total token balance held for the hook's stakers at snapshot time.
|
|
7
|
+
/// @custom:member vestingAmount The amount currently locked in vesting at snapshot time (not yet distributable).
|
|
6
8
|
struct JBTokenSnapshotData {
|
|
7
9
|
uint256 balance;
|
|
8
10
|
uint256 vestingAmount;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
/// @
|
|
5
|
-
///
|
|
6
|
-
/// @custom:member
|
|
4
|
+
/// @notice Tracks a single vesting entry for a staker token ID. Tokens vest linearly from the claim round to
|
|
5
|
+
/// `releaseRound`, and `shareClaimed` tracks how much has been collected so far (out of `MAX_SHARE = 100,000`).
|
|
6
|
+
/// @custom:member releaseRound The round at which the tokens are fully vested and 100% claimable.
|
|
7
|
+
/// @custom:member amount The original amount of reward tokens that were claimed (before any collection).
|
|
8
|
+
/// @custom:member shareClaimed The cumulative share collected so far (out of `MAX_SHARE`). Increases each
|
|
9
|
+
/// time `collectVestedRewards` is called.
|
|
7
10
|
struct JBVestingData {
|
|
8
11
|
uint256 releaseRound;
|
|
9
12
|
uint256 amount;
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Description
|
|
2
|
-
|
|
3
|
-
What does this PR do, how, and why?
|
|
4
|
-
|
|
5
|
-
## Risk Surface
|
|
6
|
-
|
|
7
|
-
What new trust boundary, failure mode, operational dependency, or integration caveat does this PR introduce or remove?
|
|
8
|
-
|
|
9
|
-
## RISKS.md Impact
|
|
10
|
-
|
|
11
|
-
- [ ] No runtime, admin, deployment, or integration risk surface changed
|
|
12
|
-
- [ ] Updated this repo's `RISKS.md`
|
|
13
|
-
- [ ] Updated `/v6/evm/RISKS.md` because ecosystem behavior changed
|
|
14
|
-
- [ ] If no `RISKS.md` update was needed, I explained why in this PR
|
|
15
|
-
|
|
16
|
-
Reference: [`/v6/evm/RISKS_MAINTENANCE.md`](../../RISKS_MAINTENANCE.md)
|
|
17
|
-
|
|
18
|
-
## Checklist
|
|
19
|
-
|
|
20
|
-
- [ ] Tests cover the behavior change
|
|
21
|
-
- [ ] Code is natspec'd where needed
|
|
22
|
-
- [ ] Code is linted and formatted
|
|
23
|
-
- [ ] I ran the relevant tests locally
|
|
24
|
-
- [ ] I checked for stale docs and updated them where needed
|
|
25
|
-
- [ ] `STYLE_GUIDE.md` is adhered to — no lint errors, warnings, or notes
|
|
26
|
-
- [ ] No build errors, warnings, or notes
|
|
27
|
-
|
|
28
|
-
## Interactions
|
|
29
|
-
|
|
30
|
-
These changes impact the following contracts or docs:
|
|
31
|
-
|
|
32
|
-
- Directly:
|
|
33
|
-
- Indirectly:
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: lint
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
branches:
|
|
5
|
-
- main
|
|
6
|
-
push:
|
|
7
|
-
branches:
|
|
8
|
-
- main
|
|
9
|
-
jobs:
|
|
10
|
-
forge-test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
with:
|
|
15
|
-
submodules: recursive
|
|
16
|
-
- name: Install Foundry
|
|
17
|
-
uses: foundry-rs/foundry-toolchain@v1
|
|
18
|
-
- name: Check linting
|
|
19
|
-
run: forge fmt --check
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: publish
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
branches:
|
|
5
|
-
- main
|
|
6
|
-
jobs:
|
|
7
|
-
publish:
|
|
8
|
-
runs-on: ubuntu-latest
|
|
9
|
-
steps:
|
|
10
|
-
- uses: actions/checkout@v4
|
|
11
|
-
- uses: actions/setup-node@v4
|
|
12
|
-
with:
|
|
13
|
-
node-version: 22.4.x
|
|
14
|
-
registry-url: https://registry.npmjs.org
|
|
15
|
-
# This will fail if the version in package.json has not increased.
|
|
16
|
-
- name: Publish to npm
|
|
17
|
-
run: npm publish --access public
|
|
18
|
-
env:
|
|
19
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
name: slither
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
branches: [main]
|
|
5
|
-
push:
|
|
6
|
-
branches: [main]
|
|
7
|
-
jobs:
|
|
8
|
-
slither:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
steps:
|
|
11
|
-
- uses: actions/checkout@v4
|
|
12
|
-
with:
|
|
13
|
-
submodules: recursive
|
|
14
|
-
- uses: actions/setup-node@v4
|
|
15
|
-
with:
|
|
16
|
-
node-version: 22.4.x
|
|
17
|
-
- name: Install npm dependencies
|
|
18
|
-
run: npm install --omit=dev
|
|
19
|
-
- name: Run Slither
|
|
20
|
-
uses: crytic/slither-action@v0.3.1
|
|
21
|
-
with:
|
|
22
|
-
slither-config: slither-ci.config.json
|
|
23
|
-
fail-on: medium
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
name: test
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
branches:
|
|
5
|
-
- main
|
|
6
|
-
push:
|
|
7
|
-
branches:
|
|
8
|
-
- main
|
|
9
|
-
jobs:
|
|
10
|
-
forge-test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
with:
|
|
15
|
-
submodules: recursive
|
|
16
|
-
- uses: actions/setup-node@v4
|
|
17
|
-
with:
|
|
18
|
-
node-version: 22.4.x
|
|
19
|
-
- name: Install npm dependencies
|
|
20
|
-
run: npm install --omit=dev
|
|
21
|
-
- name: Install Foundry
|
|
22
|
-
uses: foundry-rs/foundry-toolchain@v1
|
|
23
|
-
- name: Run tests
|
|
24
|
-
run: forge test --fail-fast --summary --detailed --skip "*/script/**"
|
|
25
|
-
env:
|
|
26
|
-
RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
|
|
27
|
-
- name: Check contract sizes
|
|
28
|
-
run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
|
package/.gitmodules
DELETED
package/ADMINISTRATION.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Administration
|
|
2
|
-
|
|
3
|
-
## At A Glance
|
|
4
|
-
|
|
5
|
-
| Item | Details |
|
|
6
|
-
| --- | --- |
|
|
7
|
-
| Scope | Round-based vesting and distribution configuration |
|
|
8
|
-
| Control posture | Mostly parameter- and caller-driven, with asset-specific authority checks |
|
|
9
|
-
| Highest-risk actions | Bad deployment parameters, wrong funding assumptions, and stale snapshot timing |
|
|
10
|
-
| Recovery posture | Some value can remain for future rounds, but bad parameters can brick an instance |
|
|
11
|
-
|
|
12
|
-
## Purpose
|
|
13
|
-
|
|
14
|
-
`nana-distributor-v6` has less admin complexity than many sibling repos, but deployment parameters and funding assumptions still create real control risk.
|
|
15
|
-
|
|
16
|
-
## Control Model
|
|
17
|
-
|
|
18
|
-
- vesting is mostly driven by deployment parameters and permissionless round starts
|
|
19
|
-
- claim authority differs by distributor type
|
|
20
|
-
- 721 forfeiture handling adds a separate recovery path not present in the token distributor
|
|
21
|
-
|
|
22
|
-
## Roles
|
|
23
|
-
|
|
24
|
-
| Role | How Assigned | Scope | Notes |
|
|
25
|
-
| --- | --- | --- | --- |
|
|
26
|
-
| Round starter | Any caller | Per distributor | Vesting is permissionless |
|
|
27
|
-
| Token claimant | Encoded claimant address | Per token slot | Token distributor authority model |
|
|
28
|
-
| NFT claimant | Current NFT owner | Per token ID | 721 distributor authority model |
|
|
29
|
-
|
|
30
|
-
## Privileged Surfaces
|
|
31
|
-
|
|
32
|
-
- deployment parameters
|
|
33
|
-
- funding flows
|
|
34
|
-
- claim entrypoints with distributor-specific authority checks
|
|
35
|
-
- 721 forfeiture release path
|
|
36
|
-
|
|
37
|
-
## Immutable And One-Way
|
|
38
|
-
|
|
39
|
-
- bad constructor parameters can permanently make an instance unusable
|
|
40
|
-
- snapshots define a round once taken
|
|
41
|
-
- vested or collected value does not rewind
|
|
42
|
-
|
|
43
|
-
## Operational Notes
|
|
44
|
-
|
|
45
|
-
- review round timing and vesting-round count before deployment
|
|
46
|
-
- verify the distributor holds the correct asset before starting rounds
|
|
47
|
-
- do not assume token and 721 variants behave identically
|
|
48
|
-
|
|
49
|
-
## Recovery
|
|
50
|
-
|
|
51
|
-
- unclaimed value can remain for future rounds
|
|
52
|
-
- 721 forfeiture release can recycle some value
|
|
53
|
-
- bad deployment parameters usually require a new distributor instance
|
|
54
|
-
|
|
55
|
-
## Admin Boundaries
|
|
56
|
-
|
|
57
|
-
- this repo does not create upstream entitlement logic
|
|
58
|
-
- permissionless vesting means operators do not fully control snapshot timing
|
|
59
|
-
- the distributor cannot make a missing or wrong stake source correct
|
|
60
|
-
|
|
61
|
-
## Source Map
|
|
62
|
-
|
|
63
|
-
- `src/JBDistributor.sol`
|
|
64
|
-
- `src/JBTokenDistributor.sol`
|
|
65
|
-
- `src/JB721Distributor.sol`
|