@bananapus/distributor-v6 0.0.30 → 0.0.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/distributor-v6",
3
- "version": "0.0.30",
3
+ "version": "0.0.31",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -85,7 +85,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
85
85
  //*********************************************************************//
86
86
 
87
87
  /// @param directory The JB directory used to verify terminal/controller callers.
88
- /// @param controller The JB controller used to burn expired or forfeited project-token rewards.
88
+ /// @param controller The JB controller used to burn forfeited project-token rewards.
89
89
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
90
90
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
91
91
  /// @param initialRoundDuration The duration of each round, specified in seconds.
@@ -295,9 +295,9 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
295
295
 
296
296
  // Skip rounds that never received funding.
297
297
  if (rewardRound.amount != 0) {
298
- // Expired rounds can no longer be claimed; burn their unclaimed remainder instead.
298
+ // Expired rounds can no longer be claimed as-is; recycle their unclaimed remainder instead.
299
299
  if (_rewardRoundExpired(rewardRound)) {
300
- _burnExpiredRewardRound({hook: ctx.hook, token: token, round: rewardRoundNumber});
300
+ _recycleExpiredRewardRound({hook: ctx.hook, token: token, round: rewardRoundNumber});
301
301
  } else if (rewardRound.totalStake != 0) {
302
302
  // Bundle the fixed round data used by every NFT in the batch.
303
303
  JBVestContext memory vestCtx = JBVestContext({
@@ -314,7 +314,8 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
314
314
  uint256 roundVestingAmount =
315
315
  _claimRewardRoundForTokenIds({ctx: vestCtx, tokenIds: tokenIds, tokenAmounts: tokenAmounts});
316
316
 
317
- // Track only the amount that actually started vesting, leaving zero-vote and dust amounts burnable.
317
+ // Track only the amount that actually started vesting, leaving zero-vote and dust amounts
318
+ // recyclable.
318
319
  if (roundVestingAmount != 0) {
319
320
  rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + roundVestingAmount);
320
321
 
@@ -80,6 +80,9 @@ abstract contract JBDistributor is IJBDistributor {
80
80
  /// @notice Thrown when unexpected native ETH is sent with an ERC-20 operation.
81
81
  error JBDistributor_UnexpectedNativeValue(uint256 msgValue, address token);
82
82
 
83
+ /// @notice Thrown when an ERC-20 repayment does not credit the exact amount pulled from the caller.
84
+ error JBDistributor_UnexpectedRepayAmount(uint256 amount, uint256 expectedAmount);
85
+
83
86
  /// @notice Thrown when a function requires exactly one reward token.
84
87
  error JBDistributor_UnexpectedTokenCount(uint256 tokenCount);
85
88
 
@@ -123,7 +126,7 @@ abstract contract JBDistributor is IJBDistributor {
123
126
  /// @dev A zero duration means reward rounds do not expire.
124
127
  uint48 public immutable override CLAIM_DURATION;
125
128
 
126
- /// @notice The JB controller used to burn expired or forfeited project-token rewards.
129
+ /// @notice The JB controller used to burn forfeited project-token rewards.
127
130
  IJBController public immutable override CONTROLLER;
128
131
 
129
132
  /// @notice The duration of each round, specified in seconds.
@@ -228,7 +231,7 @@ abstract contract JBDistributor is IJBDistributor {
228
231
  // -------------------------- constructor ---------------------------- //
229
232
  //*********************************************************************//
230
233
 
231
- /// @param controller The JB controller used to burn expired or forfeited project-token rewards.
234
+ /// @param controller The JB controller used to burn forfeited project-token rewards.
232
235
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
233
236
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
234
237
  /// @param initialRoundDuration The duration of each round, specified in seconds.
@@ -349,11 +352,12 @@ abstract contract JBDistributor is IJBDistributor {
349
352
  _fund({hook: hook, token: token, amount: amount});
350
353
  }
351
354
 
352
- /// @notice Burn unclaimed rewards from expired reward rounds.
353
- /// @param hook The hook whose expired rewards should be burned.
354
- /// @param token The reward token to burn.
355
- /// @param rounds The reward rounds to burn.
356
- /// @return amount The total amount burned.
355
+ /// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
356
+ /// @dev The selector name is kept for compatibility with existing keeper integrations.
357
+ /// @param hook The hook whose expired rewards should be recycled.
358
+ /// @param token The reward token to recycle.
359
+ /// @param rounds The reward rounds to recycle.
360
+ /// @return amount The total amount recycled.
357
361
  function burnExpiredRewards(
358
362
  address hook,
359
363
  IERC20 token,
@@ -364,13 +368,13 @@ abstract contract JBDistributor is IJBDistributor {
364
368
  override
365
369
  returns (uint256 amount)
366
370
  {
367
- // Do not let reward-token callbacks burn inventory during an inbound balance-delta measurement.
371
+ // Do not let reward-token callbacks recycle inventory during an inbound balance-delta measurement.
368
372
  _requireNotAcceptingToken();
369
373
 
370
374
  // Process every requested round independently so callers can batch keeper work.
371
375
  for (uint256 i; i < rounds.length;) {
372
376
  // Add this round's expired remainder to the batch total.
373
- amount += _burnExpiredRewardRound({hook: hook, token: token, round: rounds[i]});
377
+ amount += _recycleExpiredRewardRound({hook: hook, token: token, round: rounds[i]});
374
378
 
375
379
  unchecked {
376
380
  // Safe because the loop is bounded by calldata length.
@@ -920,9 +924,14 @@ abstract contract JBDistributor is IJBDistributor {
920
924
  revert JBDistributor_UnexpectedNativeValue({msgValue: msg.value, token: loan.sourceToken});
921
925
  }
922
926
 
923
- // Pull the exact current payoff from the caller.
927
+ // Pull the exact current payoff from the caller. Existing distributor inventory must not cover a shortfall.
924
928
  IERC20 sourceToken = IERC20(loan.sourceToken);
929
+ uint256 sourceBalanceBefore = sourceToken.balanceOf(address(this));
925
930
  sourceToken.safeTransferFrom({from: msg.sender, to: address(this), value: repayBorrowAmount});
931
+ uint256 receivedAmount = sourceToken.balanceOf(address(this)) - sourceBalanceBefore;
932
+ if (receivedAmount != repayBorrowAmount) {
933
+ revert JBDistributor_UnexpectedRepayAmount({amount: receivedAmount, expectedAmount: repayBorrowAmount});
934
+ }
926
935
 
927
936
  // Approve only the exact amount needed for this repayment.
928
937
  sourceToken.forceApprove({spender: address(REV_LOANS), value: repayBorrowAmount});
@@ -1158,32 +1167,47 @@ abstract contract JBDistributor is IJBDistributor {
1158
1167
  rewardRound.amount = _toUint208(uint256(rewardRound.amount) + amount);
1159
1168
  }
1160
1169
 
1161
- /// @notice Burn one expired reward round's unclaimed inventory.
1162
- /// @param hook The hook whose expired rewards should be burned.
1163
- /// @param token The reward token to burn.
1164
- /// @param round The reward round to burn.
1165
- /// @return burnAmount The amount burned.
1166
- function _burnExpiredRewardRound(address hook, IERC20 token, uint256 round) internal returns (uint256 burnAmount) {
1170
+ /// @notice Recycle one expired reward round's unclaimed inventory into the current reward round.
1171
+ /// @param hook The hook whose expired rewards should be recycled.
1172
+ /// @param token The reward token to recycle.
1173
+ /// @param round The reward round to recycle.
1174
+ /// @return recycleAmount The amount recycled.
1175
+ function _recycleExpiredRewardRound(
1176
+ address hook,
1177
+ IERC20 token,
1178
+ uint256 round
1179
+ )
1180
+ internal
1181
+ returns (uint256 recycleAmount)
1182
+ {
1167
1183
  // Load the reward round once so expiry, claimed amount, and funded amount stay in sync.
1168
1184
  JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
1169
1185
 
1170
1186
  // Ignore rounds that either never expire or have not reached their deadline yet.
1171
1187
  if (!_rewardRoundExpired(rewardRound)) return 0;
1172
1188
 
1173
- // If prior claims have already materialized the whole round, there is nothing left to burn.
1189
+ // If prior claims have already materialized the whole round, there is nothing left to recycle.
1174
1190
  if (rewardRound.claimedAmount >= rewardRound.amount) return 0;
1175
1191
 
1176
- // Burn only the unclaimed remainder, preserving amounts that already started vesting.
1177
- burnAmount = uint256(rewardRound.amount) - uint256(rewardRound.claimedAmount);
1192
+ // Recycle only the unclaimed remainder, preserving amounts that already started vesting.
1193
+ recycleAmount = uint256(rewardRound.amount) - uint256(rewardRound.claimedAmount);
1178
1194
 
1179
- // Mark the whole round settled before transferring to close reentrancy-sensitive accounting.
1195
+ // Mark the whole round settled before writing the recycled amount into a fresh round.
1180
1196
  rewardRound.claimedAmount = rewardRound.amount;
1181
1197
 
1182
- // Remove the expired remainder from distributor inventory and burn it through the JB controller.
1183
- _burnRewardTokens({hook: hook, token: token, amount: burnAmount});
1198
+ // Keep the inventory in the distributor and give the current staker set a new claimable round.
1199
+ uint256 recycledToRound = currentRound();
1200
+ _recordRewardRound({hook: hook, token: token, amount: recycleAmount});
1184
1201
 
1185
- // Surface the permissionless burn for off-chain accounting.
1186
- emit ExpiredRewardsBurned({hook: hook, round: round, token: token, amount: burnAmount, caller: msg.sender});
1202
+ // Surface the permissionless recycle for off-chain accounting.
1203
+ emit ExpiredRewardsRecycled({
1204
+ hook: hook,
1205
+ fromRound: round,
1206
+ toRound: recycledToRound,
1207
+ token: token,
1208
+ amount: recycleAmount,
1209
+ caller: msg.sender
1210
+ });
1187
1211
  }
1188
1212
 
1189
1213
  /// @notice Burn reward inventory using the JB controller.
@@ -1315,7 +1339,7 @@ abstract contract JBDistributor is IJBDistributor {
1315
1339
 
1316
1340
  /// @notice Whether a reward round has passed its claim deadline.
1317
1341
  /// @param rewardRound The reward round data.
1318
- /// @return expired True if unclaimed rewards can be burned.
1342
+ /// @return expired True if unclaimed rewards can be recycled.
1319
1343
  function _rewardRoundExpired(JBRewardRoundData storage rewardRound) internal view returns (bool expired) {
1320
1344
  // Copy the packed deadline into memory so the zero check and timestamp compare use the same value.
1321
1345
  uint48 claimDeadline = rewardRound.claimDeadline;
@@ -69,7 +69,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
69
69
  //*********************************************************************//
70
70
 
71
71
  /// @param directory The JB directory used to verify terminal/controller callers.
72
- /// @param controller The JB controller used to burn expired or forfeited project-token rewards.
72
+ /// @param controller The JB controller used to burn forfeited project-token rewards.
73
73
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
74
74
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
75
75
  /// @param initialRoundDuration The duration of each round, specified in seconds.
@@ -328,9 +328,9 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
328
328
 
329
329
  // Skip rounds that never received funding.
330
330
  if (rewardRound.amount != 0) {
331
- // Expired rounds can no longer be claimed; burn their unclaimed remainder instead.
331
+ // Expired rounds can no longer be claimed as-is; recycle their unclaimed remainder instead.
332
332
  if (_rewardRoundExpired(rewardRound)) {
333
- _burnExpiredRewardRound({hook: hook, token: token, round: rewardRoundNumber});
333
+ _recycleExpiredRewardRound({hook: hook, token: token, round: rewardRoundNumber});
334
334
  } else if (rewardRound.totalStake != 0) {
335
335
  // Use the funding round's snapshot block, not the block at which the staker finally claims.
336
336
  uint256 tokenStakeAmount =
@@ -344,7 +344,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
344
344
 
345
345
  // Ignore floor-rounded zero claims to avoid unnecessary storage writes.
346
346
  if (claimAmount != 0) {
347
- // Track the portion that has started vesting so expiry burns only the remainder.
347
+ // Track the portion that has started vesting so expiry recycles only the remainder.
348
348
  rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + claimAmount);
349
349
 
350
350
  // Add this round's vested amount to the staker's cumulative claim.
@@ -81,14 +81,20 @@ interface IJBDistributor {
81
81
  /// @param caller The address that triggered the snapshot recording.
82
82
  event RoundSnapshotRecorded(uint256 indexed round, uint256 snapshotBlock, address caller);
83
83
 
84
- /// @notice Emitted when an expired reward round's unclaimed amount is burned.
85
- /// @param hook The hook whose expired rewards were burned.
86
- /// @param round The expired reward round.
87
- /// @param token The reward token that was burned.
88
- /// @param amount The unclaimed reward amount burned.
89
- /// @param caller The address that triggered the burn.
90
- event ExpiredRewardsBurned(
91
- address indexed hook, uint256 indexed round, IERC20 indexed token, uint256 amount, address caller
84
+ /// @notice Emitted when an expired reward round's unclaimed amount is recycled into a later reward round.
85
+ /// @param hook The hook whose expired rewards were recycled.
86
+ /// @param fromRound The expired reward round.
87
+ /// @param toRound The reward round receiving the recycled rewards.
88
+ /// @param token The reward token that was recycled.
89
+ /// @param amount The unclaimed reward amount recycled.
90
+ /// @param caller The address that triggered the recycle.
91
+ event ExpiredRewardsRecycled(
92
+ address indexed hook,
93
+ uint256 indexed fromRound,
94
+ uint256 indexed toRound,
95
+ IERC20 token,
96
+ uint256 amount,
97
+ address caller
92
98
  );
93
99
 
94
100
  /// @notice Emitted when a liquidated distributor-held Revnet loan is written off.
@@ -147,7 +153,7 @@ interface IJBDistributor {
147
153
  /// @dev A zero duration means reward rounds do not expire.
148
154
  function CLAIM_DURATION() external view returns (uint48);
149
155
 
150
- /// @notice The JB controller used to burn expired or forfeited project-token rewards.
156
+ /// @notice The JB controller used to burn forfeited project-token rewards.
151
157
  function CONTROLLER() external view returns (IJBController);
152
158
 
153
159
  /// @notice The duration of each round, specified in seconds.
@@ -279,11 +285,11 @@ interface IJBDistributor {
279
285
  /// @param amount The amount to fund.
280
286
  function fund(address hook, IERC20 token, uint256 amount) external payable;
281
287
 
282
- /// @notice Burn unclaimed rewards from expired reward rounds.
283
- /// @param hook The hook whose expired reward rounds should be burned.
284
- /// @param token The reward token to burn.
285
- /// @param rounds The reward rounds to burn.
286
- /// @return amount The total amount burned.
288
+ /// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
289
+ /// @param hook The hook whose expired reward rounds should be recycled.
290
+ /// @param token The reward token to recycle.
291
+ /// @param rounds The reward rounds to recycle.
292
+ /// @return amount The total amount recycled.
287
293
  function burnExpiredRewards(address hook, IERC20 token, uint256[] calldata rounds) external returns (uint256 amount);
288
294
 
289
295
  /// @notice Record the snapshot block for the current round. Callable by anyone (keepers, frontends).