@bananapus/distributor-v6 0.0.28 → 0.0.29
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 +19 -7
- package/foundry.toml +1 -0
- package/package.json +3 -2
- package/script/Deploy.s.sol +20 -1
- package/src/JB721Distributor.sol +19 -8
- package/src/JBDistributor.sol +679 -107
- package/src/JBTokenDistributor.sol +18 -7
- package/src/interfaces/IJBDistributor.sol +140 -17
- package/src/structs/JBBorrowContext.sol +24 -0
- package/src/structs/JBVestingLoan.sol +18 -0
package/README.md
CHANGED
|
@@ -37,11 +37,12 @@ If the issue is "where did the project's value come from?" start in `nana-core-v
|
|
|
37
37
|
|
|
38
38
|
1. a project funds the distributor, often through a payout split
|
|
39
39
|
2. accepted funding is assigned to the current reward round for the chosen token or 721 stake source
|
|
40
|
-
3.
|
|
40
|
+
3. the distributor's immutable claim duration decides whether funded reward rounds expire
|
|
41
41
|
4. the encoded token staker or current NFT owner later claims completed past reward rounds into a fresh vesting entry
|
|
42
42
|
5. anyone can burn expired unclaimed reward rounds after their deadline
|
|
43
43
|
6. recipients collect their vested share as the configured vesting schedule unlocks
|
|
44
|
-
7.
|
|
44
|
+
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
|
|
45
46
|
|
|
46
47
|
This repo does not explain why an allocation exists. It only defines how funded inventory is handed out.
|
|
47
48
|
|
|
@@ -57,11 +58,21 @@ This repo does not explain why an allocation exists. It only defines how funded
|
|
|
57
58
|
- distribution correctness depends on the distributor actually holding the assets it is expected to vest
|
|
58
59
|
- ERC-20 and ERC-721 distributions share historical reward-round accounting, but claim authority differs:
|
|
59
60
|
token rewards are claimed by the encoded staker address, while 721 rewards are claimed by the current NFT owner
|
|
60
|
-
- `
|
|
61
|
-
same
|
|
61
|
+
- `CLAIM_DURATION` is fixed at deployment; `0` means reward rounds do not expire, otherwise all funding paths use the
|
|
62
|
+
same deadline measured from when the funded round first becomes claimable
|
|
62
63
|
- `burnExpiredRewards` is permissionless and only burns the unclaimed remainder; already-materialized vesting entries
|
|
63
64
|
remain claimable on their normal vesting curve
|
|
64
|
-
-
|
|
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
|
|
67
|
+
- revnet loan-backed vesting is opt-in at deployment; the reward token must be a REVOwner-owned revnet token, the
|
|
68
|
+
distributor keeps the loan NFT, and repayment restores the original vesting schedule instead of releasing all
|
|
69
|
+
collateral immediately
|
|
70
|
+
- if Revnet liquidates a distributor-held vesting loan, anyone can call `writeOffLiquidatedVestingLoan` to clear the
|
|
71
|
+
stale collection lock and forfeit only the vesting rewards that were collateralized by that loan
|
|
72
|
+
- distributors deployed with `VESTING_ROUNDS == 0` disable revnet vesting loans because rewards are immediately
|
|
73
|
+
collectible instead of locked in a vesting position
|
|
74
|
+
- `releaseForfeitedRewards` matters for 721 distributions; token-vote distributions do not have the same burned-token
|
|
75
|
+
forfeiture path
|
|
65
76
|
- snapshot timing is part of the trusted surface
|
|
66
77
|
- this repo settles distributions, but it does not prove the upstream entitlement math was correct
|
|
67
78
|
|
|
@@ -77,6 +88,7 @@ This repo does not explain why an allocation exists. It only defines how funded
|
|
|
77
88
|
1. `test/JBTokenDistributor.t.sol`
|
|
78
89
|
2. `test/JB721Distributor.t.sol`
|
|
79
90
|
3. `test/invariant/JB721DistributorInvariant.t.sol`
|
|
91
|
+
4. `test/regression/VestingLoanRegression.t.sol`
|
|
80
92
|
|
|
81
93
|
## Install
|
|
82
94
|
|
|
@@ -118,8 +130,8 @@ script/
|
|
|
118
130
|
- distributors are only as trustworthy as the vesting parameters and funding they receive
|
|
119
131
|
- operational mistakes often come from funding the wrong asset or underfunding the distributor
|
|
120
132
|
- teams should review claim timing and snapshot assumptions with the same care they review the payout source
|
|
121
|
-
-
|
|
122
|
-
unclaimed rewards can be burned by anyone
|
|
133
|
+
- 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
|
|
123
135
|
|
|
124
136
|
## For AI Agents
|
|
125
137
|
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/distributor-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"@bananapus/core-v6": "^0.0.60",
|
|
29
29
|
"@bananapus/permission-ids-v6": "^0.0.27",
|
|
30
30
|
"@openzeppelin/contracts": "5.6.1",
|
|
31
|
-
"@prb/math": "4.1.1"
|
|
31
|
+
"@prb/math": "4.1.1",
|
|
32
|
+
"@rev-net/core-v6": "^0.0.74"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@sphinx-labs/plugins": "0.33.3"
|
package/script/Deploy.s.sol
CHANGED
|
@@ -3,7 +3,10 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Script} from "forge-std/Script.sol";
|
|
5
5
|
|
|
6
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
6
7
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IREVLoans} from "@rev-net/core-v6/src/interfaces/IREVLoans.sol";
|
|
9
|
+
import {IREVOwner} from "@rev-net/core-v6/src/interfaces/IREVOwner.sol";
|
|
7
10
|
|
|
8
11
|
import {JB721Distributor} from "../src/JB721Distributor.sol";
|
|
9
12
|
|
|
@@ -13,11 +16,27 @@ contract Deploy is Script {
|
|
|
13
16
|
|
|
14
17
|
// Configure these values before deploying.
|
|
15
18
|
IJBDirectory directory = IJBDirectory(vm.envAddress("DIRECTORY_ADDRESS"));
|
|
19
|
+
IJBController controller = IJBController(vm.envAddress("CONTROLLER_ADDRESS"));
|
|
20
|
+
IREVLoans revLoans = IREVLoans(vm.envOr("REV_LOANS_ADDRESS", address(0)));
|
|
21
|
+
IREVOwner revOwner = IREVOwner(vm.envOr("REV_OWNER_ADDRESS", address(0)));
|
|
16
22
|
uint256 roundDuration = vm.envUint("ROUND_DURATION");
|
|
17
23
|
uint256 vestingRounds = vm.envUint("VESTING_ROUNDS");
|
|
24
|
+
uint256 rawClaimDuration = vm.envUint("CLAIM_DURATION");
|
|
25
|
+
|
|
26
|
+
require(rawClaimDuration <= type(uint48).max, "CLAIM_DURATION_TOO_LARGE");
|
|
27
|
+
|
|
28
|
+
// Safe because the explicit bound above rejects values larger than uint48.
|
|
29
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
30
|
+
uint48 claimDuration = uint48(rawClaimDuration);
|
|
18
31
|
|
|
19
32
|
new JB721Distributor({
|
|
20
|
-
directory: directory,
|
|
33
|
+
directory: directory,
|
|
34
|
+
controller: controller,
|
|
35
|
+
revLoans: revLoans,
|
|
36
|
+
revOwner: revOwner,
|
|
37
|
+
initialRoundDuration: roundDuration,
|
|
38
|
+
initialVestingRounds: vestingRounds,
|
|
39
|
+
initialClaimDuration: claimDuration
|
|
21
40
|
});
|
|
22
41
|
|
|
23
42
|
vm.stopBroadcast();
|
package/src/JB721Distributor.sol
CHANGED
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {IJB721Checkpoints} from "@bananapus/721-hook-v6/src/interfaces/IJB721Checkpoints.sol";
|
|
5
5
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
6
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
6
7
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
8
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
8
9
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
@@ -13,6 +14,8 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
13
14
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
14
15
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
15
16
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
17
|
+
import {IREVLoans} from "@rev-net/core-v6/src/interfaces/IREVLoans.sol";
|
|
18
|
+
import {IREVOwner} from "@rev-net/core-v6/src/interfaces/IREVOwner.sol";
|
|
16
19
|
|
|
17
20
|
import {JBDistributor} from "./JBDistributor.sol";
|
|
18
21
|
import {IJB721Distributor} from "./interfaces/IJB721Distributor.sol";
|
|
@@ -26,7 +29,7 @@ import {JBVestingData} from "./structs/JBVestingData.sol";
|
|
|
26
29
|
/// @dev Any project can use this distributor by configuring a payout split with
|
|
27
30
|
/// `hook = this contract` and `beneficiary = address(their 721 hook)`.
|
|
28
31
|
/// @dev The stake weight of each NFT is its tier's `votingUnits`. Burned NFTs are excluded from the total stake
|
|
29
|
-
/// calculation and their
|
|
32
|
+
/// calculation and their unlocked forfeited rewards can be burned via `releaseForfeitedRewards`.
|
|
30
33
|
/// @dev Funded rewards are assigned to the funding round. NFT owners claim historical rounds lazily; all unclaimed
|
|
31
34
|
/// past rewards begin vesting when the current NFT owner claims, not when the rewards were funded.
|
|
32
35
|
/// @dev Implements `IJBSplitHook` so it can receive tokens directly from Juicebox project payout splits.
|
|
@@ -82,14 +85,22 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
82
85
|
//*********************************************************************//
|
|
83
86
|
|
|
84
87
|
/// @param directory The JB directory used to verify terminal/controller callers.
|
|
88
|
+
/// @param controller The JB controller used to burn expired or forfeited project-token rewards.
|
|
89
|
+
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
90
|
+
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
85
91
|
/// @param initialRoundDuration The duration of each round, specified in seconds.
|
|
86
92
|
/// @param initialVestingRounds The number of rounds until tokens are fully vested.
|
|
93
|
+
/// @param initialClaimDuration The number of seconds claimants have after each reward round becomes claimable.
|
|
87
94
|
constructor(
|
|
88
95
|
IJBDirectory directory,
|
|
96
|
+
IJBController controller,
|
|
97
|
+
IREVLoans revLoans,
|
|
98
|
+
IREVOwner revOwner,
|
|
89
99
|
uint256 initialRoundDuration,
|
|
90
|
-
uint256 initialVestingRounds
|
|
100
|
+
uint256 initialVestingRounds,
|
|
101
|
+
uint48 initialClaimDuration
|
|
91
102
|
)
|
|
92
|
-
JBDistributor(initialRoundDuration, initialVestingRounds)
|
|
103
|
+
JBDistributor(controller, revLoans, revOwner, initialRoundDuration, initialVestingRounds, initialClaimDuration)
|
|
93
104
|
{
|
|
94
105
|
DIRECTORY = directory;
|
|
95
106
|
}
|
|
@@ -129,7 +140,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
129
140
|
|
|
130
141
|
if (msg.value != 0) {
|
|
131
142
|
// Assign native split proceeds to the current reward round for this 721 hook.
|
|
132
|
-
_recordRewardFunding({hook: hook, token: IERC20(context.token), amount: msg.value
|
|
143
|
+
_recordRewardFunding({hook: hook, token: IERC20(context.token), amount: msg.value});
|
|
133
144
|
}
|
|
134
145
|
} else {
|
|
135
146
|
// Validate that native ETH is not cross-booked under an ERC-20 token.
|
|
@@ -147,7 +158,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
147
158
|
_acceptErc20FundsFrom({token: IERC20(context.token), from: msg.sender, amount: context.amount});
|
|
148
159
|
|
|
149
160
|
// Assign only the amount actually received to this round's reward pot.
|
|
150
|
-
_recordRewardFunding({hook: hook, token: IERC20(context.token), amount: delta
|
|
161
|
+
_recordRewardFunding({hook: hook, token: IERC20(context.token), amount: delta});
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -223,14 +234,14 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
223
234
|
/// @param hook The 721 hook whose NFT owners are claiming.
|
|
224
235
|
/// @param tokenIds The NFT token IDs to claim for.
|
|
225
236
|
/// @param tokens The reward tokens to claim.
|
|
226
|
-
function _claimPastRewards(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) internal {
|
|
237
|
+
function _claimPastRewards(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) internal override {
|
|
227
238
|
// Round 0 has no completed reward rounds behind it, so nothing can be claimed yet.
|
|
228
239
|
uint256 round = currentRound();
|
|
229
240
|
if (round == 0) return;
|
|
230
241
|
|
|
231
242
|
// Current-round funding is excluded. It becomes claimable only after a later round starts.
|
|
232
243
|
JBClaimContext memory ctx =
|
|
233
|
-
JBClaimContext({hook: hook, lastClaimableRound: round - 1, vestingReleaseRound: round +
|
|
244
|
+
JBClaimContext({hook: hook, lastClaimableRound: round - 1, vestingReleaseRound: round + VESTING_ROUNDS});
|
|
234
245
|
|
|
235
246
|
// Process each reward token independently because each token has its own round funding and claim cursor.
|
|
236
247
|
for (uint256 i; i < tokens.length;) {
|
|
@@ -546,7 +557,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
546
557
|
/// @notice Revert unless the caller is authorized to claim each NFT token ID.
|
|
547
558
|
/// @param hook The 721 hook whose NFT owners are claiming.
|
|
548
559
|
/// @param tokenIds The NFT token IDs to check.
|
|
549
|
-
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view {
|
|
560
|
+
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view override {
|
|
550
561
|
// Each requested NFT must currently belong to msg.sender and appear in strictly increasing order.
|
|
551
562
|
for (uint256 i; i < tokenIds.length;) {
|
|
552
563
|
uint256 tokenId = tokenIds[i];
|