@bananapus/distributor-v6 0.0.24 → 0.0.26

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.
@@ -9,16 +9,23 @@ import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookCont
9
9
  import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
10
10
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
11
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
12
+ import {mulDiv} from "@prb/math/src/Common.sol";
12
13
 
13
- import {IJBTokenDistributor} from "./interfaces/IJBTokenDistributor.sol";
14
14
  import {JBDistributor} from "./JBDistributor.sol";
15
+ import {IJBDistributor} from "./interfaces/IJBDistributor.sol";
16
+ import {IJBTokenDistributor} from "./interfaces/IJBTokenDistributor.sol";
17
+ import {JBClaimContext} from "./structs/JBClaimContext.sol";
18
+ import {JBRewardRoundData} from "./structs/JBRewardRoundData.sol";
19
+ import {JBVestingData} from "./structs/JBVestingData.sol";
15
20
 
16
21
  /// @notice A singleton distributor that distributes ERC-20 rewards to IVotes-compatible token stakers with linear
17
22
  /// vesting.
18
23
  /// @dev Any project can use this distributor by configuring a payout split with
19
24
  /// `hook = this contract` and `beneficiary = address(their IVotes token)`.
20
- /// @dev The stake weight of each staker is their delegated voting power at round start (via `getPastVotes`).
21
- /// Holders must delegate (even to themselves) to participate. Non-delegated supply stays in pool for future rounds.
25
+ /// @dev The stake weight of each staker is their delegated voting power at the funded round's snapshot block.
26
+ /// Holders must delegate (even to themselves) to participate.
27
+ /// @dev Funded rewards are assigned to the funding round. Stakers claim historical rounds lazily; all unclaimed past
28
+ /// rewards begin vesting when the staker claims, not when the rewards were funded.
22
29
  /// @dev Implements `IJBSplitHook` so it can receive tokens directly from Juicebox project payout splits.
23
30
  contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
24
31
  //*********************************************************************//
@@ -44,6 +51,16 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
44
51
  /// @notice The JB directory used to verify terminal/controller callers.
45
52
  IJBDirectory public immutable DIRECTORY;
46
53
 
54
+ //*********************************************************************//
55
+ // --------------------- public stored properties -------------------- //
56
+ //*********************************************************************//
57
+
58
+ /// @notice The next reward round a staker has not yet claimed.
59
+ /// @custom:param hook The IVotes token whose stakers are claiming.
60
+ /// @custom:param tokenId The encoded staker address.
61
+ /// @custom:param token The reward token being claimed.
62
+ mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256))) public nextClaimRoundOf;
63
+
47
64
  //*********************************************************************//
48
65
  // -------------------------- constructor ---------------------------- //
49
66
  //*********************************************************************//
@@ -93,8 +110,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
93
110
  }
94
111
 
95
112
  if (msg.value != 0) {
96
- _balanceOf[hook][IERC20(context.token)] += msg.value;
97
- _accountedBalanceOf[IERC20(context.token)] += msg.value;
113
+ // Assign native split proceeds to the current reward round for this IVotes hook.
114
+ _recordRewardFunding({hook: hook, token: IERC20(context.token), amount: msg.value, claimDuration: 0});
98
115
  }
99
116
  } else {
100
117
  // Validate that native ETH is not cross-booked under an ERC-20 token.
@@ -110,11 +127,65 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
110
127
  // allowance before calling. Balance delta handles fee-on-transfer tokens correctly.
111
128
  uint256 delta =
112
129
  _acceptErc20FundsFrom({token: IERC20(context.token), from: msg.sender, amount: context.amount});
113
- _balanceOf[hook][IERC20(context.token)] += delta;
114
- _accountedBalanceOf[IERC20(context.token)] += delta;
130
+
131
+ // Assign only the amount actually received to this round's reward pot.
132
+ _recordRewardFunding({hook: hook, token: IERC20(context.token), amount: delta, claimDuration: 0});
115
133
  }
116
134
  }
117
135
 
136
+ /// @notice Snapshot this staker's past reward rounds and start vesting them now.
137
+ /// @dev Unlike the shared distributor flow, token claims are owner-initiated. This prevents third parties from
138
+ /// starting a staker's vesting clock before the staker actually claims.
139
+ /// @param hook The IVotes token whose stakers are vesting.
140
+ /// @param tokenIds The encoded staker addresses to claim for.
141
+ /// @param tokens The reward tokens to begin vesting.
142
+ function beginVesting(
143
+ address hook,
144
+ uint256[] calldata tokenIds,
145
+ IERC20[] calldata tokens
146
+ )
147
+ external
148
+ override(JBDistributor, IJBDistributor)
149
+ {
150
+ // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
151
+ _requireNotAcceptingToken();
152
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
153
+
154
+ // Token IDs encode staker addresses, so only the encoded staker can start their own vesting clock.
155
+ _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
156
+
157
+ // Materialize all unclaimed historical rewards into fresh vesting entries that start now.
158
+ _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
159
+ }
160
+
161
+ /// @notice Collect already-vested rewards and first start vesting any unclaimed past reward rounds.
162
+ /// @param hook The IVotes token whose stakers are collecting.
163
+ /// @param tokenIds The encoded staker addresses to collect for.
164
+ /// @param tokens The reward tokens to collect.
165
+ /// @param beneficiary The recipient of collected vested rewards.
166
+ function collectVestedRewards(
167
+ address hook,
168
+ uint256[] calldata tokenIds,
169
+ IERC20[] calldata tokens,
170
+ address beneficiary
171
+ )
172
+ public
173
+ override(JBDistributor, IJBDistributor)
174
+ {
175
+ // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
176
+ _requireNotAcceptingToken();
177
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
178
+
179
+ // Only the encoded staker can materialize and collect their token rewards.
180
+ _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
181
+
182
+ // Before collecting, bring the caller current by starting vesting for any past reward rounds.
183
+ _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
184
+
185
+ // Release whatever portion of existing vesting entries has unlocked by this round.
186
+ _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true});
187
+ }
188
+
118
189
  //*********************************************************************//
119
190
  // -------------------------- public views --------------------------- //
120
191
  //*********************************************************************//
@@ -127,10 +198,157 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
127
198
  || interfaceId == type(IERC165).interfaceId;
128
199
  }
129
200
 
201
+ //*********************************************************************//
202
+ // ---------------------- internal transactions ---------------------- //
203
+ //*********************************************************************//
204
+
205
+ /// @notice Claim all past reward rounds for the given token IDs and reward tokens into fresh vesting entries.
206
+ /// @param hook The IVotes token whose stakers are claiming.
207
+ /// @param tokenIds The encoded staker addresses to claim for.
208
+ /// @param tokens The reward tokens to claim.
209
+ function _claimPastRewards(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) internal {
210
+ // Round 0 has no completed reward rounds behind it, so nothing can be claimed yet.
211
+ uint256 round = currentRound();
212
+ if (round == 0) return;
213
+
214
+ // Current-round funding is excluded. It becomes claimable only after a later round starts.
215
+ JBClaimContext memory ctx =
216
+ JBClaimContext({hook: hook, lastClaimableRound: round - 1, vestingReleaseRound: round + vestingRounds});
217
+
218
+ // Process each reward token independently because each token has its own round funding and claim cursor.
219
+ for (uint256 i; i < tokens.length;) {
220
+ IERC20 token = tokens[i];
221
+ uint256 totalVestingAmount;
222
+
223
+ // Materialize this reward token for every staker address encoded in tokenIds.
224
+ for (uint256 j; j < tokenIds.length;) {
225
+ uint256 tokenId = tokenIds[j];
226
+ uint256 tokenAmount = _claimPastRewardsForTokenId({ctx: ctx, tokenId: tokenId, token: token});
227
+
228
+ // Accumulate once per reward token so totalVestingAmountOf is updated with one storage write.
229
+ totalVestingAmount += tokenAmount;
230
+
231
+ unchecked {
232
+ ++j;
233
+ }
234
+ }
235
+
236
+ // Track the newly claimed amount as vesting, so later collections unlock against it over time.
237
+ if (totalVestingAmount != 0) totalVestingAmountOf[hook][token] += totalVestingAmount;
238
+
239
+ unchecked {
240
+ ++i;
241
+ }
242
+ }
243
+ }
244
+
245
+ /// @notice Claim all past reward rounds for one token ID into one fresh vesting entry.
246
+ /// @param ctx The claim-round context.
247
+ /// @param tokenId The encoded staker address to claim for.
248
+ /// @param token The reward token to claim.
249
+ /// @return tokenAmount The amount added to vesting.
250
+ function _claimPastRewardsForTokenId(
251
+ JBClaimContext memory ctx,
252
+ uint256 tokenId,
253
+ IERC20 token
254
+ )
255
+ internal
256
+ returns (uint256 tokenAmount)
257
+ {
258
+ // Load this staker's cursor for the reward token. All earlier rounds have already been settled.
259
+ uint256 nextClaimRound = nextClaimRoundOf[ctx.hook][tokenId][token];
260
+
261
+ // If the cursor is already past the last completed round, this staker is current.
262
+ if (nextClaimRound > ctx.lastClaimableRound) return 0;
263
+
264
+ // Sum this staker's pro-rata share from every unclaimed completed reward round.
265
+ tokenAmount = _claimRewardsFor({
266
+ hook: ctx.hook,
267
+ tokenId: tokenId,
268
+ token: token,
269
+ firstRound: nextClaimRound,
270
+ lastRound: ctx.lastClaimableRound
271
+ });
272
+
273
+ // Advance the cursor even when the amount is zero, so empty or zero-stake rounds are not rescanned forever.
274
+ nextClaimRoundOf[ctx.hook][tokenId][token] = ctx.lastClaimableRound + 1;
275
+ if (tokenAmount == 0) return 0;
276
+
277
+ // All accumulated past rewards start a single fresh vesting schedule at the claim round.
278
+ vestingDataOf[ctx.hook][tokenId][token].push(
279
+ JBVestingData({releaseRound: ctx.vestingReleaseRound, amount: tokenAmount, shareClaimed: 0})
280
+ );
281
+
282
+ emit Claimed({
283
+ hook: ctx.hook,
284
+ tokenId: tokenId,
285
+ token: token,
286
+ amount: tokenAmount,
287
+ vestingReleaseRound: ctx.vestingReleaseRound,
288
+ caller: msg.sender
289
+ });
290
+ }
291
+
130
292
  //*********************************************************************//
131
293
  // ----------------------- internal views ---------------------------- //
132
294
  //*********************************************************************//
133
295
 
296
+ /// @notice Claim a staker's unclaimed rewards across a range of historical reward rounds.
297
+ /// @param hook The IVotes token whose stakers are claiming.
298
+ /// @param tokenId The encoded staker address.
299
+ /// @param token The reward token.
300
+ /// @param firstRound The first reward round to include.
301
+ /// @param lastRound The last reward round to include.
302
+ /// @return tokenAmount The cumulative unclaimed reward amount.
303
+ function _claimRewardsFor(
304
+ address hook,
305
+ uint256 tokenId,
306
+ IERC20 token,
307
+ uint256 firstRound,
308
+ uint256 lastRound
309
+ )
310
+ internal
311
+ returns (uint256 tokenAmount)
312
+ {
313
+ // Walk every unclaimed historical round. The caller bounds this to completed rounds only.
314
+ for (uint256 rewardRoundNumber = firstRound; rewardRoundNumber <= lastRound;) {
315
+ // Load this round's reward data for the hook and reward token.
316
+ JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][rewardRoundNumber];
317
+
318
+ // Skip rounds that never received funding.
319
+ if (rewardRound.amount != 0) {
320
+ // Expired rounds can no longer be claimed; burn their unclaimed remainder instead.
321
+ if (_rewardRoundExpired(rewardRound)) {
322
+ _burnExpiredRewardRound({hook: hook, token: token, round: rewardRoundNumber});
323
+ } else if (rewardRound.totalStake != 0) {
324
+ // Use the funding round's snapshot block, not the block at which the staker finally claims.
325
+ uint256 tokenStakeAmount =
326
+ _tokenStakeAt({hook: hook, tokenId: tokenId, blockNumber: rewardRound.snapshotBlock});
327
+
328
+ // Zero-vote stakers advance their cursor but do not consume reward inventory.
329
+ if (tokenStakeAmount != 0) {
330
+ // The round's reward pot is split pro-rata across checkpointed voting power.
331
+ uint256 claimAmount =
332
+ mulDiv({x: rewardRound.amount, y: tokenStakeAmount, denominator: rewardRound.totalStake});
333
+
334
+ // Ignore floor-rounded zero claims to avoid unnecessary storage writes.
335
+ if (claimAmount != 0) {
336
+ // Track the portion that has started vesting so expiry burns only the remainder.
337
+ rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + claimAmount);
338
+
339
+ // Add this round's vested amount to the staker's cumulative claim.
340
+ tokenAmount += claimAmount;
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ unchecked {
347
+ ++rewardRoundNumber;
348
+ }
349
+ }
350
+ }
351
+
134
352
  /// @notice Check if the account matches the staker address encoded in the tokenId.
135
353
  /// @dev tokenId encodes the staker address as `uint256(uint160(stakerAddress))`.
136
354
  /// @param hook Unused — access is determined by the tokenId encoding.
@@ -164,11 +382,8 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
164
382
  /// @param tokenId The encoded staker address (`uint256(uint160(stakerAddress))`).
165
383
  /// @return tokenStakeAmount The delegated voting power at the round's snapshot block.
166
384
  function _tokenStake(address hook, uint256 tokenId) internal view override returns (uint256 tokenStakeAmount) {
167
- if (tokenId >> 160 != 0) revert JBTokenDistributor_InvalidTokenId({tokenId: tokenId});
168
- // The high bits were checked above, so this cast recovers the encoded address.
169
- // forge-lint: disable-next-line(unsafe-typecast)
170
- address account = address(uint160(tokenId));
171
- tokenStakeAmount = IVotes(hook).getPastVotes({account: account, timepoint: roundSnapshotBlock[currentRound()]});
385
+ tokenStakeAmount =
386
+ _tokenStakeAt({hook: hook, tokenId: tokenId, blockNumber: roundSnapshotBlock[currentRound()]});
172
387
  }
173
388
 
174
389
  /// @notice The total supply of votes at a specific block.
@@ -179,4 +394,45 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
179
394
  function _totalStake(address hook, uint256 blockNumber) internal view override returns (uint256 totalStakedAmount) {
180
395
  totalStakedAmount = IVotes(hook).getPastTotalSupply(blockNumber);
181
396
  }
397
+
398
+ /// @notice Revert unless the caller is authorized to claim each token ID.
399
+ /// @param hook The IVotes token whose stakers are claiming.
400
+ /// @param tokenIds The encoded staker addresses to check.
401
+ function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view {
402
+ // Each tokenId is an encoded address, so every requested claim must belong to msg.sender.
403
+ for (uint256 i; i < tokenIds.length;) {
404
+ if (!_canClaim({hook: hook, tokenId: tokenIds[i], account: msg.sender})) {
405
+ revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
406
+ }
407
+
408
+ unchecked {
409
+ ++i;
410
+ }
411
+ }
412
+ }
413
+
414
+ /// @notice The delegated voting power of a staker at an explicit snapshot block.
415
+ /// @param hook The IVotes-compatible token contract.
416
+ /// @param tokenId The encoded staker address.
417
+ /// @param blockNumber The historical block to query.
418
+ /// @return tokenStakeAmount The delegated voting power at `blockNumber`.
419
+ function _tokenStakeAt(
420
+ address hook,
421
+ uint256 tokenId,
422
+ uint256 blockNumber
423
+ )
424
+ internal
425
+ view
426
+ returns (uint256 tokenStakeAmount)
427
+ {
428
+ // Reject aliases where high bits would be truncated by the address cast below.
429
+ if (tokenId >> 160 != 0) revert JBTokenDistributor_InvalidTokenId({tokenId: tokenId});
430
+
431
+ // The high bits were checked above, so this cast recovers the encoded address.
432
+ // forge-lint: disable-next-line(unsafe-typecast)
433
+ address account = address(uint160(tokenId));
434
+
435
+ // Query the staker's delegated votes at the reward round's fixed snapshot block.
436
+ tokenStakeAmount = IVotes(hook).getPastVotes({account: account, timepoint: blockNumber});
437
+ }
182
438
  }
@@ -5,8 +5,8 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
5
 
6
6
  import {JBTokenSnapshotData} from "../structs/JBTokenSnapshotData.sol";
7
7
 
8
- /// @notice Interface for round-based reward distributors with linear vesting. Stakers claim their share of a
9
- /// distributable balance each round, and claimed amounts vest linearly over a configurable number of rounds.
8
+ /// @notice Interface for round-based reward distributors with linear vesting. Stakers claim their share of funded
9
+ /// reward rounds, and claimed amounts vest linearly over a configurable number of rounds.
10
10
  /// Two implementations exist: `JBTokenDistributor` (IVotes token stakers) and `JB721Distributor` (NFT holders).
11
11
  interface IJBDistributor {
12
12
  //*********************************************************************//
@@ -19,8 +19,14 @@ interface IJBDistributor {
19
19
  /// @param token The address of the token to vest.
20
20
  /// @param amount The amount of tokens to vest.
21
21
  /// @param vestingReleaseRound The round at which the tokens will be fully released.
22
+ /// @param caller The address that triggered the claim.
22
23
  event Claimed(
23
- address indexed hook, uint256 indexed tokenId, IERC20 token, uint256 amount, uint256 vestingReleaseRound
24
+ address indexed hook,
25
+ uint256 indexed tokenId,
26
+ IERC20 token,
27
+ uint256 amount,
28
+ uint256 vestingReleaseRound,
29
+ address caller
24
30
  );
25
31
 
26
32
  /// @notice Emitted when vested tokens are collected.
@@ -29,14 +35,31 @@ interface IJBDistributor {
29
35
  /// @param token The address of the token collected.
30
36
  /// @param amount The amount of tokens collected.
31
37
  /// @param vestingReleaseRound The round at which the tokens will be fully released.
38
+ /// @param caller The address that triggered the collection.
32
39
  event Collected(
33
- address indexed hook, uint256 indexed tokenId, IERC20 token, uint256 amount, uint256 vestingReleaseRound
40
+ address indexed hook,
41
+ uint256 indexed tokenId,
42
+ IERC20 token,
43
+ uint256 amount,
44
+ uint256 vestingReleaseRound,
45
+ address caller
34
46
  );
35
47
 
36
48
  /// @notice Emitted when a snapshot block is first recorded for a round.
37
49
  /// @param round The round the snapshot block was recorded for.
38
50
  /// @param snapshotBlock The block number recorded as the snapshot point.
39
- event RoundSnapshotRecorded(uint256 indexed round, uint256 snapshotBlock);
51
+ /// @param caller The address that triggered the snapshot recording.
52
+ event RoundSnapshotRecorded(uint256 indexed round, uint256 snapshotBlock, address caller);
53
+
54
+ /// @notice Emitted when an expired reward round's unclaimed amount is burned.
55
+ /// @param hook The hook whose expired rewards were burned.
56
+ /// @param round The expired reward round.
57
+ /// @param token The reward token that was burned.
58
+ /// @param amount The unclaimed reward amount burned.
59
+ /// @param caller The address that triggered the burn.
60
+ event ExpiredRewardsBurned(
61
+ address indexed hook, uint256 indexed round, IERC20 indexed token, uint256 amount, address caller
62
+ );
40
63
 
41
64
  /// @notice Emitted when a snapshot is created for a round.
42
65
  /// @param hook The hook the snapshot is for.
@@ -44,8 +67,14 @@ interface IJBDistributor {
44
67
  /// @param token The token the snapshot is of.
45
68
  /// @param balance The token balance at the time of the snapshot.
46
69
  /// @param vestingAmount The amount of tokens vesting at the time of the snapshot.
70
+ /// @param caller The address that triggered the snapshot.
47
71
  event SnapshotCreated(
48
- address indexed hook, uint256 indexed round, IERC20 indexed token, uint256 balance, uint256 vestingAmount
72
+ address indexed hook,
73
+ uint256 indexed round,
74
+ IERC20 indexed token,
75
+ uint256 balance,
76
+ uint256 vestingAmount,
77
+ address caller
49
78
  );
50
79
 
51
80
  //*********************************************************************//
@@ -135,6 +164,22 @@ interface IJBDistributor {
135
164
  /// @param amount The amount to fund.
136
165
  function fund(address hook, IERC20 token, uint256 amount) external payable;
137
166
 
167
+ /// @notice Fund the distributor for a specific hook with expiring rewards.
168
+ /// @dev `claimDuration` is measured from the first timestamp at which the funded round can be claimed.
169
+ /// @dev A zero duration means the reward round does not expire.
170
+ /// @param hook The hook to fund.
171
+ /// @param token The token to fund with.
172
+ /// @param amount The amount to fund.
173
+ /// @param claimDuration The number of seconds claimants have after the round becomes claimable.
174
+ function fundWithClaimDuration(address hook, IERC20 token, uint256 amount, uint48 claimDuration) external payable;
175
+
176
+ /// @notice Burn unclaimed rewards from expired reward rounds.
177
+ /// @param hook The hook whose expired reward rounds should be burned.
178
+ /// @param token The reward token to burn.
179
+ /// @param rounds The reward rounds to burn.
180
+ /// @return amount The total amount burned.
181
+ function burnExpiredRewards(address hook, IERC20 token, uint256[] calldata rounds) external returns (uint256 amount);
182
+
138
183
  /// @notice Record the snapshot block for the current round. Callable by anyone (keepers, frontends).
139
184
  function poke() external;
140
185
 
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @custom:member hook The stake source whose historical rewards are being claimed.
5
+ /// @custom:member lastClaimableRound The last completed reward round included in the claim.
6
+ /// @custom:member vestingReleaseRound The round at which newly materialized rewards finish vesting.
7
+ struct JBClaimContext {
8
+ address hook;
9
+ uint256 lastClaimableRound;
10
+ uint256 vestingReleaseRound;
11
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @notice A reward amount assigned to a specific distributor round.
5
+ /// @custom:member amount The reward amount assigned to the round.
6
+ /// @custom:member snapshotBlock The block used for per-account historical stake lookups.
7
+ /// @custom:member claimedAmount The reward amount already materialized into vesting.
8
+ /// @custom:member claimDeadline The timestamp at which unclaimed rewards can be burned. Zero means no expiration.
9
+ /// @custom:member totalStake The aggregate stake at the round's snapshot block.
10
+ struct JBRewardRoundData {
11
+ uint208 amount;
12
+ uint48 snapshotBlock;
13
+ uint208 claimedAmount;
14
+ uint48 claimDeadline;
15
+ uint208 totalStake;
16
+ }
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+
6
+ /// @custom:member hook The stake source whose rewards are being vested.
7
+ /// @custom:member token The reward token being vested.
8
+ /// @custom:member distributable The reward amount assigned to the round.
9
+ /// @custom:member totalStakeAmount The total checkpointed stake sharing the round's rewards.
10
+ /// @custom:member vestingReleaseRound The round at which newly materialized rewards finish vesting.
11
+ /// @custom:member rewardRound The historical reward round being claimed.
12
+ /// @custom:member snapshotBlock The block used for historical stake and ownership lookups.
13
+ struct JBVestContext {
14
+ address hook;
15
+ IERC20 token;
16
+ uint256 distributable;
17
+ uint256 totalStakeAmount;
18
+ uint256 vestingReleaseRound;
19
+ uint256 rewardRound;
20
+ uint256 snapshotBlock;
21
+ }