@bananapus/distributor-v6 0.0.33 → 0.0.35
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 +7 -0
- package/package.json +2 -2
- package/references/operations.md +2 -1
- package/src/JB721Distributor.sol +335 -248
- package/src/JBDistributor.sol +344 -390
- package/src/JBTokenDistributor.sol +53 -69
- package/src/interfaces/IJB721Distributor.sol +139 -0
- package/src/interfaces/IJBDistributor.sol +22 -38
- package/src/structs/JBBorrowContext.sol +2 -0
- package/src/structs/JBClaimContext.sol +5 -0
- package/src/structs/JBVestContext.sol +5 -0
- package/src/structs/JBVestingLoan.sol +2 -0
- package/src/structs/JBTokenSnapshotData.sol +0 -11
package/src/JB721Distributor.sol
CHANGED
|
@@ -41,6 +41,10 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
41
41
|
/// @notice Thrown when native ETH does not match the split hook context amount.
|
|
42
42
|
error JB721Distributor_NativeAmountMismatch(uint256 msgValue, uint256 contextAmount);
|
|
43
43
|
|
|
44
|
+
/// @notice Thrown when a tier-scoped call's tier IDs are not strictly increasing (so a canonical group ID
|
|
45
|
+
/// cannot be derived).
|
|
46
|
+
error JB721Distributor_TierIdsNotIncreasing(uint256 previousTierId, uint256 tierId);
|
|
47
|
+
|
|
44
48
|
/// @notice Thrown when claim batch NFT token IDs are not strictly increasing.
|
|
45
49
|
error JB721Distributor_TokenIdsNotIncreasing(uint256 previousTokenId, uint256 tokenId);
|
|
46
50
|
|
|
@@ -63,9 +67,12 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
63
67
|
|
|
64
68
|
/// @notice The next reward round an NFT token ID has not yet claimed.
|
|
65
69
|
/// @custom:param hook The 721 hook whose NFTs are claiming.
|
|
70
|
+
/// @custom:param groupId The reward group (0 = all tiers).
|
|
66
71
|
/// @custom:param tokenId The NFT token ID.
|
|
67
72
|
/// @custom:param token The reward token being claimed.
|
|
68
|
-
mapping(
|
|
73
|
+
mapping(
|
|
74
|
+
address hook => mapping(uint256 groupId => mapping(uint256 tokenId => mapping(IERC20 token => uint256)))
|
|
75
|
+
) public nextClaimRoundOf;
|
|
69
76
|
|
|
70
77
|
//*********************************************************************//
|
|
71
78
|
// -------------------- internal stored properties ------------------- //
|
|
@@ -80,6 +87,12 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
80
87
|
address hook => mapping(IERC20 token => mapping(uint256 rewardRound => mapping(address owner => uint256)))
|
|
81
88
|
) internal _consumedVotesOf;
|
|
82
89
|
|
|
90
|
+
/// @notice The tier set that defines a reward group, recorded the first time the group is funded.
|
|
91
|
+
/// @dev Empty for the default group (0 = all tiers). Read by the stake math to scope the tier-set denominator.
|
|
92
|
+
/// @custom:param hook The hook the group belongs to.
|
|
93
|
+
/// @custom:param groupId The reward group.
|
|
94
|
+
mapping(address hook => mapping(uint256 groupId => uint256[])) internal _tierIdsOfGroup;
|
|
95
|
+
|
|
83
96
|
//*********************************************************************//
|
|
84
97
|
// -------------------------- constructor ---------------------------- //
|
|
85
98
|
//*********************************************************************//
|
|
@@ -139,8 +152,8 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
if (msg.value != 0) {
|
|
142
|
-
//
|
|
143
|
-
_recordRewardFunding({hook: hook, token: IERC20(context.token), amount: msg.value});
|
|
155
|
+
// Split-funded pots go to the all-tiers group (0); a split cannot carry a tier set.
|
|
156
|
+
_recordRewardFunding({hook: hook, groupId: 0, token: IERC20(context.token), amount: msg.value});
|
|
144
157
|
}
|
|
145
158
|
} else {
|
|
146
159
|
// Validate that native ETH is not cross-booked under an ERC-20 token.
|
|
@@ -157,61 +170,209 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
157
170
|
uint256 delta =
|
|
158
171
|
_acceptErc20FundsFrom({token: IERC20(context.token), from: msg.sender, amount: context.amount});
|
|
159
172
|
|
|
160
|
-
// Assign only the amount actually received to this round's reward pot.
|
|
161
|
-
_recordRewardFunding({hook: hook, token: IERC20(context.token), amount: delta});
|
|
173
|
+
// Assign only the amount actually received to this round's reward pot (all-tiers group, 0).
|
|
174
|
+
_recordRewardFunding({hook: hook, groupId: 0, token: IERC20(context.token), amount: delta});
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
// The group-0 (all-tiers) `beginVesting` and `collectVestedRewards` are provided by `JBDistributor`. Both
|
|
179
|
+
// distributors share the exact same flow (authorize -> materialize past rounds via `_claimPastRewards` ->
|
|
180
|
+
// optionally release unlocked), so the round-claim logic lives once in the base and dispatches to this contract's
|
|
181
|
+
// `_claimPastRewards` / `_requireCanClaimTokenIds` overrides below. The tier-scoped overloads below derive a
|
|
182
|
+
// canonical group ID from the tier set and call the same base helpers.
|
|
183
|
+
|
|
184
|
+
/// @notice Begin vesting all unclaimed past reward rounds for the specified NFT token IDs in a tier-scoped group.
|
|
185
|
+
/// @param hook The 721 hook whose NFT owners are vesting.
|
|
186
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
187
|
+
/// @param tokenIds The NFT token IDs to claim rewards for.
|
|
169
188
|
/// @param tokens The reward tokens to begin vesting.
|
|
170
189
|
function beginVesting(
|
|
171
190
|
address hook,
|
|
191
|
+
uint256[] calldata tierIds,
|
|
172
192
|
uint256[] calldata tokenIds,
|
|
173
193
|
IERC20[] calldata tokens
|
|
174
194
|
)
|
|
175
195
|
external
|
|
176
|
-
override
|
|
196
|
+
override
|
|
177
197
|
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
198
|
+
_beginVesting({hook: hook, groupId: _groupIdFor(tierIds), tokenIds: tokenIds, tokens: tokens});
|
|
199
|
+
}
|
|
181
200
|
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
/// @notice Fund a tier-scoped reward group: only holders of the given tiers can claim this pot.
|
|
202
|
+
/// @dev For native ETH, send `msg.value` and pass `IERC20(JBConstants.NATIVE_TOKEN)` as the token. Uses balance
|
|
203
|
+
/// delta to handle fee-on-transfer tokens correctly. The tier set is recorded on the group's first funding.
|
|
204
|
+
/// @param hook The 721 hook to fund (determines which staker pool receives the tokens).
|
|
205
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
206
|
+
/// @param token The token to fund with.
|
|
207
|
+
/// @param amount The amount to fund (ignored for native ETH — `msg.value` is used instead).
|
|
208
|
+
function fund(address hook, uint256[] calldata tierIds, IERC20 token, uint256 amount) external payable override {
|
|
209
|
+
// Derive the canonical group ID for the tier set.
|
|
210
|
+
uint256 groupId = _groupIdFor(tierIds);
|
|
211
|
+
|
|
212
|
+
// Record the tier set the first time a tier-scoped group is funded, so the stake math can scope it later.
|
|
213
|
+
if (groupId != 0 && _tierIdsOfGroup[hook][groupId].length == 0) {
|
|
214
|
+
_tierIdsOfGroup[hook][groupId] = tierIds;
|
|
215
|
+
}
|
|
184
216
|
|
|
185
|
-
|
|
186
|
-
_claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
|
|
217
|
+
_fund({hook: hook, groupId: groupId, token: token, amount: amount});
|
|
187
218
|
}
|
|
188
219
|
|
|
189
|
-
/// @notice
|
|
190
|
-
/// @param hook The 721 hook whose
|
|
191
|
-
/// @param
|
|
192
|
-
/// @param
|
|
193
|
-
/// @param
|
|
194
|
-
|
|
220
|
+
/// @notice Recycle unclaimed rewards from expired tier-scoped reward rounds into the current reward round.
|
|
221
|
+
/// @param hook The 721 hook whose expired rewards should be recycled.
|
|
222
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
223
|
+
/// @param token The reward token to recycle.
|
|
224
|
+
/// @param rounds The reward rounds to recycle.
|
|
225
|
+
/// @return amount The total amount recycled.
|
|
226
|
+
function burnExpiredRewards(
|
|
227
|
+
address hook,
|
|
228
|
+
uint256[] calldata tierIds,
|
|
229
|
+
IERC20 token,
|
|
230
|
+
uint256[] calldata rounds
|
|
231
|
+
)
|
|
232
|
+
external
|
|
233
|
+
override
|
|
234
|
+
returns (uint256 amount)
|
|
235
|
+
{
|
|
236
|
+
amount = _burnExpiredRewards({hook: hook, groupId: _groupIdFor(tierIds), token: token, rounds: rounds});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// @notice Recycle unlocked rewards tied to burned NFTs in a tier-scoped group into the current reward round.
|
|
240
|
+
/// @dev Anyone can call this for burned tokens.
|
|
241
|
+
/// @param hook The 721 hook whose NFTs were burned.
|
|
242
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
243
|
+
/// @param tokenIds The IDs of the burned NFTs (reverts if any are not actually burned).
|
|
244
|
+
/// @param tokens The reward tokens to recycle.
|
|
245
|
+
/// @param beneficiary Unused for forfeiture. Kept for interface compatibility.
|
|
246
|
+
function releaseForfeitedRewards(
|
|
195
247
|
address hook,
|
|
248
|
+
uint256[] calldata tierIds,
|
|
196
249
|
uint256[] calldata tokenIds,
|
|
197
250
|
IERC20[] calldata tokens,
|
|
198
251
|
address beneficiary
|
|
199
252
|
)
|
|
200
|
-
|
|
201
|
-
override
|
|
253
|
+
external
|
|
254
|
+
override
|
|
202
255
|
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
256
|
+
_releaseForfeitedRewards({
|
|
257
|
+
hook: hook, groupId: _groupIdFor(tierIds), tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary
|
|
258
|
+
});
|
|
259
|
+
}
|
|
206
260
|
|
|
207
|
-
|
|
208
|
-
|
|
261
|
+
//*********************************************************************//
|
|
262
|
+
// ----------------------- external views ---------------------------- //
|
|
263
|
+
//*********************************************************************//
|
|
209
264
|
|
|
210
|
-
|
|
211
|
-
|
|
265
|
+
/// @notice Calculate the total uncollected (vesting + vested-but-uncollected) amount for an NFT token ID in a
|
|
266
|
+
/// tier-scoped group.
|
|
267
|
+
/// @param hook The 721 hook the tokenId belongs to.
|
|
268
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
269
|
+
/// @param tokenId The ID of the NFT token to calculate for.
|
|
270
|
+
/// @param token The reward token to check.
|
|
271
|
+
/// @return tokenAmount The total uncollected amount (vesting + vested-but-uncollected).
|
|
272
|
+
function claimedFor(
|
|
273
|
+
address hook,
|
|
274
|
+
uint256[] calldata tierIds,
|
|
275
|
+
uint256 tokenId,
|
|
276
|
+
IERC20 token
|
|
277
|
+
)
|
|
278
|
+
external
|
|
279
|
+
view
|
|
280
|
+
override
|
|
281
|
+
returns (uint256 tokenAmount)
|
|
282
|
+
{
|
|
283
|
+
tokenAmount =
|
|
284
|
+
_unclaimedVestingAmountOf({hook: hook, groupId: _groupIdFor(tierIds), tokenId: tokenId, token: token});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// @notice Calculate how much of a reward token is currently unlocked and ready to be collected for a given NFT
|
|
288
|
+
/// token ID in a tier-scoped group.
|
|
289
|
+
/// @param hook The 721 hook the tokenId belongs to.
|
|
290
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
291
|
+
/// @param tokenId The ID of the NFT token to calculate for.
|
|
292
|
+
/// @param token The reward token to check.
|
|
293
|
+
/// @return tokenAmount The amount of tokens that can be collected right now via `collectVestedRewards`.
|
|
294
|
+
function collectableFor(
|
|
295
|
+
address hook,
|
|
296
|
+
uint256[] calldata tierIds,
|
|
297
|
+
uint256 tokenId,
|
|
298
|
+
IERC20 token
|
|
299
|
+
)
|
|
300
|
+
external
|
|
301
|
+
view
|
|
302
|
+
override
|
|
303
|
+
returns (uint256 tokenAmount)
|
|
304
|
+
{
|
|
305
|
+
tokenAmount = _collectableFor({hook: hook, groupId: _groupIdFor(tierIds), tokenId: tokenId, token: token});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// @notice The tier set that defines a reward group, recorded when the group is first funded.
|
|
309
|
+
/// @param hook The 721 hook the group belongs to.
|
|
310
|
+
/// @param groupId The reward group.
|
|
311
|
+
/// @return tierIds The strictly-increasing tier set defining the group (empty for the all-tiers group, 0).
|
|
312
|
+
function tierIdsOf(address hook, uint256 groupId) external view override returns (uint256[] memory tierIds) {
|
|
313
|
+
tierIds = _tierIdsOfGroup[hook][groupId];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//*********************************************************************//
|
|
317
|
+
// ----------------------- public transactions ----------------------- //
|
|
318
|
+
//*********************************************************************//
|
|
319
|
+
|
|
320
|
+
/// @notice Begin vesting then collect everything unlocked for a tier-scoped reward group.
|
|
321
|
+
/// @param hook The 721 hook whose NFT owners are collecting.
|
|
322
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
323
|
+
/// @param tokenIds The IDs of the NFTs to collect for (caller must be authorized for all of them).
|
|
324
|
+
/// @param tokens The reward tokens to collect vested amounts of.
|
|
325
|
+
/// @param beneficiary The recipient of the collected tokens.
|
|
326
|
+
function collectVestedRewards(
|
|
327
|
+
address hook,
|
|
328
|
+
uint256[] calldata tierIds,
|
|
329
|
+
uint256[] calldata tokenIds,
|
|
330
|
+
IERC20[] calldata tokens,
|
|
331
|
+
address beneficiary
|
|
332
|
+
)
|
|
333
|
+
external
|
|
334
|
+
override
|
|
335
|
+
{
|
|
336
|
+
_collectVestedRewards({
|
|
337
|
+
hook: hook, groupId: _groupIdFor(tierIds), tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary
|
|
338
|
+
});
|
|
339
|
+
}
|
|
212
340
|
|
|
213
|
-
|
|
214
|
-
|
|
341
|
+
/// @notice Borrow against one NFT token ID's uncollected vesting rewards in a tier-scoped group.
|
|
342
|
+
/// @param hook The 721 hook whose NFT owner is borrowing against vesting rewards.
|
|
343
|
+
/// @param tierIds The strictly-increasing tier set defining the group.
|
|
344
|
+
/// @param tokenIds The single NFT token ID to borrow against.
|
|
345
|
+
/// @param tokens The single revnet reward token to collateralize.
|
|
346
|
+
/// @param sourceToken The token to borrow from the revnet.
|
|
347
|
+
/// @param minBorrowAmount The minimum amount to borrow, denominated in `sourceToken`.
|
|
348
|
+
/// @param prepaidFeePercent The fee percent to charge upfront.
|
|
349
|
+
/// @param beneficiary The recipient of the borrowed funds.
|
|
350
|
+
/// @return loanId The Revnet loan NFT ID held by this distributor.
|
|
351
|
+
/// @return collateralCount The amount of vesting rewards used as collateral.
|
|
352
|
+
function borrowAgainstVesting(
|
|
353
|
+
address hook,
|
|
354
|
+
uint256[] calldata tierIds,
|
|
355
|
+
uint256[] calldata tokenIds,
|
|
356
|
+
IERC20[] calldata tokens,
|
|
357
|
+
address sourceToken,
|
|
358
|
+
uint256 minBorrowAmount,
|
|
359
|
+
uint256 prepaidFeePercent,
|
|
360
|
+
address payable beneficiary
|
|
361
|
+
)
|
|
362
|
+
external
|
|
363
|
+
override
|
|
364
|
+
returns (uint256 loanId, uint256 collateralCount)
|
|
365
|
+
{
|
|
366
|
+
(loanId, collateralCount) = _borrowAgainstVestingFor({
|
|
367
|
+
hook: hook,
|
|
368
|
+
groupId: _groupIdFor(tierIds),
|
|
369
|
+
tokenIds: tokenIds,
|
|
370
|
+
tokens: tokens,
|
|
371
|
+
sourceToken: sourceToken,
|
|
372
|
+
minBorrowAmount: minBorrowAmount,
|
|
373
|
+
prepaidFeePercent: prepaidFeePercent,
|
|
374
|
+
beneficiary: beneficiary
|
|
375
|
+
});
|
|
215
376
|
}
|
|
216
377
|
|
|
217
378
|
//*********************************************************************//
|
|
@@ -232,16 +393,31 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
232
393
|
|
|
233
394
|
/// @notice Claim all past reward rounds for the given NFT token IDs and reward tokens into fresh vesting entries.
|
|
234
395
|
/// @param hook The 721 hook whose NFT owners are claiming.
|
|
396
|
+
/// @param groupId The reward group being claimed (0 = all tiers).
|
|
235
397
|
/// @param tokenIds The NFT token IDs to claim for.
|
|
236
398
|
/// @param tokens The reward tokens to claim.
|
|
237
|
-
function _claimPastRewards(
|
|
399
|
+
function _claimPastRewards(
|
|
400
|
+
address hook,
|
|
401
|
+
uint256 groupId,
|
|
402
|
+
uint256[] calldata tokenIds,
|
|
403
|
+
IERC20[] calldata tokens
|
|
404
|
+
)
|
|
405
|
+
internal
|
|
406
|
+
override
|
|
407
|
+
{
|
|
238
408
|
// Round 0 has no completed reward rounds behind it, so nothing can be claimed yet.
|
|
239
409
|
uint256 round = currentRound();
|
|
240
410
|
if (round == 0) return;
|
|
241
411
|
|
|
242
|
-
// Current-round funding is excluded. It becomes claimable only after a later round starts.
|
|
243
|
-
|
|
244
|
-
|
|
412
|
+
// Current-round funding is excluded. It becomes claimable only after a later round starts. For a tier-scoped
|
|
413
|
+
// group, load the tier set once so per-token eligibility can be checked without repeated storage reads.
|
|
414
|
+
JBClaimContext memory ctx = JBClaimContext({
|
|
415
|
+
hook: hook,
|
|
416
|
+
groupId: groupId,
|
|
417
|
+
tierIds: groupId == 0 ? new uint256[](0) : _tierIdsOfGroup[hook][groupId],
|
|
418
|
+
lastClaimableRound: round - 1,
|
|
419
|
+
vestingReleaseRound: round + VESTING_ROUNDS
|
|
420
|
+
});
|
|
245
421
|
|
|
246
422
|
// Process each reward token independently because each token has its own round funding and claim cursor.
|
|
247
423
|
for (uint256 i; i < tokens.length;) {
|
|
@@ -275,7 +451,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
275
451
|
|
|
276
452
|
// Find the earliest cursor in the batch, skipping token IDs that are already current.
|
|
277
453
|
for (uint256 i; i < tokenIds.length;) {
|
|
278
|
-
uint256 nextClaimRound = nextClaimRoundOf[ctx.hook][tokenIds[i]][token];
|
|
454
|
+
uint256 nextClaimRound = nextClaimRoundOf[ctx.hook][ctx.groupId][tokenIds[i]][token];
|
|
279
455
|
if (nextClaimRound <= ctx.lastClaimableRound && nextClaimRound < firstClaimRound) {
|
|
280
456
|
firstClaimRound = nextClaimRound;
|
|
281
457
|
}
|
|
@@ -291,17 +467,21 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
291
467
|
// Walk every unclaimed historical round needed by at least one token ID.
|
|
292
468
|
for (uint256 rewardRoundNumber = firstClaimRound; rewardRoundNumber <= ctx.lastClaimableRound;) {
|
|
293
469
|
// Load this reward round's funding, snapshot, claim counter, and deadline.
|
|
294
|
-
JBRewardRoundData storage rewardRound = rewardRoundOf[ctx.hook][token][rewardRoundNumber];
|
|
470
|
+
JBRewardRoundData storage rewardRound = rewardRoundOf[ctx.hook][ctx.groupId][token][rewardRoundNumber];
|
|
295
471
|
|
|
296
472
|
// Skip rounds that never received funding.
|
|
297
473
|
if (rewardRound.amount != 0) {
|
|
298
474
|
// Expired rounds can no longer be claimed as-is; recycle their unclaimed remainder instead.
|
|
299
475
|
if (_rewardRoundExpired(rewardRound)) {
|
|
300
|
-
_recycleExpiredRewardRound({
|
|
476
|
+
_recycleExpiredRewardRound({
|
|
477
|
+
hook: ctx.hook, groupId: ctx.groupId, token: token, round: rewardRoundNumber
|
|
478
|
+
});
|
|
301
479
|
} else if (rewardRound.totalStake != 0) {
|
|
302
480
|
// Bundle the fixed round data used by every NFT in the batch.
|
|
303
481
|
JBVestContext memory vestCtx = JBVestContext({
|
|
304
482
|
hook: ctx.hook,
|
|
483
|
+
groupId: ctx.groupId,
|
|
484
|
+
tierIds: ctx.tierIds,
|
|
305
485
|
token: token,
|
|
306
486
|
distributable: rewardRound.amount,
|
|
307
487
|
totalStakeAmount: rewardRound.totalStake,
|
|
@@ -333,17 +513,18 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
333
513
|
// Advance cursors even when a token ID earned zero, so empty or zero-stake rounds are not rescanned forever.
|
|
334
514
|
for (uint256 i; i < tokenIds.length;) {
|
|
335
515
|
uint256 tokenId = tokenIds[i];
|
|
336
|
-
nextClaimRoundOf[ctx.hook][tokenId][token] = ctx.lastClaimableRound + 1;
|
|
516
|
+
nextClaimRoundOf[ctx.hook][ctx.groupId][tokenId][token] = ctx.lastClaimableRound + 1;
|
|
337
517
|
|
|
338
518
|
// All accumulated past rewards for this NFT start a single fresh vesting schedule at the claim round.
|
|
339
519
|
if (tokenAmounts[i] != 0) {
|
|
340
|
-
vestingDataOf[ctx.hook][tokenId][token].push(
|
|
520
|
+
vestingDataOf[ctx.hook][ctx.groupId][tokenId][token].push(
|
|
341
521
|
JBVestingData({releaseRound: ctx.vestingReleaseRound, amount: tokenAmounts[i], shareClaimed: 0})
|
|
342
522
|
);
|
|
343
523
|
|
|
344
524
|
emit Claimed({
|
|
345
525
|
hook: ctx.hook,
|
|
346
526
|
tokenId: tokenId,
|
|
527
|
+
groupId: ctx.groupId,
|
|
347
528
|
token: token,
|
|
348
529
|
amount: tokenAmounts[i],
|
|
349
530
|
vestingReleaseRound: ctx.vestingReleaseRound,
|
|
@@ -370,6 +551,31 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
370
551
|
internal
|
|
371
552
|
returns (uint256 totalVestingAmount)
|
|
372
553
|
{
|
|
554
|
+
// Tier-scoped groups distribute a tier's pot among that tier's eligible NFTs. Each NFT contributes its tier's
|
|
555
|
+
// voting units, with no per-owner cap: the denominator (summed `getPastTierVotingUnits`) counts exactly those
|
|
556
|
+
// eligible NFTs, so the shares reconcile without the all-tiers delegation-cap machinery.
|
|
557
|
+
if (ctx.groupId != 0) {
|
|
558
|
+
for (uint256 j; j < tokenIds.length;) {
|
|
559
|
+
if (nextClaimRoundOf[ctx.hook][ctx.groupId][tokenIds[j]][ctx.token] <= ctx.rewardRound) {
|
|
560
|
+
uint256 stake = _tierScopedStake({ctx: ctx, tokenId: tokenIds[j]});
|
|
561
|
+
if (stake != 0) {
|
|
562
|
+
uint256 tokenAmount =
|
|
563
|
+
mulDiv({x: ctx.distributable, y: stake, denominator: ctx.totalStakeAmount});
|
|
564
|
+
tokenAmounts[j] += tokenAmount;
|
|
565
|
+
totalVestingAmount += tokenAmount;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
unchecked {
|
|
570
|
+
++j;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return totalVestingAmount;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// All-tiers group (0): split the pot pro-rata across delegated voting power, capping each owner so multiple
|
|
578
|
+
// NFTs cannot over-claim beyond the owner's checkpointed votes.
|
|
373
579
|
// Allocate scratch arrays sized to the maximum possible number of distinct snapshot owners.
|
|
374
580
|
address[] memory owners = new address[](tokenIds.length);
|
|
375
581
|
uint256[] memory consumed = new uint256[](tokenIds.length);
|
|
@@ -377,7 +583,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
377
583
|
|
|
378
584
|
// Claim each token ID that has not yet advanced past this reward round.
|
|
379
585
|
for (uint256 j; j < tokenIds.length;) {
|
|
380
|
-
if (nextClaimRoundOf[ctx.hook][tokenIds[j]][ctx.token] <= ctx.rewardRound) {
|
|
586
|
+
if (nextClaimRoundOf[ctx.hook][0][tokenIds[j]][ctx.token] <= ctx.rewardRound) {
|
|
381
587
|
(uint256 tokenAmount, uint256 newUniqueCount) = _claimRewardRoundForTokenId({
|
|
382
588
|
ctx: ctx, tokenId: tokenIds[j], owners: owners, consumed: consumed, uniqueCount: uniqueCount
|
|
383
589
|
});
|
|
@@ -473,75 +679,6 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
473
679
|
consumed[ownerIndex] += stake;
|
|
474
680
|
}
|
|
475
681
|
|
|
476
|
-
/// @notice Override vesting to cap each owner's consumed voting power across all their NFTs.
|
|
477
|
-
/// @dev Prevents an owner with N NFTs of V voting units each from claiming N*V when their pastVotes < N*V.
|
|
478
|
-
/// Iterates over all token IDs in the batch, delegating per-token logic to `_vestSingleToken`. A pair of
|
|
479
|
-
/// scratch arrays (`owners` and `consumed`) tracks how much voting power each distinct owner has used so far,
|
|
480
|
-
/// ensuring the aggregate claim never exceeds the owner's snapshot voting power.
|
|
481
|
-
/// Silently skips burned tokens, already-vested tokens, and tokens whose owner had no snapshot voting power.
|
|
482
|
-
/// @param hook The address of the 721 hook whose stakers are vesting.
|
|
483
|
-
/// @param tokenIds The NFT token IDs to vest rewards for.
|
|
484
|
-
/// @param token The ERC-20 reward token to distribute.
|
|
485
|
-
/// @param distributable The total distributable amount of `token` for this round.
|
|
486
|
-
/// @param totalStakeAmount The aggregate voting power at the round's snapshot block.
|
|
487
|
-
/// @param vestingReleaseRound The round number at which the vesting period ends and tokens become fully claimable.
|
|
488
|
-
/// @return totalVestingAmount The sum of reward tokens that began vesting across all processed token IDs.
|
|
489
|
-
function _vestTokenIds(
|
|
490
|
-
address hook,
|
|
491
|
-
uint256[] calldata tokenIds,
|
|
492
|
-
IERC20 token,
|
|
493
|
-
uint256 distributable,
|
|
494
|
-
uint256 totalStakeAmount,
|
|
495
|
-
uint256 vestingReleaseRound
|
|
496
|
-
)
|
|
497
|
-
internal
|
|
498
|
-
override
|
|
499
|
-
returns (uint256 totalVestingAmount)
|
|
500
|
-
{
|
|
501
|
-
// Bundle iteration-constant parameters into a struct to avoid stack-too-deep errors.
|
|
502
|
-
JBVestContext memory ctx = JBVestContext({
|
|
503
|
-
hook: hook,
|
|
504
|
-
token: token,
|
|
505
|
-
distributable: distributable,
|
|
506
|
-
totalStakeAmount: totalStakeAmount,
|
|
507
|
-
vestingReleaseRound: vestingReleaseRound,
|
|
508
|
-
rewardRound: currentRound(),
|
|
509
|
-
snapshotBlock: roundSnapshotBlock[currentRound()]
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// Allocate scratch arrays sized to the maximum possible number of distinct owners (one per token ID).
|
|
513
|
-
address[] memory owners = new address[](tokenIds.length);
|
|
514
|
-
uint256[] memory consumed = new uint256[](tokenIds.length);
|
|
515
|
-
|
|
516
|
-
// Track how many distinct owners have been recorded in the scratch arrays so far.
|
|
517
|
-
uint256 uniqueCount;
|
|
518
|
-
|
|
519
|
-
// Iterate over every token ID in the batch.
|
|
520
|
-
for (uint256 j; j < tokenIds.length;) {
|
|
521
|
-
// Vest the single token, receiving its reward amount and the updated distinct owner count.
|
|
522
|
-
(uint256 tokenAmount, uint256 newUniqueCount) = _vestSingleToken({
|
|
523
|
-
ctx: ctx, tokenId: tokenIds[j], owners: owners, consumed: consumed, uniqueCount: uniqueCount
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// Carry the updated owner count forward so subsequent tokens can reference the same tracking data.
|
|
527
|
-
uniqueCount = newUniqueCount;
|
|
528
|
-
|
|
529
|
-
unchecked {
|
|
530
|
-
// Accumulate the individual token's reward into the batch-wide total.
|
|
531
|
-
totalVestingAmount += tokenAmount;
|
|
532
|
-
++j;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Persist consumed voting power to storage to prevent cap resets across calls.
|
|
537
|
-
for (uint256 k; k < uniqueCount;) {
|
|
538
|
-
_consumedVotesOf[hook][token][ctx.rewardRound][owners[k]] = consumed[k];
|
|
539
|
-
unchecked {
|
|
540
|
-
++k;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
682
|
//*********************************************************************//
|
|
546
683
|
// ----------------------- internal views ---------------------------- //
|
|
547
684
|
//*********************************************************************//
|
|
@@ -555,6 +692,37 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
555
692
|
canClaim = IERC721(hook).ownerOf(tokenId) == account;
|
|
556
693
|
}
|
|
557
694
|
|
|
695
|
+
/// @notice Derive the canonical group ID for a tier set. The empty set is the all-tiers group (0).
|
|
696
|
+
/// @param tierIds Strictly-increasing tier IDs; empty for the all-tiers group.
|
|
697
|
+
/// @return groupId 0 for the all-tiers group, else `keccak256(abi.encode(tierIds))`.
|
|
698
|
+
function _groupIdFor(uint256[] calldata tierIds) internal pure returns (uint256 groupId) {
|
|
699
|
+
if (tierIds.length == 0) return 0;
|
|
700
|
+
for (uint256 i = 1; i < tierIds.length;) {
|
|
701
|
+
if (tierIds[i] <= tierIds[i - 1]) {
|
|
702
|
+
revert JB721Distributor_TierIdsNotIncreasing({previousTierId: tierIds[i - 1], tierId: tierIds[i]});
|
|
703
|
+
}
|
|
704
|
+
unchecked {
|
|
705
|
+
++i;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
groupId = uint256(keccak256(abi.encode(tierIds)));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/// @notice Whether a tier ID is present in a strictly-increasing tier set.
|
|
712
|
+
/// @param tierId The tier ID to look for.
|
|
713
|
+
/// @param tierIds The strictly-increasing tier set to search.
|
|
714
|
+
/// @return found True if `tierId` is in `tierIds`.
|
|
715
|
+
function _isTierInSet(uint256 tierId, uint256[] memory tierIds) internal pure returns (bool found) {
|
|
716
|
+
for (uint256 i; i < tierIds.length;) {
|
|
717
|
+
if (tierIds[i] == tierId) return true;
|
|
718
|
+
// The set is strictly increasing, so once an entry exceeds the target it cannot appear later.
|
|
719
|
+
if (tierIds[i] > tierId) return false;
|
|
720
|
+
unchecked {
|
|
721
|
+
++i;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
558
726
|
/// @notice Revert unless the caller is authorized to claim each NFT token ID.
|
|
559
727
|
/// @param hook The 721 hook whose NFT owners are claiming.
|
|
560
728
|
/// @param tokenIds The NFT token IDs to check.
|
|
@@ -577,6 +745,30 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
577
745
|
}
|
|
578
746
|
}
|
|
579
747
|
|
|
748
|
+
/// @notice The tier-scoped stake of a single NFT in a reward round: its tier's voting units if the NFT's tier is
|
|
749
|
+
/// in the group's set and the NFT existed at the round snapshot, else zero.
|
|
750
|
+
/// @dev No per-owner cap is applied. Eligibility (`ownerOfAt != 0`) plus tier membership matches exactly the set
|
|
751
|
+
/// counted by the `getPastTierVotingUnits` denominator, so per-NFT shares reconcile against the pot.
|
|
752
|
+
/// @param ctx The reward-round context (carries the group's tier set and snapshot block).
|
|
753
|
+
/// @param tokenId The NFT token ID to weigh.
|
|
754
|
+
/// @return stake The NFT's tier voting units, or 0 if ineligible.
|
|
755
|
+
function _tierScopedStake(JBVestContext memory ctx, uint256 tokenId) internal view returns (uint256 stake) {
|
|
756
|
+
// The NFT's tier must be one of the funded tiers.
|
|
757
|
+
uint256 tierId = IJB721TiersHook(ctx.hook).STORE().tierIdOfToken(tokenId);
|
|
758
|
+
if (!_isTierInSet({tierId: tierId, tierIds: ctx.tierIds})) return 0;
|
|
759
|
+
|
|
760
|
+
// The NFT must have existed at the round snapshot block (proven via the checkpoint owner history).
|
|
761
|
+
if (_snapshotOwnerOf({hook: ctx.hook, tokenId: tokenId, snapshotBlock: ctx.snapshotBlock}) == address(0)) {
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Eligible: weigh the NFT by its tier's voting units.
|
|
766
|
+
stake =
|
|
767
|
+
IJB721TiersHook(ctx.hook)
|
|
768
|
+
.STORE()
|
|
769
|
+
.tierOfTokenId({hook: ctx.hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
770
|
+
}
|
|
771
|
+
|
|
580
772
|
/// @notice Checks if the given token was burned.
|
|
581
773
|
/// @param hook The hook the token belongs to.
|
|
582
774
|
/// @param tokenId The tokenId to check.
|
|
@@ -620,16 +812,39 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
620
812
|
tokenStakeAmount = votingUnits < pastVotes ? votingUnits : pastVotes;
|
|
621
813
|
}
|
|
622
814
|
|
|
623
|
-
/// @notice The total stake
|
|
624
|
-
/// @dev
|
|
625
|
-
///
|
|
626
|
-
///
|
|
815
|
+
/// @notice The total stake sharing a group's round rewards at a specific block.
|
|
816
|
+
/// @dev For the all-tiers group (0) this is `getPastTotalSupply` from the hook's checkpoints module (all NFTs that
|
|
817
|
+
/// existed and were delegated at `blockNumber`). For a tier-scoped group it is the summed
|
|
818
|
+
/// `getPastTierVotingUnits` over the group's tier set — the eligible voting units of those tiers at the snapshot.
|
|
627
819
|
/// @param hook The hook to get the total stake for.
|
|
820
|
+
/// @param groupId The reward group (0 = all tiers).
|
|
628
821
|
/// @param blockNumber The block number to get the total staked amount at.
|
|
629
|
-
/// @return total The total
|
|
630
|
-
function _totalStake(
|
|
822
|
+
/// @return total The total stake at the given block.
|
|
823
|
+
function _totalStake(
|
|
824
|
+
address hook,
|
|
825
|
+
uint256 groupId,
|
|
826
|
+
uint256 blockNumber
|
|
827
|
+
)
|
|
828
|
+
internal
|
|
829
|
+
view
|
|
830
|
+
override
|
|
831
|
+
returns (uint256 total)
|
|
832
|
+
{
|
|
631
833
|
IJB721Checkpoints checkpoints = IJB721TiersHook(hook).checkpoints();
|
|
632
|
-
|
|
834
|
+
|
|
835
|
+
// All-tiers group (0): the global checkpointed voting supply.
|
|
836
|
+
if (groupId == 0) {
|
|
837
|
+
return IVotes(address(checkpoints)).getPastTotalSupply(blockNumber);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Tier-scoped group: sum the eligible voting units of each tier in the set at the snapshot block.
|
|
841
|
+
uint256[] memory tierIds = _tierIdsOfGroup[hook][groupId];
|
|
842
|
+
for (uint256 i; i < tierIds.length;) {
|
|
843
|
+
total += checkpoints.getPastTierVotingUnits({tierId: tierIds[i], blockNumber: blockNumber});
|
|
844
|
+
unchecked {
|
|
845
|
+
++i;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
633
848
|
}
|
|
634
849
|
|
|
635
850
|
//*********************************************************************//
|
|
@@ -664,132 +879,4 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
664
879
|
// A zero owner means the token was not owned at the snapshot block and is not eligible this round.
|
|
665
880
|
owner = abi.decode(data, (address));
|
|
666
881
|
}
|
|
667
|
-
|
|
668
|
-
/// @notice Vest a single NFT token, enforcing a per-owner voting power cap across the batch.
|
|
669
|
-
/// @dev Returns 0 for burned tokens, already-vested tokens, tokens whose owner had no snapshot voting power,
|
|
670
|
-
/// and tokens whose owner has already exhausted their voting power cap within this batch.
|
|
671
|
-
/// The `owners` and `consumed` arrays form a compact map that tracks how much voting power each unique
|
|
672
|
-
/// owner has consumed so far. `uniqueCount` tracks how many slots are used.
|
|
673
|
-
/// @param ctx The vesting context containing hook address, reward token, distributable amount, total stake,
|
|
674
|
-
/// and release round.
|
|
675
|
-
/// @param tokenId The NFT token ID to process.
|
|
676
|
-
/// @param owners A scratch array mapping slot indices to owner addresses for deduplication within this batch.
|
|
677
|
-
/// @param consumed A scratch array tracking how much voting power each owner (by slot index) has consumed.
|
|
678
|
-
/// @param uniqueCount The number of distinct owners seen so far in the batch.
|
|
679
|
-
/// @return tokenAmount The reward amount vested for this token ID (0 if skipped).
|
|
680
|
-
/// @return newUniqueCount The updated count of distinct owners after processing this token ID.
|
|
681
|
-
function _vestSingleToken(
|
|
682
|
-
JBVestContext memory ctx,
|
|
683
|
-
uint256 tokenId,
|
|
684
|
-
address[] memory owners,
|
|
685
|
-
uint256[] memory consumed,
|
|
686
|
-
uint256 uniqueCount
|
|
687
|
-
)
|
|
688
|
-
private
|
|
689
|
-
returns (uint256 tokenAmount, uint256 newUniqueCount)
|
|
690
|
-
{
|
|
691
|
-
// Initialize the return value to the current count of distinct owners.
|
|
692
|
-
newUniqueCount = uniqueCount;
|
|
693
|
-
|
|
694
|
-
// Skip burned tokens — they are excluded from _totalStake, so including them would overbook vesting.
|
|
695
|
-
if (_tokenBurned({hook: ctx.hook, tokenId: tokenId})) return (0, newUniqueCount);
|
|
696
|
-
|
|
697
|
-
// Skip already-vested tokenIds — check if the last vesting entry targets the same release round.
|
|
698
|
-
{
|
|
699
|
-
// Load the number of existing vesting entries for this token.
|
|
700
|
-
JBVestingData[] storage vestings = vestingDataOf[ctx.hook][tokenId][ctx.token];
|
|
701
|
-
uint256 numVesting = vestings.length;
|
|
702
|
-
|
|
703
|
-
// If at least one entry exists and its release round matches, this token was already vested this round.
|
|
704
|
-
if (numVesting != 0 && vestings[numVesting - 1].releaseRound == ctx.vestingReleaseRound) {
|
|
705
|
-
return (0, newUniqueCount);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Look up the NFT's voting units from its tier in the hook's store.
|
|
710
|
-
uint256 votingUnits =
|
|
711
|
-
IJB721TiersHook(ctx.hook)
|
|
712
|
-
.STORE()
|
|
713
|
-
.tierOfTokenId({hook: ctx.hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
714
|
-
|
|
715
|
-
// Look up the snapshot owner, verify snapshot eligibility, and find or create the owner's tracking slot.
|
|
716
|
-
uint256 ownerIndex;
|
|
717
|
-
uint256 pastVotes;
|
|
718
|
-
{
|
|
719
|
-
// Reuse the same round snapshot block for every token in this vesting batch.
|
|
720
|
-
uint256 snapshotBlock = ctx.snapshotBlock;
|
|
721
|
-
address owner = _snapshotOwnerOf({hook: ctx.hook, tokenId: tokenId, snapshotBlock: snapshotBlock});
|
|
722
|
-
if (owner == address(0)) return (0, newUniqueCount);
|
|
723
|
-
|
|
724
|
-
// Query the owner's checkpointed voting power at the round's snapshot block.
|
|
725
|
-
pastVotes = IVotes(address(IJB721TiersHook(ctx.hook).checkpoints()))
|
|
726
|
-
.getPastVotes({account: owner, timepoint: snapshotBlock});
|
|
727
|
-
|
|
728
|
-
// If the snapshot owner had no voting power at the snapshot block, the token is ineligible for this round.
|
|
729
|
-
if (pastVotes == 0) return (0, newUniqueCount);
|
|
730
|
-
|
|
731
|
-
// Search the owners array for an existing slot belonging to this owner.
|
|
732
|
-
bool found;
|
|
733
|
-
for (uint256 k; k < newUniqueCount;) {
|
|
734
|
-
if (owners[k] == owner) {
|
|
735
|
-
// Re-use the existing tracking slot for this owner.
|
|
736
|
-
ownerIndex = k;
|
|
737
|
-
found = true;
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
unchecked {
|
|
741
|
-
++k;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// If no existing slot was found, allocate a new one at the end of the arrays.
|
|
746
|
-
if (!found) {
|
|
747
|
-
ownerIndex = newUniqueCount;
|
|
748
|
-
owners[newUniqueCount] = owner;
|
|
749
|
-
// Initialize from persistent storage to prevent cap resets across calls.
|
|
750
|
-
consumed[newUniqueCount] = _consumedVotesOf[ctx.hook][ctx.token][ctx.rewardRound][owner];
|
|
751
|
-
unchecked {
|
|
752
|
-
++newUniqueCount;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Cap this NFT's effective stake at the owner's remaining voting power budget for this batch.
|
|
758
|
-
uint256 stake;
|
|
759
|
-
{
|
|
760
|
-
// Calculate how much voting power the owner has left after prior tokens in this batch.
|
|
761
|
-
uint256 remaining = pastVotes > consumed[ownerIndex] ? pastVotes - consumed[ownerIndex] : 0;
|
|
762
|
-
|
|
763
|
-
// The effective stake is the lesser of the NFT's voting units and the owner's remaining budget.
|
|
764
|
-
stake = votingUnits < remaining ? votingUnits : remaining;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// If the effective stake is zero, the owner's budget is exhausted — skip this token.
|
|
768
|
-
if (stake == 0) return (0, newUniqueCount);
|
|
769
|
-
|
|
770
|
-
// Calculate the pro-rata reward amount: (distributable * stake) / totalStakeAmount.
|
|
771
|
-
tokenAmount = mulDiv({x: ctx.distributable, y: stake, denominator: ctx.totalStakeAmount});
|
|
772
|
-
|
|
773
|
-
// If the pro-rata amount rounds to zero, do not consume the owner's voting budget.
|
|
774
|
-
if (tokenAmount == 0) return (0, newUniqueCount);
|
|
775
|
-
|
|
776
|
-
// Record that this owner has consumed additional voting power from their budget.
|
|
777
|
-
consumed[ownerIndex] += stake;
|
|
778
|
-
|
|
779
|
-
// Only create a vesting entry and emit an event if there is a non-zero reward.
|
|
780
|
-
// Push a new vesting data entry for this token ID, starting with zero shareClaimed.
|
|
781
|
-
vestingDataOf[ctx.hook][tokenId][ctx.token].push(
|
|
782
|
-
JBVestingData({releaseRound: ctx.vestingReleaseRound, amount: tokenAmount, shareClaimed: 0})
|
|
783
|
-
);
|
|
784
|
-
|
|
785
|
-
// Emit the claim event for off-chain indexers.
|
|
786
|
-
emit Claimed({
|
|
787
|
-
hook: ctx.hook,
|
|
788
|
-
tokenId: tokenId,
|
|
789
|
-
token: ctx.token,
|
|
790
|
-
amount: tokenAmount,
|
|
791
|
-
vestingReleaseRound: ctx.vestingReleaseRound,
|
|
792
|
-
caller: msg.sender
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
882
|
}
|