@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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
28
|
-
"@bananapus/core-v6": "^0.0.
|
|
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",
|
package/src/JB721Distributor.sol
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/JBDistributor.sol
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
393
|
-
///
|
|
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
|
|
398
|
-
/// @param beneficiary Unused for forfeiture
|
|
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
|
|
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
|
|
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
|
|
481
|
-
|
|
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 =
|
|
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:
|
|
1401
|
-
|
|
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
|
|
1598
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|