@bananapus/distributor-v6 0.0.3 → 0.0.4
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/foundry.lock +5 -0
- package/package.json +1 -1
- package/src/JB721Distributor.sol +236 -22
- package/src/JBDistributor.sol +293 -213
- package/src/JBTokenDistributor.sol +32 -27
- package/src/interfaces/IJBDistributor.sol +37 -24
- package/test/AuditFixes.t.sol +429 -0
- package/test/JB721Distributor.t.sol +232 -163
- package/test/JBTokenDistributor.t.sol +92 -13
- package/test/audit/H26VotingPowerCap.t.sol +338 -0
- package/test/invariant/JB721DistributorInvariant.t.sol +11 -12
package/src/JBDistributor.sol
CHANGED
|
@@ -13,45 +13,56 @@ import {JBVestingData} from "./structs/JBVestingData.sol";
|
|
|
13
13
|
/// @notice A contract managing distributions of tokens to be claimed and vested by stakers of any other token.
|
|
14
14
|
abstract contract JBDistributor is IJBDistributor {
|
|
15
15
|
using SafeERC20 for IERC20;
|
|
16
|
+
|
|
16
17
|
//*********************************************************************//
|
|
17
18
|
// --------------------------- custom errors ------------------------- //
|
|
18
19
|
//*********************************************************************//
|
|
19
20
|
|
|
20
|
-
/// @notice Thrown when
|
|
21
|
-
error
|
|
21
|
+
/// @notice Thrown when an empty tokenIds array is passed.
|
|
22
|
+
error JBDistributor_EmptyTokenIds();
|
|
22
23
|
|
|
23
24
|
/// @notice Thrown when a native ETH transfer fails.
|
|
24
25
|
error JBDistributor_NativeTransferFailed();
|
|
25
26
|
|
|
27
|
+
/// @notice Thrown when the caller does not have access to the token.
|
|
28
|
+
error JBDistributor_NoAccess();
|
|
29
|
+
|
|
26
30
|
/// @notice Thrown when there is nothing to distribute for a token in the current round.
|
|
27
31
|
error JBDistributor_NothingToDistribute();
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
//*********************************************************************//
|
|
34
|
+
// ------------------------- public constants ------------------------ //
|
|
35
|
+
//*********************************************************************//
|
|
36
|
+
|
|
37
|
+
/// @notice The number of shares that represent 100%.
|
|
38
|
+
uint256 public constant MAX_SHARE = 100_000;
|
|
31
39
|
|
|
32
40
|
//*********************************************************************//
|
|
33
41
|
// ---------------- public immutable stored properties --------------- //
|
|
34
42
|
//*********************************************************************//
|
|
35
43
|
|
|
36
|
-
/// @notice The
|
|
37
|
-
uint256 public immutable startingBlock;
|
|
38
|
-
|
|
39
|
-
/// @notice The minimum amount of time stakers have to claim rewards, specified in blocks.
|
|
44
|
+
/// @notice The duration of each round, specified in seconds.
|
|
40
45
|
uint256 public immutable override roundDuration;
|
|
41
46
|
|
|
47
|
+
/// @notice The starting timestamp of the distributor.
|
|
48
|
+
uint256 public immutable startingTimestamp;
|
|
49
|
+
|
|
42
50
|
/// @notice The number of rounds until tokens are fully vested.
|
|
43
51
|
uint256 public immutable override vestingRounds;
|
|
44
52
|
|
|
45
53
|
//*********************************************************************//
|
|
46
|
-
// --------------------- public
|
|
54
|
+
// --------------------- public stored properties -------------------- //
|
|
47
55
|
//*********************************************************************//
|
|
48
56
|
|
|
49
|
-
/// @notice The
|
|
50
|
-
|
|
57
|
+
/// @notice The index within `vestingDataOf` of the latest vest.
|
|
58
|
+
/// @custom:param hook The hook the tokenId belongs to.
|
|
59
|
+
/// @custom:param tokenId The ID of the token to which the vests belong.
|
|
60
|
+
/// @custom:param token The address of the token being vested.
|
|
61
|
+
mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256))) public latestVestedIndexOf;
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
/// @notice The block number recorded as the snapshot point for each round.
|
|
64
|
+
/// @dev Set to `block.number - 1` on first interaction in a round, so that `IVotes.getPastVotes` works.
|
|
65
|
+
mapping(uint256 round => uint256) public override roundSnapshotBlock;
|
|
55
66
|
|
|
56
67
|
/// @notice The amount of a token that is currently vesting for a hook's stakers.
|
|
57
68
|
/// @custom:param hook The hook whose stakers are vesting.
|
|
@@ -65,14 +76,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
65
76
|
// slither-disable-next-line uninitialized-state
|
|
66
77
|
mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => JBVestingData[]))) public vestingDataOf;
|
|
67
78
|
|
|
68
|
-
/// @notice The index within `vestingDataOf` of the latest vest.
|
|
69
|
-
/// @custom:param hook The hook the tokenId belongs to.
|
|
70
|
-
/// @custom:param tokenId The ID of the token to which the vests belong.
|
|
71
|
-
/// @custom:param token The address of the token being vested.
|
|
72
|
-
mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256))) public latestVestedIndexOf;
|
|
73
|
-
|
|
74
79
|
//*********************************************************************//
|
|
75
|
-
//
|
|
80
|
+
// -------------------- internal stored properties ------------------- //
|
|
76
81
|
//*********************************************************************//
|
|
77
82
|
|
|
78
83
|
/// @notice The balance of a token held for a specific hook's stakers.
|
|
@@ -88,42 +93,123 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
88
93
|
_snapshotAtRoundOf;
|
|
89
94
|
|
|
90
95
|
//*********************************************************************//
|
|
91
|
-
// --------------------------
|
|
96
|
+
// -------------------------- constructor ---------------------------- //
|
|
92
97
|
//*********************************************************************//
|
|
93
98
|
|
|
94
|
-
/// @
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
/// @param roundDuration_ The duration of each round, specified in seconds.
|
|
100
|
+
/// @param vestingRounds_ The number of rounds until tokens are fully vested.
|
|
101
|
+
constructor(uint256 roundDuration_, uint256 vestingRounds_) {
|
|
102
|
+
startingTimestamp = block.timestamp;
|
|
103
|
+
roundDuration = roundDuration_;
|
|
104
|
+
vestingRounds = vestingRounds_;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//*********************************************************************//
|
|
108
|
+
// ---------------------- external transactions ---------------------- //
|
|
109
|
+
//*********************************************************************//
|
|
110
|
+
|
|
111
|
+
/// @notice Claims tokens and begins vesting.
|
|
112
|
+
/// @param hook The hook whose stakers are vesting.
|
|
113
|
+
/// @param tokenIds The IDs to claim rewards for.
|
|
114
|
+
/// @param tokens The tokens to claim.
|
|
115
|
+
function beginVesting(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) external override {
|
|
116
|
+
// Revert if no token IDs are provided.
|
|
117
|
+
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds();
|
|
118
|
+
|
|
119
|
+
// Keep a reference to the current round.
|
|
120
|
+
uint256 round = currentRound();
|
|
121
|
+
|
|
122
|
+
// Ensure the snapshot block is recorded for this round.
|
|
123
|
+
_ensureSnapshotBlock(round);
|
|
124
|
+
|
|
125
|
+
// Keep a reference to the total staked amount at the snapshot block.
|
|
126
|
+
uint256 totalStakeAmount = _totalStake(hook, roundSnapshotBlock[round]);
|
|
127
|
+
|
|
128
|
+
// Skip vesting when there are no stakers — funds carry over to the next round.
|
|
129
|
+
// slither-disable-next-line incorrect-equality
|
|
130
|
+
if (totalStakeAmount == 0) return;
|
|
131
|
+
|
|
132
|
+
// Loop through each token for which vesting is beginning.
|
|
133
|
+
for (uint256 i; i < tokens.length;) {
|
|
134
|
+
IERC20 token = tokens[i];
|
|
135
|
+
|
|
136
|
+
// Take a snapshot of the token balance if it hasn't been taken already.
|
|
137
|
+
JBTokenSnapshotData memory snapshot = _takeSnapshotOf(hook, token);
|
|
138
|
+
uint256 distributable = snapshot.balance - snapshot.vestingAmount;
|
|
139
|
+
|
|
140
|
+
// Revert if there is nothing to distribute for this token.
|
|
141
|
+
if (distributable == 0) revert JBDistributor_NothingToDistribute();
|
|
142
|
+
|
|
143
|
+
// Vest each token ID and get the total amount vested.
|
|
144
|
+
uint256 totalVestingAmount =
|
|
145
|
+
_vestTokenIds(hook, tokenIds, token, distributable, totalStakeAmount, round + vestingRounds);
|
|
146
|
+
|
|
147
|
+
unchecked {
|
|
148
|
+
// Store the updated total claimed amount now vesting.
|
|
149
|
+
totalVestingAmountOf[hook][token] += totalVestingAmount;
|
|
150
|
+
|
|
151
|
+
++i;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
97
154
|
}
|
|
98
155
|
|
|
99
|
-
/// @notice
|
|
100
|
-
/// @
|
|
101
|
-
|
|
102
|
-
|
|
156
|
+
/// @notice Fund the distributor for a specific hook by pulling tokens from the caller.
|
|
157
|
+
/// @dev For native ETH, send `msg.value` and pass `IERC20(JBConstants.NATIVE_TOKEN)` as the token.
|
|
158
|
+
/// @param hook The hook to fund.
|
|
159
|
+
/// @param token The token to fund with.
|
|
160
|
+
/// @param amount The amount to fund.
|
|
161
|
+
function fund(address hook, IERC20 token, uint256 amount) external payable override {
|
|
162
|
+
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
163
|
+
amount = msg.value;
|
|
164
|
+
} else {
|
|
165
|
+
// Use balance delta to handle fee-on-transfer tokens correctly.
|
|
166
|
+
uint256 balanceBefore = token.balanceOf(address(this));
|
|
167
|
+
token.safeTransferFrom(msg.sender, address(this), amount);
|
|
168
|
+
amount = token.balanceOf(address(this)) - balanceBefore;
|
|
169
|
+
}
|
|
170
|
+
_balanceOf[hook][token] += amount;
|
|
103
171
|
}
|
|
104
172
|
|
|
105
|
-
/// @notice
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
function balanceOf(address hook, IERC20 token) external view override returns (uint256) {
|
|
109
|
-
return _balanceOf[hook][token];
|
|
173
|
+
/// @notice Record the snapshot block for the current round. Callable by anyone (keepers, frontends).
|
|
174
|
+
function poke() external override {
|
|
175
|
+
_ensureSnapshotBlock(currentRound());
|
|
110
176
|
}
|
|
111
177
|
|
|
112
|
-
/// @notice
|
|
113
|
-
/// @param hook The hook
|
|
114
|
-
/// @param
|
|
115
|
-
/// @param
|
|
116
|
-
|
|
178
|
+
/// @notice Release vested rewards in the case that a token was burned.
|
|
179
|
+
/// @param hook The hook whose tokens were burned.
|
|
180
|
+
/// @param tokenIds The IDs of the burned tokens.
|
|
181
|
+
/// @param tokens The address of the tokens being released.
|
|
182
|
+
/// @param beneficiary The recipient of the released tokens.
|
|
183
|
+
function releaseForfeitedRewards(
|
|
117
184
|
address hook,
|
|
118
|
-
|
|
119
|
-
|
|
185
|
+
uint256[] calldata tokenIds,
|
|
186
|
+
IERC20[] calldata tokens,
|
|
187
|
+
address beneficiary
|
|
120
188
|
)
|
|
121
189
|
external
|
|
122
|
-
view
|
|
123
190
|
override
|
|
124
|
-
returns (JBTokenSnapshotData memory)
|
|
125
191
|
{
|
|
126
|
-
|
|
192
|
+
// Make sure that all tokens are burned.
|
|
193
|
+
for (uint256 i; i < tokenIds.length;) {
|
|
194
|
+
if (!_tokenBurned(hook, tokenIds[i])) revert JBDistributor_NoAccess();
|
|
195
|
+
unchecked {
|
|
196
|
+
++i;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Unlock the rewards and send them to the beneficiary.
|
|
201
|
+
_unlockRewards(hook, tokenIds, tokens, beneficiary, false);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//*********************************************************************//
|
|
205
|
+
// ----------------------- external views ---------------------------- //
|
|
206
|
+
//*********************************************************************//
|
|
207
|
+
|
|
208
|
+
/// @notice The balance of a token held for a specific hook's stakers.
|
|
209
|
+
/// @param hook The hook whose balance to check.
|
|
210
|
+
/// @param token The token to check the balance of.
|
|
211
|
+
function balanceOf(address hook, IERC20 token) external view override returns (uint256) {
|
|
212
|
+
return _balanceOf[hook][token];
|
|
127
213
|
}
|
|
128
214
|
|
|
129
215
|
/// @notice Calculate how much of the token has been claimed for the given tokenId.
|
|
@@ -202,161 +288,43 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
202
288
|
}
|
|
203
289
|
}
|
|
204
290
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/// this is correct for each blockchain/rollup this gets deployed to.
|
|
211
|
-
/// @param vestingRounds_ The number of rounds until tokens are fully vested.
|
|
212
|
-
constructor(uint256 roundDuration_, uint256 vestingRounds_) {
|
|
213
|
-
startingBlock = block.number;
|
|
214
|
-
roundDuration = roundDuration_;
|
|
215
|
-
vestingRounds = vestingRounds_;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
//*********************************************************************//
|
|
219
|
-
// ---------------------- external transactions ---------------------- //
|
|
220
|
-
//*********************************************************************//
|
|
221
|
-
|
|
222
|
-
/// @notice Fund the distributor for a specific hook by pulling tokens from the caller.
|
|
223
|
-
/// @dev For native ETH, send `msg.value` and pass `IERC20(JBConstants.NATIVE_TOKEN)` as the token.
|
|
224
|
-
/// @param hook The hook to fund.
|
|
225
|
-
/// @param token The token to fund with.
|
|
226
|
-
/// @param amount The amount to fund.
|
|
227
|
-
function fund(address hook, IERC20 token, uint256 amount) external payable override {
|
|
228
|
-
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
229
|
-
amount = msg.value;
|
|
230
|
-
} else {
|
|
231
|
-
// Use balance delta to handle fee-on-transfer tokens correctly.
|
|
232
|
-
uint256 balanceBefore = token.balanceOf(address(this));
|
|
233
|
-
token.safeTransferFrom(msg.sender, address(this), amount);
|
|
234
|
-
amount = token.balanceOf(address(this)) - balanceBefore;
|
|
235
|
-
}
|
|
236
|
-
_balanceOf[hook][token] += amount;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/// @notice Claims tokens and begins vesting.
|
|
240
|
-
/// @param hook The hook whose stakers are vesting.
|
|
241
|
-
/// @param tokenIds The IDs to claim rewards for.
|
|
242
|
-
/// @param tokens The tokens to claim.
|
|
243
|
-
function beginVesting(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) external override {
|
|
244
|
-
// Keep a reference to the current round.
|
|
245
|
-
uint256 round = currentRound();
|
|
246
|
-
|
|
247
|
-
// Keep a reference to the total staked amount at the current round.
|
|
248
|
-
uint256 totalStakeAmount = _totalStake(hook, roundStartBlock(round));
|
|
249
|
-
|
|
250
|
-
// Loop through each token for which vesting is beginning.
|
|
251
|
-
for (uint256 i; i < tokens.length;) {
|
|
252
|
-
IERC20 token = tokens[i];
|
|
253
|
-
|
|
254
|
-
// Take a snapshot of the token balance if it hasn't been taken already.
|
|
255
|
-
JBTokenSnapshotData memory snapshot = _takeSnapshotOf(hook, token);
|
|
256
|
-
uint256 distributable = snapshot.balance - snapshot.vestingAmount;
|
|
257
|
-
|
|
258
|
-
// Revert if there is nothing to distribute for this token.
|
|
259
|
-
if (distributable == 0) revert JBDistributor_NothingToDistribute();
|
|
260
|
-
|
|
261
|
-
// Vest each token ID and get the total amount vested.
|
|
262
|
-
uint256 totalVestingAmount =
|
|
263
|
-
_vestTokenIds(hook, tokenIds, token, distributable, totalStakeAmount, round + vestingRounds);
|
|
264
|
-
|
|
265
|
-
unchecked {
|
|
266
|
-
// Store the updated total claimed amount now vesting.
|
|
267
|
-
totalVestingAmountOf[hook][token] += totalVestingAmount;
|
|
268
|
-
|
|
269
|
-
++i;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/// @notice Vests each token ID for a given reward token and returns the total amount vested.
|
|
275
|
-
/// @param hook The hook whose stakers are vesting.
|
|
276
|
-
/// @param tokenIds The IDs to claim rewards for.
|
|
277
|
-
/// @param token The reward token.
|
|
278
|
-
/// @param distributable The distributable amount for this round.
|
|
279
|
-
/// @param totalStakeAmount The total stake amount.
|
|
280
|
-
/// @param vestingReleaseRound The round at which vesting will be released.
|
|
281
|
-
/// @return totalVestingAmount The total amount that began vesting.
|
|
282
|
-
function _vestTokenIds(
|
|
291
|
+
/// @notice The snapshot data of the token information for each round.
|
|
292
|
+
/// @param hook The hook the snapshot is for.
|
|
293
|
+
/// @param token The address of the token being claimed and vested.
|
|
294
|
+
/// @param round The round to which the data applies.
|
|
295
|
+
function snapshotAtRoundOf(
|
|
283
296
|
address hook,
|
|
284
|
-
uint256[] calldata tokenIds,
|
|
285
297
|
IERC20 token,
|
|
286
|
-
uint256
|
|
287
|
-
uint256 totalStakeAmount,
|
|
288
|
-
uint256 vestingReleaseRound
|
|
298
|
+
uint256 round
|
|
289
299
|
)
|
|
290
|
-
|
|
291
|
-
|
|
300
|
+
external
|
|
301
|
+
view
|
|
302
|
+
override
|
|
303
|
+
returns (JBTokenSnapshotData memory)
|
|
292
304
|
{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// Skip burned tokens — they are excluded from _totalStake, so including them would overbook vesting.
|
|
297
|
-
if (_tokenBurned(hook, tokenId)) {
|
|
298
|
-
unchecked {
|
|
299
|
-
++j;
|
|
300
|
-
}
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Keep a reference to the vesting data for this hook/tokenId/token.
|
|
305
|
-
JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
|
|
306
|
-
|
|
307
|
-
// Make sure this token hasn't already been claimed by checking if the last item is the current round.
|
|
308
|
-
uint256 numVesting = vestings.length;
|
|
309
|
-
// slither-disable-next-line incorrect-equality
|
|
310
|
-
if (numVesting != 0 && vestings[numVesting - 1].releaseRound == vestingReleaseRound) {
|
|
311
|
-
revert JBDistributor_AlreadyVesting();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Keep a reference to the amount of tokens being claimed.
|
|
315
|
-
uint256 tokenAmount = mulDiv(distributable, _tokenStake(hook, tokenId), totalStakeAmount);
|
|
316
|
-
|
|
317
|
-
// Add to the list of vesting data.
|
|
318
|
-
vestings.push(JBVestingData({releaseRound: vestingReleaseRound, amount: tokenAmount, shareClaimed: 0}));
|
|
305
|
+
return _snapshotAtRoundOf[hook][token][round];
|
|
306
|
+
}
|
|
319
307
|
|
|
320
|
-
|
|
308
|
+
//*********************************************************************//
|
|
309
|
+
// -------------------------- public views --------------------------- //
|
|
310
|
+
//*********************************************************************//
|
|
321
311
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
}
|
|
312
|
+
/// @notice The number of the current round.
|
|
313
|
+
function currentRound() public view override returns (uint256) {
|
|
314
|
+
return (block.timestamp - startingTimestamp) / roundDuration;
|
|
327
315
|
}
|
|
328
316
|
|
|
329
|
-
/// @notice
|
|
330
|
-
/// @param
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
/// @param beneficiary The recipient of the released tokens.
|
|
334
|
-
function releaseForfeitedRewards(
|
|
335
|
-
address hook,
|
|
336
|
-
uint256[] calldata tokenIds,
|
|
337
|
-
IERC20[] calldata tokens,
|
|
338
|
-
address beneficiary
|
|
339
|
-
)
|
|
340
|
-
external
|
|
341
|
-
override
|
|
342
|
-
{
|
|
343
|
-
// Make sure that all tokens are burned.
|
|
344
|
-
for (uint256 i; i < tokenIds.length;) {
|
|
345
|
-
if (!_tokenBurned(hook, tokenIds[i])) revert JBDistributor_NoAccess();
|
|
346
|
-
unchecked {
|
|
347
|
-
++i;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Unlock the rewards and send them to the beneficiary.
|
|
352
|
-
_unlockRewards(hook, tokenIds, tokens, beneficiary, false);
|
|
317
|
+
/// @notice The timestamp at which a round started.
|
|
318
|
+
/// @param round The round to get the start timestamp of.
|
|
319
|
+
function roundStartTimestamp(uint256 round) public view override returns (uint256) {
|
|
320
|
+
return startingTimestamp + roundDuration * round;
|
|
353
321
|
}
|
|
354
322
|
|
|
355
323
|
//*********************************************************************//
|
|
356
324
|
// ----------------------- public transactions ----------------------- //
|
|
357
325
|
//*********************************************************************//
|
|
358
326
|
|
|
359
|
-
/// @notice Collect vested tokens.
|
|
327
|
+
/// @notice Collect vested tokens. Auto-vests for the current round if not already vested.
|
|
360
328
|
/// @param hook The hook whose stakers are collecting.
|
|
361
329
|
/// @param tokenIds The IDs of the tokens to collect for.
|
|
362
330
|
/// @param tokens The address of the tokens being claimed.
|
|
@@ -370,6 +338,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
370
338
|
public
|
|
371
339
|
override
|
|
372
340
|
{
|
|
341
|
+
// Revert if no token IDs are provided.
|
|
342
|
+
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds();
|
|
343
|
+
|
|
373
344
|
// Make sure that all tokens can be claimed by this sender.
|
|
374
345
|
for (uint256 i; i < tokenIds.length;) {
|
|
375
346
|
if (!_canClaim(hook, tokenIds[i], msg.sender)) revert JBDistributor_NoAccess();
|
|
@@ -378,6 +349,38 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
378
349
|
}
|
|
379
350
|
}
|
|
380
351
|
|
|
352
|
+
// --- Auto-vest for the current round ---
|
|
353
|
+
uint256 round = currentRound();
|
|
354
|
+
|
|
355
|
+
// Ensure the snapshot block is recorded for this round.
|
|
356
|
+
_ensureSnapshotBlock(round);
|
|
357
|
+
|
|
358
|
+
// Keep a reference to the total staked amount at the snapshot block.
|
|
359
|
+
uint256 totalStakeAmount = _totalStake(hook, roundSnapshotBlock[round]);
|
|
360
|
+
|
|
361
|
+
// Loop through each token and auto-vest if there's something distributable.
|
|
362
|
+
for (uint256 i; i < tokens.length;) {
|
|
363
|
+
IERC20 token = tokens[i];
|
|
364
|
+
|
|
365
|
+
// Take a snapshot of the token balance if it hasn't been taken already.
|
|
366
|
+
JBTokenSnapshotData memory snapshot = _takeSnapshotOf(hook, token);
|
|
367
|
+
uint256 distributable = snapshot.balance - snapshot.vestingAmount;
|
|
368
|
+
|
|
369
|
+
// Only auto-vest if there's something to distribute and there's stake.
|
|
370
|
+
if (distributable > 0 && totalStakeAmount > 0) {
|
|
371
|
+
uint256 totalVestingAmount =
|
|
372
|
+
_vestTokenIds(hook, tokenIds, token, distributable, totalStakeAmount, round + vestingRounds);
|
|
373
|
+
|
|
374
|
+
unchecked {
|
|
375
|
+
totalVestingAmountOf[hook][token] += totalVestingAmount;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
unchecked {
|
|
380
|
+
++i;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
381
384
|
// Unlock the rewards and send them to the beneficiary.
|
|
382
385
|
_unlockRewards(hook, tokenIds, tokens, beneficiary, true);
|
|
383
386
|
}
|
|
@@ -386,6 +389,47 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
386
389
|
// ---------------------- internal transactions ---------------------- //
|
|
387
390
|
//*********************************************************************//
|
|
388
391
|
|
|
392
|
+
/// @notice Ensures that a snapshot block is recorded for the given round.
|
|
393
|
+
/// @dev Uses `block.number - 1` because `IVotes.getPastVotes` requires a strictly past block.
|
|
394
|
+
/// @param round The round to ensure a snapshot block for.
|
|
395
|
+
function _ensureSnapshotBlock(uint256 round) internal {
|
|
396
|
+
// slither-disable-next-line incorrect-equality
|
|
397
|
+
if (roundSnapshotBlock[round] == 0) {
|
|
398
|
+
roundSnapshotBlock[round] = block.number - 1;
|
|
399
|
+
emit RoundSnapshotRecorded(round, block.number - 1);
|
|
400
|
+
}
|
|
401
|
+
// Eagerly lock the next round's snapshot to prevent first-caller manipulation.
|
|
402
|
+
// slither-disable-next-line incorrect-equality
|
|
403
|
+
if (roundSnapshotBlock[round + 1] == 0) {
|
|
404
|
+
roundSnapshotBlock[round + 1] = block.number - 1;
|
|
405
|
+
emit RoundSnapshotRecorded(round + 1, block.number - 1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// @notice Takes a snapshot of the token balance and vesting amount for the current round.
|
|
410
|
+
/// @param hook The hook to take the snapshot for.
|
|
411
|
+
/// @param token The token address to take a snapshot of.
|
|
412
|
+
/// @return snapshot The snapshot data.
|
|
413
|
+
function _takeSnapshotOf(address hook, IERC20 token) internal returns (JBTokenSnapshotData memory snapshot) {
|
|
414
|
+
// Keep a reference to the current round.
|
|
415
|
+
uint256 round = currentRound();
|
|
416
|
+
|
|
417
|
+
// Keep a reference to the token's snapshot.
|
|
418
|
+
snapshot = _snapshotAtRoundOf[hook][token][round];
|
|
419
|
+
|
|
420
|
+
// If a snapshot was already taken at this cycle, do not take a new one.
|
|
421
|
+
if (snapshot.balance != 0) return snapshot;
|
|
422
|
+
|
|
423
|
+
// Take a snapshot using the hook's tracked balance.
|
|
424
|
+
snapshot =
|
|
425
|
+
JBTokenSnapshotData({balance: _balanceOf[hook][token], vestingAmount: totalVestingAmountOf[hook][token]});
|
|
426
|
+
|
|
427
|
+
// Store the snapshot.
|
|
428
|
+
_snapshotAtRoundOf[hook][token][round] = snapshot;
|
|
429
|
+
|
|
430
|
+
emit SnapshotCreated(hook, round, token, snapshot.balance, snapshot.vestingAmount);
|
|
431
|
+
}
|
|
432
|
+
|
|
389
433
|
/// @notice Unlocks rewards for the given token IDs and tokens, either for collection or forfeiture.
|
|
390
434
|
/// @param hook The hook the tokens belong to.
|
|
391
435
|
/// @param tokenIds The IDs of the tokens to unlock rewards for.
|
|
@@ -508,32 +552,68 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
508
552
|
}
|
|
509
553
|
}
|
|
510
554
|
|
|
511
|
-
/// @notice
|
|
512
|
-
/// @
|
|
513
|
-
/// @param
|
|
514
|
-
/// @
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
555
|
+
/// @notice Vests each token ID for a given reward token and returns the total amount vested.
|
|
556
|
+
/// @dev Silently skips already-vested tokenIds instead of reverting, to support auto-vest.
|
|
557
|
+
/// @param hook The hook whose stakers are vesting.
|
|
558
|
+
/// @param tokenIds The IDs to claim rewards for.
|
|
559
|
+
/// @param token The reward token.
|
|
560
|
+
/// @param distributable The distributable amount for this round.
|
|
561
|
+
/// @param totalStakeAmount The total stake amount.
|
|
562
|
+
/// @param vestingReleaseRound The round at which vesting will be released.
|
|
563
|
+
/// @return totalVestingAmount The total amount that began vesting.
|
|
564
|
+
function _vestTokenIds(
|
|
565
|
+
address hook,
|
|
566
|
+
uint256[] calldata tokenIds,
|
|
567
|
+
IERC20 token,
|
|
568
|
+
uint256 distributable,
|
|
569
|
+
uint256 totalStakeAmount,
|
|
570
|
+
uint256 vestingReleaseRound
|
|
571
|
+
)
|
|
572
|
+
internal
|
|
573
|
+
virtual
|
|
574
|
+
returns (uint256 totalVestingAmount)
|
|
575
|
+
{
|
|
576
|
+
for (uint256 j; j < tokenIds.length;) {
|
|
577
|
+
uint256 tokenId = tokenIds[j];
|
|
518
578
|
|
|
519
|
-
|
|
520
|
-
|
|
579
|
+
// Skip burned tokens — they are excluded from _totalStake, so including them would overbook vesting.
|
|
580
|
+
if (_tokenBurned(hook, tokenId)) {
|
|
581
|
+
unchecked {
|
|
582
|
+
++j;
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
521
586
|
|
|
522
|
-
|
|
523
|
-
|
|
587
|
+
// Keep a reference to the vesting data for this hook/tokenId/token.
|
|
588
|
+
JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
|
|
524
589
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
590
|
+
// Skip if this token has already been vested for this round (same releaseRound).
|
|
591
|
+
uint256 numVesting = vestings.length;
|
|
592
|
+
// slither-disable-next-line incorrect-equality
|
|
593
|
+
if (numVesting != 0 && vestings[numVesting - 1].releaseRound == vestingReleaseRound) {
|
|
594
|
+
unchecked {
|
|
595
|
+
++j;
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
528
599
|
|
|
529
|
-
|
|
530
|
-
|
|
600
|
+
// Keep a reference to the amount of tokens being claimed.
|
|
601
|
+
uint256 tokenAmount = mulDiv(distributable, _tokenStake(hook, tokenId), totalStakeAmount);
|
|
531
602
|
|
|
532
|
-
|
|
603
|
+
// Add to the list of vesting data.
|
|
604
|
+
vestings.push(JBVestingData({releaseRound: vestingReleaseRound, amount: tokenAmount, shareClaimed: 0}));
|
|
605
|
+
|
|
606
|
+
emit Claimed(hook, tokenId, token, tokenAmount, vestingReleaseRound);
|
|
607
|
+
|
|
608
|
+
unchecked {
|
|
609
|
+
totalVestingAmount += tokenAmount;
|
|
610
|
+
++j;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
533
613
|
}
|
|
534
614
|
|
|
535
615
|
//*********************************************************************//
|
|
536
|
-
// -----------------------
|
|
616
|
+
// ----------------------- internal views ---------------------------- //
|
|
537
617
|
//*********************************************************************//
|
|
538
618
|
|
|
539
619
|
/// @notice A flag indicating if an account can currently claim their tokens.
|
|
@@ -543,11 +623,11 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
543
623
|
/// @return canClaim A flag indicating if claiming is allowed.
|
|
544
624
|
function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
|
|
545
625
|
|
|
546
|
-
/// @notice
|
|
547
|
-
/// @param hook The hook
|
|
548
|
-
/// @param
|
|
549
|
-
/// @return
|
|
550
|
-
function
|
|
626
|
+
/// @notice Checks if the given token was burned or not.
|
|
627
|
+
/// @param hook The hook the token belongs to.
|
|
628
|
+
/// @param tokenId The tokenId to check.
|
|
629
|
+
/// @return tokenWasBurned A boolean that is true if the token was burned.
|
|
630
|
+
function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
|
|
551
631
|
|
|
552
632
|
/// @notice The amount of tokens staked for the given token ID.
|
|
553
633
|
/// @param hook The hook the token belongs to.
|
|
@@ -555,9 +635,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
555
635
|
/// @return tokenStakeAmount The amount of staked tokens that is being represented by the token.
|
|
556
636
|
function _tokenStake(address hook, uint256 tokenId) internal view virtual returns (uint256 tokenStakeAmount);
|
|
557
637
|
|
|
558
|
-
/// @notice
|
|
559
|
-
/// @param hook The hook the
|
|
560
|
-
/// @param
|
|
561
|
-
/// @return
|
|
562
|
-
function
|
|
638
|
+
/// @notice The total amount staked at the given block.
|
|
639
|
+
/// @param hook The hook to get the total stake for.
|
|
640
|
+
/// @param blockNumber The block number to get the total staked amount at.
|
|
641
|
+
/// @return totalStakedAmount The total amount staked at a block number.
|
|
642
|
+
function _totalStake(address hook, uint256 blockNumber) internal view virtual returns (uint256 totalStakedAmount);
|
|
563
643
|
}
|