@bananapus/distributor-v6 0.0.31 → 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/README.md CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  `@bananapus/distributor-v6` distributes ERC-20 balances or 721 token inventories to many recipients under round-based vesting rules. It is a payout utility package for Juicebox-adjacent flows, not a protocol accounting layer.
4
4
 
5
- Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
- User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
7
- Skills: [SKILLS.md](./SKILLS.md)
8
- Risks: [RISKS.md](./RISKS.md)
9
- Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
10
- Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
5
+ ## Documentation
6
+
7
+ - [ARCHITECTURE.md](./ARCHITECTURE.md) — system overview, modules, trust boundaries, and core invariants.
8
+ - [USER_JOURNEYS.md](./USER_JOURNEYS.md) — end-to-end flows for funders and claimants across token and 721 variants.
9
+ - [INVARIANTS.md](./INVARIANTS.md) — per-section invariants for snapshot fairness, vesting math, claim authority, loans, and recycling.
10
+ - [RISKS.md](./RISKS.md) — risk register with priority risks and the minimum invariants to verify.
11
+ - [ADMINISTRATION.md](./ADMINISTRATION.md) — deployment parameters, control posture, and recovery guidance.
12
+ - [SKILLS.md](./SKILLS.md) — quick index for routing tasks into the right sub-document.
13
+ - [STYLE_GUIDE.md](./STYLE_GUIDE.md) — Solidity and repo conventions used across the Juicebox V6 ecosystem.
14
+ - [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md) — audit framing, targets, and suggested hunting grounds.
15
+ - [CHANGELOG.md](./CHANGELOG.md) — release notes and dependency bumps.
11
16
 
12
17
  ## Overview
13
18
 
@@ -39,10 +44,10 @@ If the issue is "where did the project's value come from?" start in `nana-core-v
39
44
  2. accepted funding is assigned to the current reward round for the chosen token or 721 stake source
40
45
  3. the distributor's immutable claim duration decides whether funded reward rounds expire
41
46
  4. the encoded token staker or current NFT owner later claims completed past reward rounds into a fresh vesting entry
42
- 5. anyone can burn expired unclaimed reward rounds after their deadline
47
+ 5. anyone can recycle expired unclaimed reward rounds after their deadline
43
48
  6. recipients collect their vested share as the configured vesting schedule unlocks
44
49
  7. eligible claimants can borrow against vesting revnet rewards without bypassing the vesting schedule
45
- 8. some unclaimable value can be burned through explicit cleanup paths, depending on the distributor type
50
+ 8. some unclaimable value can be recycled through explicit cleanup paths, depending on the distributor type
46
51
 
47
52
  This repo does not explain why an allocation exists. It only defines how funded inventory is handed out.
48
53
 
@@ -60,10 +65,9 @@ This repo does not explain why an allocation exists. It only defines how funded
60
65
  token rewards are claimed by the encoded staker address, while 721 rewards are claimed by the current NFT owner
61
66
  - `CLAIM_DURATION` is fixed at deployment; `0` means reward rounds do not expire, otherwise all funding paths use the
62
67
  same deadline measured from when the funded round first becomes claimable
63
- - `burnExpiredRewards` is permissionless and only burns the unclaimed remainder; already-materialized vesting entries
68
+ - `burnExpiredRewards` is permissionless and only recycles the unclaimed remainder; already-materialized vesting entries
64
69
  remain claimable on their normal vesting curve
65
- - expired and forfeited rewards are burned with `JBController.burnTokensOf`; rewards that are not registered project
66
- tokens in the configured controller cannot use those burn paths
70
+ - expired and forfeited rewards stay in distributor inventory and are recycled into the current reward round
67
71
  - revnet loan-backed vesting is opt-in at deployment; the reward token must be a REVOwner-owned revnet token, the
68
72
  distributor keeps the loan NFT, and repayment restores the original vesting schedule instead of releasing all
69
73
  collateral immediately
@@ -131,7 +135,7 @@ script/
131
135
  - operational mistakes often come from funding the wrong asset or underfunding the distributor
132
136
  - teams should review claim timing and snapshot assumptions with the same care they review the payout source
133
137
  - deployers that set a nonzero claim duration should choose a window long enough for expected claimants, because
134
- expired unclaimed rewards can be burned by anyone
138
+ expired unclaimed rewards can be recycled by anyone
135
139
 
136
140
  ## For AI Agents
137
141
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@bananapus/distributor-v6",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/Bananapus/nana-distributor-v6"
7
+ "url": "git+https://github.com/Bananapus/nana-distributor-v6.git"
8
8
  },
9
9
  "files": [
10
10
  "foundry.toml",
@@ -24,8 +24,8 @@
24
24
  "deploy:testnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks testnets"
25
25
  },
26
26
  "dependencies": {
27
- "@bananapus/721-hook-v6": "^0.0.57",
28
- "@bananapus/core-v6": "^0.0.68",
27
+ "@bananapus/721-hook-v6": "^0.0.59",
28
+ "@bananapus/core-v6": "^0.0.72",
29
29
  "@bananapus/permission-ids-v6": "^0.0.27",
30
30
  "@openzeppelin/contracts": "5.6.1",
31
31
  "@prb/math": "4.1.1",
@@ -29,7 +29,7 @@ import {JBVestingData} from "./structs/JBVestingData.sol";
29
29
  /// @dev Any project can use this distributor by configuring a payout split with
30
30
  /// `hook = this contract` and `beneficiary = address(their 721 hook)`.
31
31
  /// @dev The stake weight of each NFT is its tier's `votingUnits`. Burned NFTs are excluded from the total stake
32
- /// calculation and their unlocked forfeited rewards can be burned via `releaseForfeitedRewards`.
32
+ /// calculation and their unlocked forfeited rewards can be recycled via `releaseForfeitedRewards`.
33
33
  /// @dev Funded rewards are assigned to the funding round. NFT owners claim historical rounds lazily; all unclaimed
34
34
  /// past rewards begin vesting when the current NFT owner claims, not when the rewards were funded.
35
35
  /// @dev Implements `IJBSplitHook` so it can receive tokens directly from Juicebox project payout splits.
@@ -85,7 +85,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
85
85
  //*********************************************************************//
86
86
 
87
87
  /// @param directory The JB directory used to verify terminal/controller callers.
88
- /// @param controller The JB controller used to burn forfeited project-token rewards.
88
+ /// @param controller The JB controller used for token registry lookups and revnet loan permissions.
89
89
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
90
90
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
91
91
  /// @param initialRoundDuration The duration of each round, specified in seconds.
@@ -697,14 +697,11 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
697
697
  // Skip already-vested tokenIds — check if the last vesting entry targets the same release round.
698
698
  {
699
699
  // Load the number of existing vesting entries for this token.
700
- uint256 numVesting = vestingDataOf[ctx.hook][tokenId][ctx.token].length;
700
+ JBVestingData[] storage vestings = vestingDataOf[ctx.hook][tokenId][ctx.token];
701
+ uint256 numVesting = vestings.length;
701
702
 
702
703
  // If at least one entry exists and its release round matches, this token was already vested this round.
703
- if (
704
- numVesting != 0
705
- && vestingDataOf[ctx.hook][tokenId][ctx.token][numVesting - 1].releaseRound
706
- == ctx.vestingReleaseRound
707
- ) {
704
+ if (numVesting != 0 && vestings[numVesting - 1].releaseRound == ctx.vestingReleaseRound) {
708
705
  return (0, newUniqueCount);
709
706
  }
710
707
  }
@@ -95,9 +95,6 @@ abstract contract JBDistributor is IJBDistributor {
95
95
  /// @notice Thrown when vesting loans are requested from a distributor with no vesting period.
96
96
  error JBDistributor_VestingLoansDisabled();
97
97
 
98
- /// @notice Thrown when rewards cannot be burned by the JB controller.
99
- error JBDistributor_TokenNotBurnable(address token);
100
-
101
98
  /// @notice Thrown when a value cannot fit in a uint208 reward-round field.
102
99
  error JBDistributor_Uint208Overflow(uint256 value);
103
100
 
@@ -126,7 +123,7 @@ abstract contract JBDistributor is IJBDistributor {
126
123
  /// @dev A zero duration means reward rounds do not expire.
127
124
  uint48 public immutable override CLAIM_DURATION;
128
125
 
129
- /// @notice The JB controller used to burn forfeited project-token rewards.
126
+ /// @notice The JB controller used for token registry lookups and revnet loan permissions.
130
127
  IJBController public immutable override CONTROLLER;
131
128
 
132
129
  /// @notice The duration of each round, specified in seconds.
@@ -231,7 +228,7 @@ abstract contract JBDistributor is IJBDistributor {
231
228
  // -------------------------- constructor ---------------------------- //
232
229
  //*********************************************************************//
233
230
 
234
- /// @param controller The JB controller used to burn forfeited project-token rewards.
231
+ /// @param controller The JB controller used for token registry lookups and revnet loan permissions.
235
232
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
236
233
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
237
234
  /// @param initialRoundDuration The duration of each round, specified in seconds.
@@ -389,13 +386,12 @@ abstract contract JBDistributor is IJBDistributor {
389
386
  _ensureSnapshotBlock(currentRound());
390
387
  }
391
388
 
392
- /// @notice Burn unlocked rewards tied to burned tokens. When an NFT is burned, its pending vesting entries become
393
- /// stranded this function unlocks and burns them instead of sending them to the beneficiary. Anyone can call
394
- /// this for burned tokens.
389
+ /// @notice Recycle unlocked rewards tied to burned tokens into the current reward round.
390
+ /// @dev Anyone can call this for burned tokens.
395
391
  /// @param hook The hook whose tokens were burned.
396
392
  /// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
397
- /// @param tokens The reward tokens to release.
398
- /// @param beneficiary Unused for forfeiture — tokens are burned. Kept for interface compatibility.
393
+ /// @param tokens The reward tokens to recycle.
394
+ /// @param beneficiary Unused for forfeiture. Kept for interface compatibility.
399
395
  function releaseForfeitedRewards(
400
396
  address hook,
401
397
  uint256[] calldata tokenIds,
@@ -408,7 +404,7 @@ abstract contract JBDistributor is IJBDistributor {
408
404
  // Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
409
405
  _requireNotAcceptingToken();
410
406
 
411
- // Make sure that all tokens are burned.
407
+ // Make sure that all staker token IDs are burned.
412
408
  for (uint256 i; i < tokenIds.length;) {
413
409
  if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
414
410
  revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
@@ -418,7 +414,7 @@ abstract contract JBDistributor is IJBDistributor {
418
414
  }
419
415
  }
420
416
 
421
- // Unlock the rewards and burn the forfeited amount.
417
+ // Unlock the rewards and recycle the forfeited amount.
422
418
  _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: false});
423
419
  }
424
420
 
@@ -477,14 +473,15 @@ abstract contract JBDistributor is IJBDistributor {
477
473
  // Keep a reference to the latest vested index.
478
474
  uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
479
475
 
480
- // Keep a reference to the number of vesting rounds for the tokenId and token.
481
- uint256 numberOfVestingRounds = vestingDataOf[hook][tokenId][token].length;
476
+ // Keep a reference to the vesting data array.
477
+ JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
478
+ uint256 numberOfVestingRounds = vestings.length;
482
479
 
483
480
  while (vestedIndex < numberOfVestingRounds) {
484
481
  uint256 lockedShare;
485
482
 
486
483
  // Keep a reference to the vested data being iterated on.
487
- JBVestingData memory vesting = vestingDataOf[hook][tokenId][token][vestedIndex];
484
+ JBVestingData memory vesting = vestings[vestedIndex];
488
485
 
489
486
  lockedShare = JBVestingMath.lockedShareOf({
490
487
  releaseRound: vesting.releaseRound,
@@ -1210,33 +1207,6 @@ abstract contract JBDistributor is IJBDistributor {
1210
1207
  });
1211
1208
  }
1212
1209
 
1213
- /// @notice Burn reward inventory using the JB controller.
1214
- /// @param hook The hook whose tracked balance is being burned.
1215
- /// @param token The reward token to burn.
1216
- /// @param amount The amount to burn.
1217
- function _burnRewardTokens(address hook, IERC20 token, uint256 amount) internal {
1218
- // No-op zero burns so callers can batch empty or already-settled rounds safely.
1219
- if (amount == 0) return;
1220
-
1221
- // A missing controller means there is no burn authority for any reward token.
1222
- if (address(CONTROLLER) == address(0)) revert JBDistributor_TokenNotBurnable({token: address(token)});
1223
-
1224
- // Only JB project tokens can be burned through `JBController.burnTokensOf`.
1225
- uint256 projectId = CONTROLLER.TOKENS().projectIdOf({token: IJBToken(address(token))});
1226
-
1227
- // Revert instead of sending unsupported rewards to a burn address.
1228
- if (projectId == 0) revert JBDistributor_TokenNotBurnable({token: address(token)});
1229
-
1230
- // Remove the burned amount from the hook's reward inventory.
1231
- _balanceOf[hook][token] -= amount;
1232
-
1233
- // Remove the same amount from the global inventory tracked for this token.
1234
- _accountedBalanceOf[token] -= amount;
1235
-
1236
- // Burn from this distributor's project-token balance or token credits.
1237
- CONTROLLER.burnTokensOf({holder: address(this), projectId: projectId, tokenCount: amount, memo: ""});
1238
- }
1239
-
1240
1210
  /// @notice Resolve the revnet project ID for a reward token.
1241
1211
  /// @param token The reward token to resolve.
1242
1212
  /// @return revnetId The token's revnet project ID.
@@ -1397,8 +1367,11 @@ abstract contract JBDistributor is IJBDistributor {
1397
1367
  token.safeTransfer({to: beneficiary, value: totalTokenAmount});
1398
1368
  }
1399
1369
  } else {
1400
- // If forfeiture: remove the unlocked amount from inventory and burn it through the JB controller.
1401
- _burnRewardTokens({hook: hook, token: token, amount: totalTokenAmount});
1370
+ // If forfeiture: keep inventory in the distributor and give the current staker set a fresh round.
1371
+ _recordRewardRound({hook: hook, token: token, amount: totalTokenAmount});
1372
+ emit ForfeitedRewardsRecycled({
1373
+ hook: hook, round: round, token: token, amount: totalTokenAmount, caller: msg.sender
1374
+ });
1402
1375
  }
1403
1376
  }
1404
1377
 
@@ -1594,12 +1567,13 @@ abstract contract JBDistributor is IJBDistributor {
1594
1567
  // Keep a reference to the latest fully vested index.
1595
1568
  uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
1596
1569
 
1597
- // Keep a reference to the number of vesting entries for the token ID and token.
1598
- uint256 numberOfVestingRounds = vestingDataOf[hook][tokenId][token].length;
1570
+ // Keep a reference to the vesting data array.
1571
+ JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
1572
+ uint256 numberOfVestingRounds = vestings.length;
1599
1573
 
1600
1574
  while (vestedIndex < numberOfVestingRounds) {
1601
1575
  // Keep a reference to the vested data being iterated on.
1602
- JBVestingData memory vesting = vestingDataOf[hook][tokenId][token][vestedIndex];
1576
+ JBVestingData memory vesting = vestings[vestedIndex];
1603
1577
 
1604
1578
  // Use `original - alreadyPaid` to include rounding dust in the remaining amount.
1605
1579
  tokenAmount += JBVestingMath.unclaimedAmountOf({
@@ -1641,8 +1615,8 @@ abstract contract JBDistributor is IJBDistributor {
1641
1615
  }
1642
1616
  }
1643
1617
 
1644
- /// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations
1645
- /// and their unvested rewards can be released via `releaseForfeitedRewards`.
1618
+ /// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations,
1619
+ /// and their unlocked forfeited rewards can be recycled via `releaseForfeitedRewards`.
1646
1620
  /// @param hook The hook the token belongs to.
1647
1621
  /// @param tokenId The token ID to check.
1648
1622
  /// @return tokenWasBurned True if the token has been burned.
@@ -69,7 +69,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
69
69
  //*********************************************************************//
70
70
 
71
71
  /// @param directory The JB directory used to verify terminal/controller callers.
72
- /// @param controller The JB controller used to burn forfeited project-token rewards.
72
+ /// @param controller The JB controller used for token registry lookups and revnet loan permissions.
73
73
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
74
74
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
75
75
  /// @param initialRoundDuration The duration of each round, specified in seconds.
@@ -97,6 +97,16 @@ interface IJBDistributor {
97
97
  address caller
98
98
  );
99
99
 
100
+ /// @notice Emitted when unlocked rewards from burned tokens are recycled into the current reward round.
101
+ /// @param hook The hook whose forfeited rewards were recycled.
102
+ /// @param round The reward round receiving the recycled rewards.
103
+ /// @param token The reward token that was recycled.
104
+ /// @param amount The forfeited reward amount recycled.
105
+ /// @param caller The address that triggered the recycle.
106
+ event ForfeitedRewardsRecycled(
107
+ address indexed hook, uint256 indexed round, IERC20 indexed token, uint256 amount, address caller
108
+ );
109
+
100
110
  /// @notice Emitted when a liquidated distributor-held Revnet loan is written off.
101
111
  /// @param hook The hook whose vesting rewards were collateralized.
102
112
  /// @param tokenId The token ID whose vesting rewards were collateralized.
@@ -153,7 +163,7 @@ interface IJBDistributor {
153
163
  /// @dev A zero duration means reward rounds do not expire.
154
164
  function CLAIM_DURATION() external view returns (uint48);
155
165
 
156
- /// @notice The JB controller used to burn forfeited project-token rewards.
166
+ /// @notice The JB controller used for token registry lookups and revnet loan permissions.
157
167
  function CONTROLLER() external view returns (IJBController);
158
168
 
159
169
  /// @notice The duration of each round, specified in seconds.
@@ -307,10 +317,10 @@ interface IJBDistributor {
307
317
  payable
308
318
  returns (uint256 paidOffLoanId);
309
319
 
310
- /// @notice Burn unlocked rewards for burned tokens.
320
+ /// @notice Recycle unlocked rewards from burned tokens into the current reward round.
311
321
  /// @param hook The hook whose tokens were burned.
312
322
  /// @param tokenIds The IDs of the burned tokens.
313
- /// @param tokens The addresses of the tokens to burn.
323
+ /// @param tokens The reward tokens to recycle.
314
324
  /// @param beneficiary Unused for forfeiture.
315
325
  function releaseForfeitedRewards(
316
326
  address hook,
@@ -5,7 +5,7 @@ pragma solidity ^0.8.0;
5
5
  /// @custom:member amount The reward amount assigned to the round.
6
6
  /// @custom:member snapshotBlock The block used for per-account historical stake lookups.
7
7
  /// @custom:member claimedAmount The reward amount already materialized into vesting.
8
- /// @custom:member claimDeadline The timestamp at which unclaimed rewards can be burned. Zero means no expiration.
8
+ /// @custom:member claimDeadline The timestamp at which unclaimed rewards can be recycled. Zero means no expiration.
9
9
  /// @custom:member totalStake The aggregate stake at the round's snapshot block.
10
10
  struct JBRewardRoundData {
11
11
  uint208 amount;