@bananapus/distributor-v6 0.0.6 → 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 +2 -2
- package/package.json +16 -7
- package/src/JB721Distributor.sol +55 -18
- package/src/JBTokenDistributor.sol +10 -0
- 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 -2054
- package/test/JBTokenDistributor.t.sol +0 -503
- package/test/audit/CodexNemesisAccountingPoC.t.sol +0 -339
- package/test/audit/CodexNemesisPoC.t.sol +0 -191
- package/test/audit/H26VotingPowerCap.t.sol +0 -338
- package/test/audit/Pass12Fixes.t.sol +0 -344
- package/test/fork/TokenDistributorFork.t.sol +0 -603
- package/test/invariant/JB721DistributorInvariant.t.sol +0 -409
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.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": "
|
|
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
|
@@ -6,6 +6,7 @@ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Tiers
|
|
|
6
6
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
7
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
8
8
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
9
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
9
10
|
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
10
11
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
11
12
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
@@ -32,6 +33,9 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
32
33
|
// --------------------------- custom errors ------------------------- //
|
|
33
34
|
//*********************************************************************//
|
|
34
35
|
|
|
36
|
+
/// @notice Thrown when native ETH is sent but context.token is not NATIVE_TOKEN.
|
|
37
|
+
error JB721Distributor_TokenMismatch();
|
|
38
|
+
|
|
35
39
|
/// @notice Thrown when the caller is not a terminal or controller for the project.
|
|
36
40
|
error JB721Distributor_Unauthorized();
|
|
37
41
|
|
|
@@ -133,6 +137,8 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
133
137
|
_balanceOf[hook][IERC20(context.token)] += context.amount;
|
|
134
138
|
}
|
|
135
139
|
} else if (msg.value != 0) {
|
|
140
|
+
// Validate that context.token matches NATIVE_TOKEN to prevent cross-booking attacks.
|
|
141
|
+
if (context.token != JBConstants.NATIVE_TOKEN) revert JB721Distributor_TokenMismatch();
|
|
136
142
|
// Native ETH: credit actual value received.
|
|
137
143
|
_balanceOf[hook][IERC20(context.token)] += msg.value;
|
|
138
144
|
_accountedBalanceOf[IERC20(context.token)] += msg.value;
|
|
@@ -250,8 +256,8 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
250
256
|
|
|
251
257
|
/// @notice The stake weight of a given NFT token ID based on its tier's voting units, validated against historical
|
|
252
258
|
/// state.
|
|
253
|
-
/// @dev Returns 0 if the token
|
|
254
|
-
/// 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.
|
|
255
261
|
/// @param hook The hook the token belongs to.
|
|
256
262
|
/// @param tokenId The ID of the token to get the stake weight of.
|
|
257
263
|
/// @return tokenStakeAmount The voting units of the token's tier (or 0 if ineligible).
|
|
@@ -259,16 +265,17 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
259
265
|
uint256 votingUnits =
|
|
260
266
|
IJB721TiersHook(hook)
|
|
261
267
|
.STORE()
|
|
262
|
-
.tierOfTokenId({hook: hook, tokenId: tokenId, includeResolvedUri: false})
|
|
263
|
-
.votingUnits;
|
|
268
|
+
.tierOfTokenId({hook: hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
264
269
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
277
|
+
uint256 pastVotes = IVotes(address(IJB721TiersHook(hook).CHECKPOINTS()))
|
|
278
|
+
.getPastVotes({account: owner, timepoint: snapshotBlock});
|
|
272
279
|
|
|
273
280
|
// If the owner had no voting power at round start, the token is ineligible.
|
|
274
281
|
// slither-disable-next-line incorrect-equality
|
|
@@ -295,6 +302,35 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
295
302
|
// ----------------------- private helpers --------------------------- //
|
|
296
303
|
//*********************************************************************//
|
|
297
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
|
+
|
|
298
334
|
/// @notice Vest a single NFT token, enforcing a per-owner voting power cap across the batch.
|
|
299
335
|
/// @dev Returns 0 for burned tokens, already-vested tokens, tokens whose owner had no snapshot voting power,
|
|
300
336
|
/// and tokens whose owner has already exhausted their voting power cap within this batch.
|
|
@@ -344,21 +380,22 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
344
380
|
uint256 votingUnits =
|
|
345
381
|
IJB721TiersHook(ctx.hook)
|
|
346
382
|
.STORE()
|
|
347
|
-
.tierOfTokenId({hook: ctx.hook, tokenId: tokenId, includeResolvedUri: false})
|
|
348
|
-
.votingUnits;
|
|
383
|
+
.tierOfTokenId({hook: ctx.hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
349
384
|
|
|
350
|
-
// 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.
|
|
351
386
|
uint256 ownerIndex;
|
|
352
387
|
uint256 pastVotes;
|
|
353
388
|
{
|
|
354
|
-
//
|
|
355
|
-
|
|
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);
|
|
356
393
|
|
|
357
394
|
// Query the owner's checkpointed voting power at the round's snapshot block.
|
|
358
395
|
pastVotes = IVotes(address(IJB721TiersHook(ctx.hook).CHECKPOINTS()))
|
|
359
|
-
.getPastVotes({account: owner, timepoint:
|
|
396
|
+
.getPastVotes({account: owner, timepoint: snapshotBlock});
|
|
360
397
|
|
|
361
|
-
// 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.
|
|
362
399
|
// slither-disable-next-line incorrect-equality
|
|
363
400
|
if (pastVotes == 0) return (0, newUniqueCount);
|
|
364
401
|
|
|
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
5
5
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
6
6
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
7
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
7
8
|
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
8
9
|
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
9
10
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
@@ -30,6 +31,9 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
30
31
|
/// @notice Thrown when a tokenId has non-zero upper bits (above 160), which would alias to the same staker address.
|
|
31
32
|
error JBTokenDistributor_InvalidTokenId();
|
|
32
33
|
|
|
34
|
+
/// @notice Thrown when native ETH is sent but context.token is not NATIVE_TOKEN.
|
|
35
|
+
error JBTokenDistributor_TokenMismatch();
|
|
36
|
+
|
|
33
37
|
/// @notice Thrown when the caller is not a terminal or controller for the project.
|
|
34
38
|
error JBTokenDistributor_Unauthorized();
|
|
35
39
|
|
|
@@ -102,6 +106,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
102
106
|
_balanceOf[hook][IERC20(context.token)] += context.amount;
|
|
103
107
|
}
|
|
104
108
|
} else if (msg.value != 0) {
|
|
109
|
+
// Validate that context.token matches NATIVE_TOKEN to prevent cross-booking attacks.
|
|
110
|
+
if (context.token != JBConstants.NATIVE_TOKEN) revert JBTokenDistributor_TokenMismatch();
|
|
105
111
|
// Native ETH: credit actual value received.
|
|
106
112
|
_balanceOf[hook][IERC20(context.token)] += msg.value;
|
|
107
113
|
_accountedBalanceOf[IERC20(context.token)] += msg.value;
|
|
@@ -133,6 +139,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
133
139
|
function _canClaim(address hook, uint256 tokenId, address account) internal pure override returns (bool canClaim) {
|
|
134
140
|
hook; // Silence unused variable warning.
|
|
135
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)
|
|
136
144
|
canClaim = address(uint160(tokenId)) == account;
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -156,6 +164,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
156
164
|
/// @return tokenStakeAmount The delegated voting power at the round's snapshot block.
|
|
157
165
|
function _tokenStake(address hook, uint256 tokenId) internal view override returns (uint256 tokenStakeAmount) {
|
|
158
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)
|
|
159
169
|
tokenStakeAmount = IVotes(hook).getPastVotes(address(uint160(tokenId)), roundSnapshotBlock[currentRound()]);
|
|
160
170
|
}
|
|
161
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
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`
|
package/AUDIT_INSTRUCTIONS.md
DELETED
|
@@ -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.
|