@bananapus/distributor-v6 0.0.7 → 0.0.8

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 CHANGED
@@ -80,8 +80,8 @@ npm install @bananapus/distributor-v6
80
80
 
81
81
  ```bash
82
82
  npm install
83
- forge build
84
- forge test
83
+ forge build --deny notes
84
+ forge test --deny notes
85
85
  ```
86
86
 
87
87
  Useful scripts:
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@bananapus/distributor-v6",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
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": "^0.0.38",
21
- "@bananapus/core-v6": "^0.0.36",
22
- "@bananapus/permission-ids-v6": "^0.0.19",
23
- "@openzeppelin/contracts": "^5.6.1",
24
- "@prb/math": "^4.1.0"
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": "^0.33.2"
36
+ "@sphinx-labs/plugins": "0.33.3"
28
37
  }
29
38
  }
@@ -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's current owner had no checkpointed voting power at the round's snapshot block,
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
- // Use the checkpoints module to verify the token's owner had voting power at the round's snapshot block.
271
- // If they had no voting power at that time, this token was minted or acquired after the round started
272
- // and is not eligible for this round's rewards.
273
- address owner = IERC721(hook).ownerOf(tokenId);
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: roundSnapshotBlock[currentRound()]});
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
- // Get the current owner of the NFT.
358
- address owner = IERC721(ctx.hook).ownerOf(tokenId);
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: roundSnapshotBlock[currentRound()]});
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
 
@@ -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
 
@@ -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
@@ -1,3 +0,0 @@
1
- [submodule "lib/forge-std"]
2
- path = lib/forge-std
3
- url = https://github.com/foundry-rs/forge-std
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`
package/ARCHITECTURE.md DELETED
@@ -1,89 +0,0 @@
1
- # Architecture
2
-
3
- ## Purpose
4
-
5
- `nana-distributor-v6` provides round-based vesting and claiming for already-owned assets. It supports both `IVotes`-based ERC-20 distributions and 721-based distributions without becoming a treasury or accounting layer.
6
-
7
- ## System Overview
8
-
9
- `JBDistributor` is the shared vesting engine. `JBTokenDistributor` changes stake measurement to checkpointed voting power. `JB721Distributor` changes stake measurement to checkpointed voting power from the hook's `CHECKPOINTS()` module, ensuring only NFTs held at round start are eligible.
10
-
11
- Both variants can be used as `IJBSplitHook` receivers.
12
-
13
- ## Core Invariants
14
-
15
- - snapshot timing must stay coherent
16
- - tracked funded balance must cover current vesting obligations
17
- - claim authority must match the distributor type
18
- - 721 forfeiture handling must not over-allocate or burn value accidentally
19
- - token and 721 variants must preserve the same core vesting math
20
-
21
- ## Modules
22
-
23
- | Module | Responsibility | Notes |
24
- | --- | --- | --- |
25
- | `JBDistributor` | Shared rounds, vesting, snapshots, and claims | Economic core |
26
- | `JBTokenDistributor` | ERC-20 distribution using `IVotes` checkpoints | Token stake source |
27
- | `JB721Distributor` | NFT distribution using checkpointed voting power | 721 stake source |
28
-
29
- ## Trust Boundaries
30
-
31
- - split-hook caller authentication depends on `JBDirectory`
32
- - `JBTokenDistributor` trusts `IVotes` checkpoint history
33
- - `JB721Distributor` trusts the 721 hook's `CHECKPOINTS()` module for historical voting power and the store for tier metadata
34
- - upstream entitlement logic still lives outside this repo
35
-
36
- ## Critical Flows
37
-
38
- ### Begin Vesting
39
-
40
- ```text
41
- funded distributor
42
- -> begin a round
43
- -> snapshot stake and tracked balance for that round
44
- -> record vesting entries for the requested token IDs
45
- ```
46
-
47
- ### Collect
48
-
49
- ```text
50
- claimant
51
- -> prove authority for the token ID or encoded claimant slot
52
- -> compute unlocked share
53
- -> transfer the vested amount
54
- ```
55
-
56
- ## Accounting Model
57
-
58
- This repo owns vesting-round accounting. It does not own upstream treasury accounting or entitlement creation.
59
-
60
- The main variables are snapshot balance, total vesting amount, and the stake source used to split each round.
61
-
62
- ## Security Model
63
-
64
- - wrong snapshots can misallocate a whole round
65
- - bad constructor parameters can brick a distributor instance
66
- - split-funding caller assumptions matter because `processSplitWith` distinguishes pull and pre-sent flows
67
- - 721 and token variants intentionally differ in authority and forfeiture behavior
68
-
69
- ## Safe Change Guide
70
-
71
- - review snapshot timing and vesting math together
72
- - if claim authority changes, re-check both distributor variants separately
73
- - if funding semantics change, test terminal-style and controller-style flows explicitly
74
-
75
- ## Canonical Checks
76
-
77
- - token distribution behavior:
78
- `test/JBTokenDistributor.t.sol`
79
- - 721 distribution behavior:
80
- `test/JB721Distributor.t.sol`
81
- - 721 invariants:
82
- `test/invariant/JB721DistributorInvariant.t.sol`
83
-
84
- ## Source Map
85
-
86
- - `src/JBDistributor.sol`
87
- - `src/JBTokenDistributor.sol`
88
- - `src/JB721Distributor.sol`
89
- - `src/interfaces/IJBDistributor.sol`
@@ -1,52 +0,0 @@
1
- # Audit Instructions
2
-
3
- This repo is a shared vesting engine plus two concrete distributor variants. Audit it as payout logic whose main risks are snapshot timing, stake measurement, and funding assumptions.
4
-
5
- ## Audit Objective
6
-
7
- Find issues that:
8
-
9
- - misallocate rewards because the snapshot or stake source is wrong
10
- - break vesting or claiming because parameters are invalid
11
- - let caller or claim authority drift from the intended model
12
- - make split-funding assumptions unsafe
13
-
14
- ## Scope
15
-
16
- In scope:
17
-
18
- - `src/JBDistributor.sol`
19
- - `src/JBTokenDistributor.sol`
20
- - `src/JB721Distributor.sol`
21
- - interfaces and structs under `src/`
22
-
23
- ## Start Here
24
-
25
- 1. `src/JBDistributor.sol`
26
- 2. `src/JBTokenDistributor.sol`
27
- 3. `src/JB721Distributor.sol`
28
-
29
- ## Security Model
30
-
31
- The shared distributor:
32
-
33
- - snapshots balance and stake for a round
34
- - tracks vesting obligations
35
- - lets authorized claimants collect what has unlocked
36
-
37
- The concrete variants only change how stake and claimant authority are measured.
38
-
39
- ## Critical Invariants
40
-
41
- 1. Snapshot and stake source stay coherent.
42
- A round should not allocate more or less than the chosen stake source supports.
43
- 2. Tracked balance covers vesting obligations.
44
- Current and future vesting must reconcile with funded inventory.
45
- 3. Claim authority matches distributor type.
46
- The token and 721 variants must enforce their distinct authority models correctly.
47
-
48
- ## Verification
49
-
50
- - `npm install`
51
- - `forge build`
52
- - `forge test`
package/RISKS.md DELETED
@@ -1,78 +0,0 @@
1
- # Distributor Risk Register
2
-
3
- This file covers the shared vesting engine in `JBDistributor` and the two concrete payout-split receivers, `JB721Distributor` and `JBTokenDistributor`.
4
-
5
- ## How To Use This File
6
-
7
- - Read `Priority risks` first. Those are the failure modes with the highest payout-integrity impact.
8
- - Treat the shared `JBDistributor` logic as the economic core.
9
- - Use `Invariants to verify` as the minimum test envelope before routing live splits through a distributor.
10
-
11
- ## Priority Risks
12
-
13
- | Priority | Risk | Why it matters | Primary controls |
14
- |----------|------|----------------|------------------|
15
- | P0 | Wrong stake snapshot or stale stake source | A bad stake reading misallocates rewards for an entire round. | Snapshot review, invariants, and careful integration with the chosen hook or `IVotes` token. |
16
- | P1 | Zero-stake or bad-parameter deployment | Bad constructor inputs or zero total stake can make core flows revert. | Deployment-time validation and operator runbooks. |
17
- | P1 | Split funding trust mismatch | `processSplitWith` distinguishes terminal-style pull flows from controller-style pre-sent flows. | Restrict callers and test both paths. |
18
-
19
- ## 1. Trust Assumptions
20
-
21
- - **`JBDirectory` is trusted.**
22
- - **Stake sources are trusted.**
23
- - **Deployment parameters must be sane.**
24
-
25
- ## 2. Economic Risks
26
-
27
- - **Round snapshot timing has a zero-balance edge case.**
28
- - **Unclaimed value stays in the pool.**
29
- - **Partial-round claims are linear, not cliff-based.**
30
- - **Forfeited 721 rewards are recycled, not burned.**
31
- - **Undelegated `IVotes` balances can dilute participation.**
32
-
33
- ## 3. Access Control And Caller Risks
34
-
35
- - **Vesting is permissionless.**
36
- - **Claim authority differs by distributor type.**
37
- - **721 claim batches are brittle to invalid token IDs.**
38
- - **Forfeiture release is effectively 721-only.**
39
- - **Split-hook entry is tightly gated.**
40
-
41
- ## 4. DoS And Liveness Risks
42
-
43
- - **Zero stake reverts vesting.**
44
- - **Zero distributable balance reverts vesting.** The `beginVesting` call reverts with `JBDistributor_NothingToDistribute` if the distributable balance for a token is zero.
45
- - **Bad constructor parameters can brick the instance.**
46
- - **Resolver or token callback failures can block collection.**
47
-
48
- ## 5. Integration Risks
49
-
50
- - **Controller-vs-terminal split funding heuristic matters.**
51
- - **Fee-on-transfer handling uses balance-delta accounting.** Both terminal-pull and controller-pre-send paths measure `balanceAfter - balanceBefore` to credit the actual received amount.
52
- - **721 stake weights depend on checkpointed voting power at round start.** The `CHECKPOINTS()` module must be deployed and delegates must be set before the round snapshot block, or stakers receive zero weight.
53
- - **721 vesting and claiming treat burned tokens differently.**
54
- - **Checkpoint availability matters for both `IVotes` token distributors and 721 distributors.**
55
- - **Token distributor rejects token IDs with non-zero upper bits** (above 160) to prevent aliasing to the same staker address.
56
-
57
- ## 6. Invariants To Verify
58
-
59
- - `totalVestingAmountOf <= _balanceOf`
60
- - collections plus remaining vesting plus future distributable balance never exceed tracked funded balance
61
- - non-zero round snapshots stay stable within a round
62
- - `latestVestedIndexOf` advances contiguously
63
- - burned NFTs are excluded from 721 stake (via zero checkpointed votes) and only recycled through the explicit forfeiture path
64
- - only the encoded address can collect from the token distributor
65
-
66
- ## 7. Accepted Behaviors
67
-
68
- ### 7.1 Anyone can trigger a round snapshot
69
-
70
- This improves liveness, but it also means operators do not fully control the exact block when a round is crystallized.
71
-
72
- ### 7.2 Rewards can remain undistributed when stake is missing
73
-
74
- If some potential participants have zero effective stake for a round, the corresponding value stays in the distributor for future rounds.
75
-
76
- ### 7.3 721 and `IVotes` variants intentionally differ
77
-
78
- They share the vesting engine but not the same ownership model.
package/SKILLS.md DELETED
@@ -1,36 +0,0 @@
1
- # Juicebox Distributor
2
-
3
- ## Use This File For
4
-
5
- - Use this file when the task involves round-based vesting, split-hook distribution, or snapshot-based payout allocation.
6
- - Start here, then decide whether the issue is in shared vesting logic, `IVotes`-based stake measurement, or 721-based stake measurement.
7
-
8
- ## Read This Next
9
-
10
- | If you need... | Open this next |
11
- |---|---|
12
- | Repo overview and architecture | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
13
- | Shared vesting engine | [`src/JBDistributor.sol`](./src/JBDistributor.sol), [`src/interfaces/IJBDistributor.sol`](./src/interfaces/IJBDistributor.sol) |
14
- | Token distributor behavior | [`src/JBTokenDistributor.sol`](./src/JBTokenDistributor.sol) |
15
- | 721 distributor behavior | [`src/JB721Distributor.sol`](./src/JB721Distributor.sol) |
16
- | Types and structs | [`src/structs/`](./src/structs/) |
17
- | Main tests | [`test/JBTokenDistributor.t.sol`](./test/JBTokenDistributor.t.sol), [`test/JB721Distributor.t.sol`](./test/JB721Distributor.t.sol), [`test/invariant/JB721DistributorInvariant.t.sol`](./test/invariant/JB721DistributorInvariant.t.sol) |
18
-
19
- ## Repo Map
20
-
21
- | Area | Where to look |
22
- |---|---|
23
- | Main contracts | [`src/`](./src/) |
24
- | Structs and interfaces | [`src/structs/`](./src/structs/), [`src/interfaces/`](./src/interfaces/) |
25
- | Tests | [`test/`](./test/) |
26
-
27
- ## Purpose
28
-
29
- Shared vesting and distribution engine for ERC-20 and 721-based payout flows.
30
-
31
- ## Working Rules
32
-
33
- - Start in [`src/JBDistributor.sol`](./src/JBDistributor.sol) for shared round logic.
34
- - Treat snapshot timing as part of correctness.
35
- - `JBTokenDistributor` and `JB721Distributor` share a vesting engine but not the same ownership model.
36
- - Verify the distributor actually holds the asset it is meant to vest before reasoning about payout correctness.