@bananapus/core-v6 0.0.9 → 0.0.10

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.
Files changed (39) hide show
  1. package/foundry.toml +0 -1
  2. package/package.json +2 -2
  3. package/src/JBChainlinkV3PriceFeed.sol +1 -5
  4. package/src/JBChainlinkV3SequencerPriceFeed.sol +1 -1
  5. package/src/JBController.sol +277 -277
  6. package/src/JBDeadline.sol +1 -1
  7. package/src/JBDirectory.sol +93 -93
  8. package/src/JBERC20.sol +43 -39
  9. package/src/JBFeelessAddresses.sol +12 -12
  10. package/src/JBFundAccessLimits.sol +82 -82
  11. package/src/JBMultiTerminal.sol +313 -313
  12. package/src/JBPermissions.sol +104 -100
  13. package/src/JBPrices.sol +68 -68
  14. package/src/JBProjects.sol +31 -31
  15. package/src/JBRulesets.sol +422 -422
  16. package/src/JBSplits.sol +116 -116
  17. package/src/JBTerminalStore.sol +651 -651
  18. package/src/JBTokens.sol +41 -41
  19. package/src/interfaces/IJBCashOutTerminal.sol +25 -7
  20. package/src/interfaces/IJBController.sol +78 -3
  21. package/src/interfaces/IJBDirectory.sol +25 -0
  22. package/src/interfaces/IJBFeeTerminal.sol +31 -0
  23. package/src/interfaces/IJBFeelessAddresses.sol +4 -0
  24. package/src/interfaces/IJBFundAccessLimits.sol +5 -0
  25. package/src/interfaces/IJBMigratable.sol +12 -8
  26. package/src/interfaces/IJBPayoutTerminal.sol +56 -9
  27. package/src/interfaces/IJBPermissions.sol +14 -7
  28. package/src/interfaces/IJBPermitTerminal.sol +4 -0
  29. package/src/interfaces/IJBPrices.sol +6 -0
  30. package/src/interfaces/IJBProjects.sol +8 -0
  31. package/src/interfaces/IJBRulesetApprovalHook.sol +1 -1
  32. package/src/interfaces/IJBRulesetDataHook.sol +23 -23
  33. package/src/interfaces/IJBRulesets.sol +54 -33
  34. package/src/interfaces/IJBSplits.sol +6 -0
  35. package/src/interfaces/IJBTerminal.sol +36 -0
  36. package/src/interfaces/IJBTerminalStore.sol +63 -63
  37. package/src/interfaces/IJBToken.sol +5 -5
  38. package/src/interfaces/IJBTokens.sol +50 -8
  39. package/test/TestDurationUnderflow.sol +3 -2
@@ -132,777 +132,777 @@ contract JBTerminalStore is IJBTerminalStore {
132
132
  }
133
133
 
134
134
  //*********************************************************************//
135
- // ------------------------- external views -------------------------- //
135
+ // ---------------------- external transactions ---------------------- //
136
136
  //*********************************************************************//
137
137
 
138
- /// @notice Returns the number of surplus terminal tokens that would be reclaimed by cashing out a given project's
139
- /// tokens based on its current ruleset and the given total project token supply and total terminal token surplus.
140
- /// @param projectId The ID of the project whose project tokens would be cashed out.
141
- /// @param cashOutCount The number of project tokens that would be cashed out, as a fixed point number with 18
138
+ /// @notice Records funds being added to a project's balance.
139
+ /// @param projectId The ID of the project which funds are being added to the balance of.
140
+ /// @param token The token being added to the balance.
141
+ /// @param amount The amount of terminal tokens added, as a fixed point number with the same amount of decimals as
142
+ /// its relative terminal.
143
+ function recordAddedBalanceFor(uint256 projectId, address token, uint256 amount) external override {
144
+ // Increment the balance.
145
+ balanceOf[msg.sender][projectId][token] = balanceOf[msg.sender][projectId][token] + amount;
146
+ }
147
+
148
+ /// @notice Records a cash out from a project.
149
+ /// @dev Cashs out the project's tokens according to values provided by the ruleset's data hook. If the ruleset has
150
+ /// no
151
+ /// data hook, cashs out tokens along a cash out bonding curve that is a function of the number of tokens being
152
+ /// burned.
153
+ /// @param holder The account that is cashing out tokens.
154
+ /// @param projectId The ID of the project being cashing out from.
155
+ /// @param cashOutCount The number of project tokens to cash out, as a fixed point number with 18 decimals.
156
+ /// @param accountingContext The accounting context of the token being reclaimed by the cash out.
157
+ /// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
158
+ /// surplus being reclaimed from.
159
+ /// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
160
+ /// @return ruleset The ruleset during the cash out was made during, as a `JBRuleset` struct. This ruleset will
161
+ /// have a cash out tax rate provided by the cash out hook if applicable.
162
+ /// @return reclaimAmount The amount of tokens reclaimed from the terminal, as a fixed point number with 18
142
163
  /// decimals.
143
- /// @param totalSupply The total project token supply, as a fixed point number with 18 decimals.
144
- /// @param surplus The total terminal token surplus amount, as a fixed point number.
145
- /// @return The number of surplus terminal tokens that would be reclaimed, as a fixed point number with the same
146
- /// number of decimals as the provided `surplus`.
147
- function currentReclaimableSurplusOf(
164
+ /// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
165
+ /// @return hookSpecifications A list of cash out hooks, including data and amounts to send to them. The terminal
166
+ /// should fulfill these specifications.
167
+ function recordCashOutFor(
168
+ address holder,
148
169
  uint256 projectId,
149
170
  uint256 cashOutCount,
150
- uint256 totalSupply,
151
- uint256 surplus
171
+ JBAccountingContext calldata accountingContext,
172
+ JBAccountingContext[] calldata balanceAccountingContexts,
173
+ bytes memory metadata
152
174
  )
153
175
  external
154
- view
155
176
  override
156
- returns (uint256)
177
+ returns (
178
+ JBRuleset memory ruleset,
179
+ uint256 reclaimAmount,
180
+ uint256 cashOutTaxRate,
181
+ JBCashOutHookSpecification[] memory hookSpecifications
182
+ )
157
183
  {
158
- // If there's no surplus, nothing can be reclaimed.
159
- if (surplus == 0) return 0;
184
+ // Get a reference to the project's current ruleset.
185
+ ruleset = RULESETS.currentOf(projectId);
160
186
 
161
- // Can't cash out more tokens than are in the total supply.
162
- if (cashOutCount > totalSupply) return 0;
187
+ // Get the current surplus amount.
188
+ // Use the local surplus if the ruleset specifies that it should be used. Otherwise, use the project's total
189
+ // surplus across all of its terminals.
190
+ uint256 currentSurplus = ruleset.useTotalSurplusForCashOuts()
191
+ ? JBSurplus.currentSurplusOf({
192
+ projectId: projectId,
193
+ terminals: DIRECTORY.terminalsOf(projectId),
194
+ accountingContexts: new JBAccountingContext[](0),
195
+ decimals: accountingContext.decimals,
196
+ currency: accountingContext.currency
197
+ })
198
+ : _surplusFrom({
199
+ terminal: msg.sender,
200
+ projectId: projectId,
201
+ accountingContexts: balanceAccountingContexts,
202
+ ruleset: ruleset,
203
+ targetDecimals: accountingContext.decimals,
204
+ targetCurrency: accountingContext.currency
205
+ });
163
206
 
164
- // Get a reference to the project's current ruleset.
165
- JBRuleset memory ruleset = RULESETS.currentOf(projectId);
207
+ // Get the total number of outstanding project tokens.
208
+ uint256 totalSupply =
209
+ IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
166
210
 
167
- // Return the amount of surplus terminal tokens that would be reclaimed.
168
- return JBCashOuts.cashOutFrom({
169
- surplus: surplus,
170
- cashOutCount: cashOutCount,
171
- totalSupply: totalSupply,
172
- cashOutTaxRate: ruleset.cashOutTaxRate()
173
- });
211
+ // Can't cash out more tokens than are in the supply.
212
+ if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
213
+
214
+ // SECURITY NOTE: The data hook has absolute control over cash-out economics.
215
+ // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
216
+ // completely overriding the terminal's bonding curve math. For example, setting
217
+ // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
218
+ // Project owners MUST audit their data hooks with the same rigor as the terminal.
219
+
220
+ // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
221
+ if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
222
+ // Create the cash out context that'll be sent to the data hook.
223
+ JBBeforeCashOutRecordedContext memory context = JBBeforeCashOutRecordedContext({
224
+ terminal: msg.sender,
225
+ holder: holder,
226
+ projectId: projectId,
227
+ rulesetId: ruleset.id,
228
+ cashOutCount: cashOutCount,
229
+ totalSupply: totalSupply,
230
+ surplus: JBTokenAmount({
231
+ token: accountingContext.token,
232
+ value: currentSurplus,
233
+ decimals: accountingContext.decimals,
234
+ currency: accountingContext.currency
235
+ }),
236
+ useTotalSurplus: ruleset.useTotalSurplusForCashOuts(),
237
+ cashOutTaxRate: ruleset.cashOutTaxRate(),
238
+ metadata: metadata
239
+ });
240
+
241
+ (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
242
+ IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
243
+ } else {
244
+ cashOutTaxRate = ruleset.cashOutTaxRate();
245
+ }
246
+
247
+ if (currentSurplus != 0) {
248
+ // Calculate reclaim amount using the current surplus amount.
249
+ reclaimAmount = JBCashOuts.cashOutFrom({
250
+ surplus: currentSurplus,
251
+ cashOutCount: cashOutCount,
252
+ totalSupply: totalSupply,
253
+ cashOutTaxRate: cashOutTaxRate
254
+ });
255
+ }
256
+
257
+ // Keep a reference to the amount that should be added to the project's balance.
258
+ uint256 balanceDiff = reclaimAmount;
259
+
260
+ // Ensure that the specifications have valid amounts.
261
+ if (hookSpecifications.length != 0) {
262
+ // Keep a reference to the number of cash out hooks specified.
263
+ uint256 numberOfSpecifications = hookSpecifications.length;
264
+
265
+ // Loop through each specification.
266
+ for (uint256 i; i < numberOfSpecifications; i++) {
267
+ // Get a reference to the specification's amount.
268
+ uint256 specificationAmount = hookSpecifications[i].amount;
269
+
270
+ // Ensure the amount is non-zero.
271
+ if (specificationAmount != 0) {
272
+ // Increment the total amount being subtracted from the balance.
273
+ balanceDiff += specificationAmount;
274
+ }
275
+ }
276
+ }
277
+
278
+ // The amount being reclaimed must be within the project's balance.
279
+ if (balanceDiff > balanceOf[msg.sender][projectId][accountingContext.token]) {
280
+ revert JBTerminalStore_InadequateTerminalStoreBalance(
281
+ balanceDiff, balanceOf[msg.sender][projectId][accountingContext.token]
282
+ );
283
+ }
284
+
285
+ // Remove the reclaimed funds from the project's balance.
286
+ if (balanceDiff != 0) {
287
+ unchecked {
288
+ balanceOf[msg.sender][projectId][accountingContext.token] =
289
+ balanceOf[msg.sender][projectId][accountingContext.token] - balanceDiff;
290
+ }
291
+ }
174
292
  }
175
293
 
176
- /// @notice Returns the number of surplus terminal tokens that would be reclaimed from a terminal by cashing out a
177
- /// given number of tokens, based on the total token supply and total surplus.
178
- /// @dev The returned amount in terms of the specified `terminal`'s base currency.
179
- /// @dev The returned amount is represented as a fixed point number with the same amount of decimals as the
180
- /// specified terminal.
181
- /// @param projectId The ID of the project whose tokens would be cashed out.
182
- /// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
183
- /// @param terminals The terminals that would be cashed out from. If this is an empty array, surplus within all
184
- /// the project's terminals are considered.
185
- /// @param accountingContexts The accounting contexts of the surplus terminal tokens that would be reclaimed. Pass
186
- /// an empty array to use all of the project's accounting contexts.
187
- /// @param decimals The number of decimals to include in the resulting fixed point number.
188
- /// @param currency The currency that the resulting number will be in terms of.
189
- /// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount`
190
- /// tokens.
191
- function currentReclaimableSurplusOf(
294
+ /// @notice Records a payment to a project.
295
+ /// @dev Mints the project's tokens according to values provided by the ruleset's data hook. If the ruleset has no
296
+ /// data hook, mints tokens in proportion with the amount paid.
297
+ /// @param payer The address that made the payment to the terminal.
298
+ /// @param amount The amount of tokens being paid. Includes the token being paid, their value, the number of
299
+ /// decimals included, and the currency of the amount.
300
+ /// @param projectId The ID of the project being paid.
301
+ /// @param beneficiary The address that should be the beneficiary of anything the payment yields (including project
302
+ /// tokens minted by the payment).
303
+ /// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
304
+ /// @return ruleset The ruleset the payment was made during, as a `JBRuleset` struct.
305
+ /// @return tokenCount The number of project tokens that were minted, as a fixed point number with 18 decimals.
306
+ /// @return hookSpecifications A list of pay hooks, including data and amounts to send to them. The terminal should
307
+ /// fulfill these specifications.
308
+ function recordPaymentFrom(
309
+ address payer,
310
+ JBTokenAmount calldata amount,
192
311
  uint256 projectId,
193
- uint256 cashOutCount,
194
- IJBTerminal[] calldata terminals,
195
- JBAccountingContext[] calldata accountingContexts,
196
- uint256 decimals,
197
- uint256 currency
312
+ address beneficiary,
313
+ bytes calldata metadata
198
314
  )
199
315
  external
200
- view
201
316
  override
202
- returns (uint256)
317
+ returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
203
318
  {
204
319
  // Get a reference to the project's current ruleset.
205
- JBRuleset memory ruleset = RULESETS.currentOf(projectId);
206
-
207
- // Get the current surplus amount.
208
- // If a terminal wasn't provided, use the total surplus across all terminals. Otherwise,
209
- // get the `terminal`'s surplus.
210
- uint256 currentSurplus = JBSurplus.currentSurplusOf({
211
- projectId: projectId,
212
- terminals: terminals.length != 0 ? terminals : DIRECTORY.terminalsOf(projectId),
213
- accountingContexts: accountingContexts,
214
- decimals: decimals,
215
- currency: currency
216
- });
320
+ ruleset = RULESETS.currentOf(projectId);
217
321
 
218
- // If there's no surplus, nothing can be reclaimed.
219
- if (currentSurplus == 0) return 0;
322
+ // The project must have a ruleset.
323
+ if (ruleset.cycleNumber == 0) revert JBTerminalStore_RulesetNotFound(projectId);
220
324
 
221
- // Get the project token's total supply.
222
- uint256 totalSupply =
223
- IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
325
+ // The ruleset must not have payments paused.
326
+ if (ruleset.pausePay()) revert JBTerminalStore_RulesetPaymentPaused();
224
327
 
225
- // Can't cash out more tokens than are in the total supply.
226
- if (cashOutCount > totalSupply) return 0;
328
+ // The weight according to which new tokens are to be minted, as a fixed point number with 18 decimals.
329
+ uint256 weight;
227
330
 
228
- // Return the amount of surplus terminal tokens that would be reclaimed.
229
- return JBCashOuts.cashOutFrom({
230
- surplus: currentSurplus,
231
- cashOutCount: cashOutCount,
232
- totalSupply: totalSupply,
233
- cashOutTaxRate: ruleset.cashOutTaxRate()
234
- });
235
- }
331
+ // SECURITY NOTE: The data hook has absolute control over payment token minting.
332
+ // It can return an arbitrary weight (overriding the ruleset's weight) and hook specifications
333
+ // that divert payment funds to external hooks before they reach the project's balance.
334
+ // Project owners MUST audit their data hooks with the same rigor as the terminal.
236
335
 
237
- /// @notice Gets the current surplus amount in a terminal for a specified project.
238
- /// @dev The surplus is the amount of funds a project has in a terminal in excess of its payout limit.
239
- /// @dev The surplus is represented as a fixed point number with the same amount of decimals as the specified
240
- /// terminal.
241
- /// @param terminal The terminal the surplus is being calculated for.
242
- /// @param projectId The ID of the project to get surplus for.
243
- /// @param accountingContexts The accounting contexts of tokens whose balances should contribute to the surplus
244
- /// being calculated.
245
- /// @param currency The currency the resulting amount should be in terms of.
246
- /// @param decimals The number of decimals to expect in the resulting fixed point number.
247
- /// @return The current surplus amount the project has in the specified terminal.
248
- function currentSurplusOf(
249
- address terminal,
250
- uint256 projectId,
251
- JBAccountingContext[] calldata accountingContexts,
252
- uint256 decimals,
253
- uint256 currency
254
- )
255
- external
256
- view
257
- override
258
- returns (uint256)
259
- {
260
- // Return the surplus during the project's current ruleset.
261
- return _surplusFrom({
262
- terminal: terminal,
263
- projectId: projectId,
264
- accountingContexts: accountingContexts,
265
- ruleset: RULESETS.currentOf(projectId),
266
- targetDecimals: decimals,
267
- targetCurrency: currency
268
- });
269
- }
336
+ // If the ruleset has a data hook enabled for payments, use it to derive a weight and memo.
337
+ if (ruleset.useDataHookForPay() && ruleset.dataHook() != address(0)) {
338
+ // Create the pay context that'll be sent to the data hook.
339
+ JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
340
+ terminal: msg.sender,
341
+ payer: payer,
342
+ amount: amount,
343
+ projectId: projectId,
344
+ rulesetId: ruleset.id,
345
+ beneficiary: beneficiary,
346
+ weight: ruleset.weight,
347
+ reservedPercent: ruleset.reservedPercent(),
348
+ metadata: metadata
349
+ });
270
350
 
271
- /// @notice Gets the current surplus amount for a specified project across all terminals.
272
- /// @param projectId The ID of the project to get the total surplus for.
273
- /// @param decimals The number of decimals that the fixed point surplus should include.
274
- /// @param currency The currency that the total surplus should be in terms of.
275
- /// @return The current total surplus amount that the project has across all terminals.
276
- function currentTotalSurplusOf(
277
- uint256 projectId,
278
- uint256 decimals,
279
- uint256 currency
280
- )
281
- external
282
- view
283
- override
284
- returns (uint256)
285
- {
286
- return JBSurplus.currentSurplusOf({
287
- projectId: projectId,
288
- terminals: DIRECTORY.terminalsOf(projectId),
289
- accountingContexts: new JBAccountingContext[](0),
290
- decimals: decimals,
291
- currency: currency
292
- });
293
- }
351
+ (weight, hookSpecifications) = IJBRulesetDataHook(ruleset.dataHook()).beforePayRecordedWith(context);
352
+ }
353
+ // Otherwise use the ruleset's weight
354
+ else {
355
+ weight = ruleset.weight;
356
+ }
294
357
 
295
- //*********************************************************************//
296
- // -------------------------- internal views ------------------------- //
297
- //*********************************************************************//
358
+ // Keep a reference to the amount that should be added to the project's balance.
359
+ uint256 balanceDiff = amount.value;
298
360
 
299
- /// @notice Gets a project's surplus amount in a terminal as measured by a given ruleset, across multiple accounting
300
- /// contexts.
301
- /// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure
302
- /// various payout limits.
303
- /// @param terminal The terminal the surplus is being calculated for.
304
- /// @param projectId The ID of the project to get the surplus for.
305
- /// @param accountingContexts The accounting contexts of tokens whose balances should contribute to the surplus
306
- /// being calculated.
307
- /// @param ruleset The ID of the ruleset to base the surplus on.
308
- /// @param targetDecimals The number of decimals to include in the resulting fixed point number.
309
- /// @param targetCurrency The currency that the reported surplus is expected to be in terms of.
310
- /// @return surplus The surplus of funds in terms of `targetCurrency`, as a fixed point number with
311
- /// `targetDecimals` decimals.
312
- function _surplusFrom(
313
- address terminal,
314
- uint256 projectId,
315
- JBAccountingContext[] memory accountingContexts,
316
- JBRuleset memory ruleset,
317
- uint256 targetDecimals,
318
- uint256 targetCurrency
319
- )
320
- internal
321
- view
322
- returns (uint256 surplus)
323
- {
324
- // Keep a reference to the number of tokens being iterated on.
325
- uint256 numberOfTokenAccountingContexts = accountingContexts.length;
361
+ // Scoped section preventing stack too deep.
362
+ {
363
+ // Keep a reference to the number of hook specifications.
364
+ uint256 numberOfSpecifications = hookSpecifications.length;
326
365
 
327
- // Add payout limits from each token.
328
- for (uint256 i; i < numberOfTokenAccountingContexts; i++) {
329
- uint256 tokenSurplus = _tokenSurplusFrom({
330
- terminal: terminal,
366
+ // Ensure that the specifications have valid amounts.
367
+ for (uint256 i; i < numberOfSpecifications; i++) {
368
+ // Get a reference to the specification's amount.
369
+ uint256 specifiedAmount = hookSpecifications[i].amount;
370
+
371
+ // Ensure the amount is non-zero.
372
+ if (specifiedAmount != 0) {
373
+ // Can't send more to hook than was paid.
374
+ if (specifiedAmount > balanceDiff) {
375
+ revert JBTerminalStore_InvalidAmountToForwardHook(specifiedAmount, balanceDiff);
376
+ }
377
+
378
+ // Decrement the total amount being added to the local balance.
379
+ balanceDiff -= specifiedAmount;
380
+ }
381
+ }
382
+ }
383
+
384
+ // If there's no amount being recorded, there's nothing left to do.
385
+ if (amount.value == 0) return (ruleset, 0, hookSpecifications);
386
+
387
+ // Add the correct balance difference to the token balance of the project.
388
+ if (balanceDiff != 0) {
389
+ balanceOf[msg.sender][projectId][amount.token] =
390
+ balanceOf[msg.sender][projectId][amount.token] + balanceDiff;
391
+ }
392
+
393
+ // If there's no weight, the token count must be 0, so there's nothing left to do.
394
+ if (weight == 0) return (ruleset, 0, hookSpecifications);
395
+
396
+ // If the terminal should base its weight on a currency other than the terminal's currency, determine the
397
+ // factor. The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the
398
+ // same
399
+ // number of decimals as the `amount`.
400
+ uint256 weightRatio = amount.currency == ruleset.baseCurrency()
401
+ ? 10 ** amount.decimals
402
+ : PRICES.pricePerUnitOf({
331
403
  projectId: projectId,
332
- accountingContext: accountingContexts[i],
333
- ruleset: ruleset,
334
- targetDecimals: targetDecimals,
335
- targetCurrency: targetCurrency
404
+ pricingCurrency: amount.currency,
405
+ unitCurrency: ruleset.baseCurrency(),
406
+ decimals: amount.decimals
336
407
  });
337
- // Increment the surplus with any remaining balance.
338
- if (tokenSurplus > 0) surplus += tokenSurplus;
339
- }
408
+
409
+ // Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
410
+ tokenCount = mulDiv(amount.value, weight, weightRatio);
340
411
  }
341
412
 
342
- /// @notice Get a project's surplus amount of a specific token in a given terminal as measured by a given ruleset
343
- /// (one specific accounting context).
344
- /// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure
345
- /// the payout limits.
346
- /// @param terminal The terminal the surplus is being calculated for.
347
- /// @param projectId The ID of the project to get the surplus of.
348
- /// @param accountingContext The accounting context of the token whose balance should contribute to the surplus
349
- /// being measured.
350
- /// @param ruleset The ID of the ruleset to base the surplus calculation on.
351
- /// @param targetDecimals The number of decimals to include in the resulting fixed point number.
352
- /// @param targetCurrency The currency that the reported surplus is expected to be in terms of.
353
- /// @return surplus The surplus of funds in terms of `targetCurrency`, as a fixed point number with
354
- /// `targetDecimals` decimals.
355
- function _tokenSurplusFrom(
356
- address terminal,
413
+ /// @notice Records a payout from a project.
414
+ /// @dev The balance is decremented and the used payout limit is incremented before the payout limit validation.
415
+ /// This is safe because the entire transaction reverts atomically if the validation fails, but callers should
416
+ /// be aware of this ordering.
417
+ /// @param projectId The ID of the project that is paying out funds.
418
+ /// @param accountingContext The context of the token being paid out.
419
+ /// @param amount The amount to pay out (use from the payout limit), as a fixed point number.
420
+ /// @param currency The currency of the `amount`. This must match the project's current ruleset's currency.
421
+ /// @return ruleset The ruleset the payout was made during, as a `JBRuleset` struct.
422
+ /// @return amountPaidOut The amount of terminal tokens paid out, as a fixed point number with the same amount of
423
+ /// decimals as its relative terminal.
424
+ function recordPayoutFor(
357
425
  uint256 projectId,
358
- JBAccountingContext memory accountingContext,
359
- JBRuleset memory ruleset,
360
- uint256 targetDecimals,
361
- uint256 targetCurrency
426
+ JBAccountingContext calldata accountingContext,
427
+ uint256 amount,
428
+ uint256 currency
362
429
  )
363
- internal
364
- view
365
- returns (uint256 surplus)
430
+ external
431
+ override
432
+ returns (JBRuleset memory ruleset, uint256 amountPaidOut)
366
433
  {
367
- // Keep a reference to the balance.
368
- surplus = balanceOf[terminal][projectId][accountingContext.token];
369
-
370
- // If needed, adjust the decimals of the fixed point number to have the correct decimals.
371
- surplus = accountingContext.decimals == targetDecimals
372
- ? surplus
373
- : JBFixedPointNumber.adjustDecimals({
374
- value: surplus, decimals: accountingContext.decimals, targetDecimals: targetDecimals
375
- });
434
+ // Get a reference to the project's current ruleset.
435
+ ruleset = RULESETS.currentOf(projectId);
376
436
 
377
- // Add up all the balances.
378
- surplus = (surplus == 0 || accountingContext.currency == targetCurrency)
379
- ? surplus
437
+ // Convert the amount to the balance's currency.
438
+ amountPaidOut = (currency == accountingContext.currency)
439
+ ? amount
380
440
  : mulDiv(
381
- surplus,
382
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
383
- // `_payoutLimitRemaining`'s fidelity as possible when converting.
441
+ amount,
442
+ 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the `_amount`'s
443
+ // fidelity as possible when converting.
384
444
  PRICES.pricePerUnitOf({
385
445
  projectId: projectId,
386
- pricingCurrency: accountingContext.currency,
387
- unitCurrency: targetCurrency,
446
+ pricingCurrency: currency,
447
+ unitCurrency: accountingContext.currency,
388
448
  decimals: _MAX_FIXED_POINT_FIDELITY
389
449
  })
390
450
  );
391
451
 
392
- // Get a reference to the payout limit during the ruleset for the token.
393
- JBCurrencyAmount[] memory payoutLimits = IJBController(address(DIRECTORY.controllerOf(projectId)))
394
- .FUND_ACCESS_LIMITS()
395
- .payoutLimitsOf({
396
- projectId: projectId, rulesetId: ruleset.id, terminal: address(terminal), token: accountingContext.token
397
- });
452
+ // The amount being paid out must be available.
453
+ if (amountPaidOut > balanceOf[msg.sender][projectId][accountingContext.token]) {
454
+ revert JBTerminalStore_InadequateTerminalStoreBalance(
455
+ amountPaidOut, balanceOf[msg.sender][projectId][accountingContext.token]
456
+ );
457
+ }
398
458
 
399
- // Keep a reference to the number of payout limits being iterated on.
400
- uint256 numberOfPayoutLimits = payoutLimits.length;
459
+ // Removed the paid out funds from the project's token balance.
460
+ unchecked {
461
+ balanceOf[msg.sender][projectId][accountingContext.token] =
462
+ balanceOf[msg.sender][projectId][accountingContext.token] - amountPaidOut;
463
+ }
401
464
 
402
- // Loop through each payout limit to determine the cumulative normalized payout limit remaining.
403
- for (uint256 i; i < numberOfPayoutLimits; i++) {
404
- JBCurrencyAmount memory payoutLimit = payoutLimits[i];
465
+ // The new total amount which has been paid out during this ruleset.
466
+ uint256 newUsedPayoutLimitOf =
467
+ usedPayoutLimitOf[msg.sender][projectId][accountingContext.token][ruleset.cycleNumber][currency] + amount;
405
468
 
406
- // Set the payout limit value to the amount still available to pay out during the ruleset.
407
- {
408
- uint256 remaining = payoutLimit.amount
409
- - usedPayoutLimitOf[
410
- terminal
411
- ][projectId][accountingContext.token][ruleset.cycleNumber][payoutLimit.currency];
412
- if (remaining > type(uint224).max) revert JBTerminalStore_Uint224Overflow(remaining);
413
- payoutLimit.amount = uint224(remaining);
414
- }
415
-
416
- // Adjust the decimals of the fixed point number if needed to have the correct decimals.
417
- if (accountingContext.decimals != targetDecimals) {
418
- uint256 adjusted = JBFixedPointNumber.adjustDecimals({
419
- value: payoutLimit.amount, decimals: accountingContext.decimals, targetDecimals: targetDecimals
420
- });
421
- if (adjusted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(adjusted);
422
- payoutLimit.amount = uint224(adjusted);
423
- }
469
+ // Store the new amount.
470
+ usedPayoutLimitOf[msg.sender][projectId][accountingContext.token][ruleset.cycleNumber][currency] =
471
+ newUsedPayoutLimitOf;
424
472
 
425
- // Convert the `payoutLimit`'s amount to be in terms of the provided currency.
426
- if (payoutLimit.amount != 0 && payoutLimit.currency != targetCurrency) {
427
- uint256 converted = mulDiv(
428
- payoutLimit.amount,
429
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
430
- // `payoutLimitRemaining`'s fidelity as possible when converting.
431
- PRICES.pricePerUnitOf({
432
- projectId: projectId,
433
- pricingCurrency: payoutLimit.currency,
434
- unitCurrency: targetCurrency,
435
- decimals: _MAX_FIXED_POINT_FIDELITY
436
- })
437
- );
438
- if (converted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(converted);
439
- payoutLimit.amount = uint224(converted);
440
- }
473
+ // Amount must be within what is still available to pay out.
474
+ uint256 payoutLimit = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
475
+ .payoutLimitOf({
476
+ projectId: projectId,
477
+ rulesetId: ruleset.id,
478
+ terminal: msg.sender,
479
+ token: accountingContext.token,
480
+ currency: currency
481
+ });
441
482
 
442
- // Decrement from the balance until it reaches zero.
443
- if (surplus > payoutLimit.amount) {
444
- surplus -= payoutLimit.amount;
445
- } else {
446
- return 0;
447
- }
483
+ // Make sure the new used amount is within the payout limit.
484
+ if (newUsedPayoutLimitOf > payoutLimit || payoutLimit == 0) {
485
+ revert JBTerminalStore_InadequateControllerPayoutLimit(newUsedPayoutLimitOf, payoutLimit);
448
486
  }
449
487
  }
450
488
 
451
- //*********************************************************************//
452
- // ---------------------- external transactions ---------------------- //
453
- //*********************************************************************//
489
+ /// @notice Records the migration of funds from this store.
490
+ /// @param projectId The ID of the project being migrated.
491
+ /// @param token The token being migrated.
492
+ /// @return balance The project's current balance (which is being migrated), as a fixed point number with the same
493
+ /// amount of decimals as its relative terminal.
494
+ function recordTerminalMigration(uint256 projectId, address token) external override returns (uint256 balance) {
495
+ // Get a reference to the project's current ruleset.
496
+ JBRuleset memory ruleset = RULESETS.currentOf(projectId);
454
497
 
455
- /// @notice Records funds being added to a project's balance.
456
- /// @param projectId The ID of the project which funds are being added to the balance of.
457
- /// @param token The token being added to the balance.
458
- /// @param amount The amount of terminal tokens added, as a fixed point number with the same amount of decimals as
459
- /// its relative terminal.
460
- function recordAddedBalanceFor(uint256 projectId, address token, uint256 amount) external override {
461
- // Increment the balance.
462
- balanceOf[msg.sender][projectId][token] = balanceOf[msg.sender][projectId][token] + amount;
498
+ // Terminal migration must be allowed.
499
+ if (!ruleset.allowTerminalMigration()) {
500
+ revert JBTerminalStore_TerminalMigrationNotAllowed();
501
+ }
502
+
503
+ // Return the current balance, which is the amount being migrated.
504
+ balance = balanceOf[msg.sender][projectId][token];
505
+
506
+ // Set the balance to 0.
507
+ balanceOf[msg.sender][projectId][token] = 0;
463
508
  }
464
509
 
465
- /// @notice Records a cash out from a project.
466
- /// @dev Cashs out the project's tokens according to values provided by the ruleset's data hook. If the ruleset has
467
- /// no
468
- /// data hook, cashs out tokens along a cash out bonding curve that is a function of the number of tokens being
469
- /// burned.
470
- /// @param holder The account that is cashing out tokens.
471
- /// @param projectId The ID of the project being cashing out from.
472
- /// @param cashOutCount The number of project tokens to cash out, as a fixed point number with 18 decimals.
473
- /// @param accountingContext The accounting context of the token being reclaimed by the cash out.
474
- /// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
475
- /// surplus being reclaimed from.
476
- /// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
477
- /// @return ruleset The ruleset during the cash out was made during, as a `JBRuleset` struct. This ruleset will
478
- /// have a cash out tax rate provided by the cash out hook if applicable.
479
- /// @return reclaimAmount The amount of tokens reclaimed from the terminal, as a fixed point number with 18
480
- /// decimals.
481
- /// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
482
- /// @return hookSpecifications A list of cash out hooks, including data and amounts to send to them. The terminal
483
- /// should fulfill these specifications.
484
- function recordCashOutFor(
485
- address holder,
510
+ /// @notice Records a use of a project's surplus allowance.
511
+ /// @dev When surplus allowance is "used", it is taken out of the project's surplus within a terminal.
512
+ /// @param projectId The ID of the project to use the surplus allowance of.
513
+ /// @param accountingContext The accounting context of the token whose balances should contribute to the surplus
514
+ /// allowance being reclaimed from.
515
+ /// @param amount The amount to use from the surplus allowance, as a fixed point number.
516
+ /// @param currency The currency of the `amount`. Must match the currency of the surplus allowance.
517
+ /// @return ruleset The ruleset during the surplus allowance is being used during, as a `JBRuleset` struct.
518
+ /// @return usedAmount The amount of terminal tokens used, as a fixed point number with the same amount of decimals
519
+ /// as its relative terminal.
520
+ function recordUsedAllowanceOf(
486
521
  uint256 projectId,
487
- uint256 cashOutCount,
488
522
  JBAccountingContext calldata accountingContext,
489
- JBAccountingContext[] calldata balanceAccountingContexts,
490
- bytes memory metadata
523
+ uint256 amount,
524
+ uint256 currency
491
525
  )
492
526
  external
493
527
  override
494
- returns (
495
- JBRuleset memory ruleset,
496
- uint256 reclaimAmount,
497
- uint256 cashOutTaxRate,
498
- JBCashOutHookSpecification[] memory hookSpecifications
499
- )
528
+ returns (JBRuleset memory ruleset, uint256 usedAmount)
500
529
  {
501
530
  // Get a reference to the project's current ruleset.
502
531
  ruleset = RULESETS.currentOf(projectId);
503
532
 
504
- // Get the current surplus amount.
505
- // Use the local surplus if the ruleset specifies that it should be used. Otherwise, use the project's total
506
- // surplus across all of its terminals.
507
- uint256 currentSurplus = ruleset.useTotalSurplusForCashOuts()
508
- ? JBSurplus.currentSurplusOf({
509
- projectId: projectId,
510
- terminals: DIRECTORY.terminalsOf(projectId),
511
- accountingContexts: new JBAccountingContext[](0),
512
- decimals: accountingContext.decimals,
513
- currency: accountingContext.currency
514
- })
515
- : _surplusFrom({
516
- terminal: msg.sender,
517
- projectId: projectId,
518
- accountingContexts: balanceAccountingContexts,
519
- ruleset: ruleset,
520
- targetDecimals: accountingContext.decimals,
521
- targetCurrency: accountingContext.currency
522
- });
533
+ // Convert the amount to this store's terminal's token.
534
+ usedAmount = currency == accountingContext.currency
535
+ ? amount
536
+ : mulDiv(
537
+ amount,
538
+ 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the `amount`'s
539
+ // fidelity as possible when converting.
540
+ PRICES.pricePerUnitOf({
541
+ projectId: projectId,
542
+ pricingCurrency: currency,
543
+ unitCurrency: accountingContext.currency,
544
+ decimals: _MAX_FIXED_POINT_FIDELITY
545
+ })
546
+ );
523
547
 
524
- // Get the total number of outstanding project tokens.
525
- uint256 totalSupply =
526
- IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
548
+ // Set the token being used as the only one to look for surplus within.
549
+ JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
550
+ accountingContexts[0] = accountingContext;
527
551
 
528
- // Can't cash out more tokens than are in the supply.
529
- if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
552
+ uint256 surplus = _surplusFrom({
553
+ terminal: msg.sender,
554
+ projectId: projectId,
555
+ accountingContexts: accountingContexts,
556
+ ruleset: ruleset,
557
+ targetDecimals: accountingContext.decimals,
558
+ targetCurrency: accountingContext.currency
559
+ });
530
560
 
531
- // SECURITY NOTE: The data hook has absolute control over cash-out economics.
532
- // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
533
- // completely overriding the terminal's bonding curve math. For example, setting
534
- // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
535
- // Project owners MUST audit their data hooks with the same rigor as the terminal.
561
+ // The amount being used must be available in the surplus.
562
+ if (usedAmount > surplus) revert JBTerminalStore_InadequateTerminalStoreBalance(usedAmount, surplus);
536
563
 
537
- // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
538
- if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
539
- // Create the cash out context that'll be sent to the data hook.
540
- JBBeforeCashOutRecordedContext memory context = JBBeforeCashOutRecordedContext({
541
- terminal: msg.sender,
542
- holder: holder,
564
+ // Update the project's balance.
565
+ balanceOf[msg.sender][projectId][accountingContext.token] =
566
+ balanceOf[msg.sender][projectId][accountingContext.token] - usedAmount;
567
+
568
+ // Get a reference to the new used surplus allowance for this ruleset ID.
569
+ uint256 newUsedSurplusAllowanceOf =
570
+ usedSurplusAllowanceOf[msg.sender][projectId][accountingContext.token][ruleset.id][currency] + amount;
571
+
572
+ // Store the incremented value.
573
+ usedSurplusAllowanceOf[msg.sender][projectId][accountingContext.token][ruleset.id][currency] =
574
+ newUsedSurplusAllowanceOf;
575
+
576
+ // There must be sufficient surplus allowance available.
577
+ uint256 surplusAllowance = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
578
+ .surplusAllowanceOf({
543
579
  projectId: projectId,
544
580
  rulesetId: ruleset.id,
545
- cashOutCount: cashOutCount,
546
- totalSupply: totalSupply,
547
- surplus: JBTokenAmount({
548
- token: accountingContext.token,
549
- value: currentSurplus,
550
- decimals: accountingContext.decimals,
551
- currency: accountingContext.currency
552
- }),
553
- useTotalSurplus: ruleset.useTotalSurplusForCashOuts(),
554
- cashOutTaxRate: ruleset.cashOutTaxRate(),
555
- metadata: metadata
581
+ terminal: msg.sender,
582
+ token: accountingContext.token,
583
+ currency: currency
556
584
  });
557
585
 
558
- (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
559
- IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
560
- } else {
561
- cashOutTaxRate = ruleset.cashOutTaxRate();
562
- }
563
-
564
- if (currentSurplus != 0) {
565
- // Calculate reclaim amount using the current surplus amount.
566
- reclaimAmount = JBCashOuts.cashOutFrom({
567
- surplus: currentSurplus,
568
- cashOutCount: cashOutCount,
569
- totalSupply: totalSupply,
570
- cashOutTaxRate: cashOutTaxRate
571
- });
586
+ // Make sure the new used amount is within the allowance.
587
+ if (newUsedSurplusAllowanceOf > surplusAllowance || surplusAllowance == 0) {
588
+ revert JBTerminalStore_InadequateControllerAllowance(newUsedSurplusAllowanceOf, surplusAllowance);
572
589
  }
590
+ }
573
591
 
574
- // Keep a reference to the amount that should be added to the project's balance.
575
- uint256 balanceDiff = reclaimAmount;
576
-
577
- // Ensure that the specifications have valid amounts.
578
- if (hookSpecifications.length != 0) {
579
- // Keep a reference to the number of cash out hooks specified.
580
- uint256 numberOfSpecifications = hookSpecifications.length;
581
-
582
- // Loop through each specification.
583
- for (uint256 i; i < numberOfSpecifications; i++) {
584
- // Get a reference to the specification's amount.
585
- uint256 specificationAmount = hookSpecifications[i].amount;
592
+ //*********************************************************************//
593
+ // ------------------------- external views -------------------------- //
594
+ //*********************************************************************//
586
595
 
587
- // Ensure the amount is non-zero.
588
- if (specificationAmount != 0) {
589
- // Increment the total amount being subtracted from the balance.
590
- balanceDiff += specificationAmount;
591
- }
592
- }
593
- }
596
+ /// @notice Returns the number of surplus terminal tokens that would be reclaimed by cashing out a given project's
597
+ /// tokens based on its current ruleset and the given total project token supply and total terminal token surplus.
598
+ /// @param projectId The ID of the project whose project tokens would be cashed out.
599
+ /// @param cashOutCount The number of project tokens that would be cashed out, as a fixed point number with 18
600
+ /// decimals.
601
+ /// @param totalSupply The total project token supply, as a fixed point number with 18 decimals.
602
+ /// @param surplus The total terminal token surplus amount, as a fixed point number.
603
+ /// @return The number of surplus terminal tokens that would be reclaimed, as a fixed point number with the same
604
+ /// number of decimals as the provided `surplus`.
605
+ function currentReclaimableSurplusOf(
606
+ uint256 projectId,
607
+ uint256 cashOutCount,
608
+ uint256 totalSupply,
609
+ uint256 surplus
610
+ )
611
+ external
612
+ view
613
+ override
614
+ returns (uint256)
615
+ {
616
+ // If there's no surplus, nothing can be reclaimed.
617
+ if (surplus == 0) return 0;
594
618
 
595
- // The amount being reclaimed must be within the project's balance.
596
- if (balanceDiff > balanceOf[msg.sender][projectId][accountingContext.token]) {
597
- revert JBTerminalStore_InadequateTerminalStoreBalance(
598
- balanceDiff, balanceOf[msg.sender][projectId][accountingContext.token]
599
- );
600
- }
619
+ // Can't cash out more tokens than are in the total supply.
620
+ if (cashOutCount > totalSupply) return 0;
601
621
 
602
- // Remove the reclaimed funds from the project's balance.
603
- if (balanceDiff != 0) {
604
- unchecked {
605
- balanceOf[msg.sender][projectId][accountingContext.token] =
606
- balanceOf[msg.sender][projectId][accountingContext.token] - balanceDiff;
607
- }
608
- }
622
+ // Get a reference to the project's current ruleset.
623
+ JBRuleset memory ruleset = RULESETS.currentOf(projectId);
624
+
625
+ // Return the amount of surplus terminal tokens that would be reclaimed.
626
+ return JBCashOuts.cashOutFrom({
627
+ surplus: surplus,
628
+ cashOutCount: cashOutCount,
629
+ totalSupply: totalSupply,
630
+ cashOutTaxRate: ruleset.cashOutTaxRate()
631
+ });
609
632
  }
610
633
 
611
- /// @notice Records a payment to a project.
612
- /// @dev Mints the project's tokens according to values provided by the ruleset's data hook. If the ruleset has no
613
- /// data hook, mints tokens in proportion with the amount paid.
614
- /// @param payer The address that made the payment to the terminal.
615
- /// @param amount The amount of tokens being paid. Includes the token being paid, their value, the number of
616
- /// decimals included, and the currency of the amount.
617
- /// @param projectId The ID of the project being paid.
618
- /// @param beneficiary The address that should be the beneficiary of anything the payment yields (including project
619
- /// tokens minted by the payment).
620
- /// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
621
- /// @return ruleset The ruleset the payment was made during, as a `JBRuleset` struct.
622
- /// @return tokenCount The number of project tokens that were minted, as a fixed point number with 18 decimals.
623
- /// @return hookSpecifications A list of pay hooks, including data and amounts to send to them. The terminal should
624
- /// fulfill these specifications.
625
- function recordPaymentFrom(
626
- address payer,
627
- JBTokenAmount calldata amount,
634
+ /// @notice Returns the number of surplus terminal tokens that would be reclaimed from a terminal by cashing out a
635
+ /// given number of tokens, based on the total token supply and total surplus.
636
+ /// @dev The returned amount in terms of the specified `terminal`'s base currency.
637
+ /// @dev The returned amount is represented as a fixed point number with the same amount of decimals as the
638
+ /// specified terminal.
639
+ /// @param projectId The ID of the project whose tokens would be cashed out.
640
+ /// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
641
+ /// @param terminals The terminals that would be cashed out from. If this is an empty array, surplus within all
642
+ /// the project's terminals are considered.
643
+ /// @param accountingContexts The accounting contexts of the surplus terminal tokens that would be reclaimed. Pass
644
+ /// an empty array to use all of the project's accounting contexts.
645
+ /// @param decimals The number of decimals to include in the resulting fixed point number.
646
+ /// @param currency The currency that the resulting number will be in terms of.
647
+ /// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount`
648
+ /// tokens.
649
+ function currentReclaimableSurplusOf(
628
650
  uint256 projectId,
629
- address beneficiary,
630
- bytes calldata metadata
651
+ uint256 cashOutCount,
652
+ IJBTerminal[] calldata terminals,
653
+ JBAccountingContext[] calldata accountingContexts,
654
+ uint256 decimals,
655
+ uint256 currency
631
656
  )
632
657
  external
658
+ view
633
659
  override
634
- returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
660
+ returns (uint256)
635
661
  {
636
662
  // Get a reference to the project's current ruleset.
637
- ruleset = RULESETS.currentOf(projectId);
638
-
639
- // The project must have a ruleset.
640
- if (ruleset.cycleNumber == 0) revert JBTerminalStore_RulesetNotFound(projectId);
641
-
642
- // The ruleset must not have payments paused.
643
- if (ruleset.pausePay()) revert JBTerminalStore_RulesetPaymentPaused();
644
-
645
- // The weight according to which new tokens are to be minted, as a fixed point number with 18 decimals.
646
- uint256 weight;
647
-
648
- // SECURITY NOTE: The data hook has absolute control over payment token minting.
649
- // It can return an arbitrary weight (overriding the ruleset's weight) and hook specifications
650
- // that divert payment funds to external hooks before they reach the project's balance.
651
- // Project owners MUST audit their data hooks with the same rigor as the terminal.
652
-
653
- // If the ruleset has a data hook enabled for payments, use it to derive a weight and memo.
654
- if (ruleset.useDataHookForPay() && ruleset.dataHook() != address(0)) {
655
- // Create the pay context that'll be sent to the data hook.
656
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
657
- terminal: msg.sender,
658
- payer: payer,
659
- amount: amount,
660
- projectId: projectId,
661
- rulesetId: ruleset.id,
662
- beneficiary: beneficiary,
663
- weight: ruleset.weight,
664
- reservedPercent: ruleset.reservedPercent(),
665
- metadata: metadata
666
- });
667
-
668
- (weight, hookSpecifications) = IJBRulesetDataHook(ruleset.dataHook()).beforePayRecordedWith(context);
669
- }
670
- // Otherwise use the ruleset's weight
671
- else {
672
- weight = ruleset.weight;
673
- }
674
-
675
- // Keep a reference to the amount that should be added to the project's balance.
676
- uint256 balanceDiff = amount.value;
677
-
678
- // Scoped section preventing stack too deep.
679
- {
680
- // Keep a reference to the number of hook specifications.
681
- uint256 numberOfSpecifications = hookSpecifications.length;
682
-
683
- // Ensure that the specifications have valid amounts.
684
- for (uint256 i; i < numberOfSpecifications; i++) {
685
- // Get a reference to the specification's amount.
686
- uint256 specifiedAmount = hookSpecifications[i].amount;
663
+ JBRuleset memory ruleset = RULESETS.currentOf(projectId);
687
664
 
688
- // Ensure the amount is non-zero.
689
- if (specifiedAmount != 0) {
690
- // Can't send more to hook than was paid.
691
- if (specifiedAmount > balanceDiff) {
692
- revert JBTerminalStore_InvalidAmountToForwardHook(specifiedAmount, balanceDiff);
693
- }
665
+ // Get the current surplus amount.
666
+ // If a terminal wasn't provided, use the total surplus across all terminals. Otherwise,
667
+ // get the `terminal`'s surplus.
668
+ uint256 currentSurplus = JBSurplus.currentSurplusOf({
669
+ projectId: projectId,
670
+ terminals: terminals.length != 0 ? terminals : DIRECTORY.terminalsOf(projectId),
671
+ accountingContexts: accountingContexts,
672
+ decimals: decimals,
673
+ currency: currency
674
+ });
694
675
 
695
- // Decrement the total amount being added to the local balance.
696
- balanceDiff -= specifiedAmount;
697
- }
698
- }
699
- }
676
+ // If there's no surplus, nothing can be reclaimed.
677
+ if (currentSurplus == 0) return 0;
700
678
 
701
- // If there's no amount being recorded, there's nothing left to do.
702
- if (amount.value == 0) return (ruleset, 0, hookSpecifications);
679
+ // Get the project token's total supply.
680
+ uint256 totalSupply =
681
+ IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
703
682
 
704
- // Add the correct balance difference to the token balance of the project.
705
- if (balanceDiff != 0) {
706
- balanceOf[msg.sender][projectId][amount.token] =
707
- balanceOf[msg.sender][projectId][amount.token] + balanceDiff;
708
- }
683
+ // Can't cash out more tokens than are in the total supply.
684
+ if (cashOutCount > totalSupply) return 0;
709
685
 
710
- // If there's no weight, the token count must be 0, so there's nothing left to do.
711
- if (weight == 0) return (ruleset, 0, hookSpecifications);
686
+ // Return the amount of surplus terminal tokens that would be reclaimed.
687
+ return JBCashOuts.cashOutFrom({
688
+ surplus: currentSurplus,
689
+ cashOutCount: cashOutCount,
690
+ totalSupply: totalSupply,
691
+ cashOutTaxRate: ruleset.cashOutTaxRate()
692
+ });
693
+ }
712
694
 
713
- // If the terminal should base its weight on a currency other than the terminal's currency, determine the
714
- // factor. The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the
715
- // same
716
- // number of decimals as the `amount`.
717
- uint256 weightRatio = amount.currency == ruleset.baseCurrency()
718
- ? 10 ** amount.decimals
719
- : PRICES.pricePerUnitOf({
720
- projectId: projectId,
721
- pricingCurrency: amount.currency,
722
- unitCurrency: ruleset.baseCurrency(),
723
- decimals: amount.decimals
724
- });
695
+ /// @notice Gets the current surplus amount in a terminal for a specified project.
696
+ /// @dev The surplus is the amount of funds a project has in a terminal in excess of its payout limit.
697
+ /// @dev The surplus is represented as a fixed point number with the same amount of decimals as the specified
698
+ /// terminal.
699
+ /// @param terminal The terminal the surplus is being calculated for.
700
+ /// @param projectId The ID of the project to get surplus for.
701
+ /// @param accountingContexts The accounting contexts of tokens whose balances should contribute to the surplus
702
+ /// being calculated.
703
+ /// @param currency The currency the resulting amount should be in terms of.
704
+ /// @param decimals The number of decimals to expect in the resulting fixed point number.
705
+ /// @return The current surplus amount the project has in the specified terminal.
706
+ function currentSurplusOf(
707
+ address terminal,
708
+ uint256 projectId,
709
+ JBAccountingContext[] calldata accountingContexts,
710
+ uint256 decimals,
711
+ uint256 currency
712
+ )
713
+ external
714
+ view
715
+ override
716
+ returns (uint256)
717
+ {
718
+ // Return the surplus during the project's current ruleset.
719
+ return _surplusFrom({
720
+ terminal: terminal,
721
+ projectId: projectId,
722
+ accountingContexts: accountingContexts,
723
+ ruleset: RULESETS.currentOf(projectId),
724
+ targetDecimals: decimals,
725
+ targetCurrency: currency
726
+ });
727
+ }
725
728
 
726
- // Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
727
- tokenCount = mulDiv(amount.value, weight, weightRatio);
729
+ /// @notice Gets the current surplus amount for a specified project across all terminals.
730
+ /// @param projectId The ID of the project to get the total surplus for.
731
+ /// @param decimals The number of decimals that the fixed point surplus should include.
732
+ /// @param currency The currency that the total surplus should be in terms of.
733
+ /// @return The current total surplus amount that the project has across all terminals.
734
+ function currentTotalSurplusOf(
735
+ uint256 projectId,
736
+ uint256 decimals,
737
+ uint256 currency
738
+ )
739
+ external
740
+ view
741
+ override
742
+ returns (uint256)
743
+ {
744
+ return JBSurplus.currentSurplusOf({
745
+ projectId: projectId,
746
+ terminals: DIRECTORY.terminalsOf(projectId),
747
+ accountingContexts: new JBAccountingContext[](0),
748
+ decimals: decimals,
749
+ currency: currency
750
+ });
728
751
  }
729
752
 
730
- /// @notice Records a payout from a project.
731
- /// @dev The balance is decremented and the used payout limit is incremented before the payout limit validation.
732
- /// This is safe because the entire transaction reverts atomically if the validation fails, but callers should
733
- /// be aware of this ordering.
734
- /// @param projectId The ID of the project that is paying out funds.
735
- /// @param accountingContext The context of the token being paid out.
736
- /// @param amount The amount to pay out (use from the payout limit), as a fixed point number.
737
- /// @param currency The currency of the `amount`. This must match the project's current ruleset's currency.
738
- /// @return ruleset The ruleset the payout was made during, as a `JBRuleset` struct.
739
- /// @return amountPaidOut The amount of terminal tokens paid out, as a fixed point number with the same amount of
740
- /// decimals as its relative terminal.
741
- function recordPayoutFor(
753
+ //*********************************************************************//
754
+ // -------------------------- internal views ------------------------- //
755
+ //*********************************************************************//
756
+
757
+ /// @notice Gets a project's surplus amount in a terminal as measured by a given ruleset, across multiple accounting
758
+ /// contexts.
759
+ /// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure
760
+ /// various payout limits.
761
+ /// @param terminal The terminal the surplus is being calculated for.
762
+ /// @param projectId The ID of the project to get the surplus for.
763
+ /// @param accountingContexts The accounting contexts of tokens whose balances should contribute to the surplus
764
+ /// being calculated.
765
+ /// @param ruleset The ID of the ruleset to base the surplus on.
766
+ /// @param targetDecimals The number of decimals to include in the resulting fixed point number.
767
+ /// @param targetCurrency The currency that the reported surplus is expected to be in terms of.
768
+ /// @return surplus The surplus of funds in terms of `targetCurrency`, as a fixed point number with
769
+ /// `targetDecimals` decimals.
770
+ function _surplusFrom(
771
+ address terminal,
742
772
  uint256 projectId,
743
- JBAccountingContext calldata accountingContext,
744
- uint256 amount,
745
- uint256 currency
773
+ JBAccountingContext[] memory accountingContexts,
774
+ JBRuleset memory ruleset,
775
+ uint256 targetDecimals,
776
+ uint256 targetCurrency
746
777
  )
747
- external
748
- override
749
- returns (JBRuleset memory ruleset, uint256 amountPaidOut)
778
+ internal
779
+ view
780
+ returns (uint256 surplus)
750
781
  {
751
- // Get a reference to the project's current ruleset.
752
- ruleset = RULESETS.currentOf(projectId);
753
-
754
- // Convert the amount to the balance's currency.
755
- amountPaidOut = (currency == accountingContext.currency)
756
- ? amount
757
- : mulDiv(
758
- amount,
759
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the `_amount`'s
760
- // fidelity as possible when converting.
761
- PRICES.pricePerUnitOf({
762
- projectId: projectId,
763
- pricingCurrency: currency,
764
- unitCurrency: accountingContext.currency,
765
- decimals: _MAX_FIXED_POINT_FIDELITY
766
- })
767
- );
768
-
769
- // The amount being paid out must be available.
770
- if (amountPaidOut > balanceOf[msg.sender][projectId][accountingContext.token]) {
771
- revert JBTerminalStore_InadequateTerminalStoreBalance(
772
- amountPaidOut, balanceOf[msg.sender][projectId][accountingContext.token]
773
- );
774
- }
775
-
776
- // Removed the paid out funds from the project's token balance.
777
- unchecked {
778
- balanceOf[msg.sender][projectId][accountingContext.token] =
779
- balanceOf[msg.sender][projectId][accountingContext.token] - amountPaidOut;
780
- }
781
-
782
- // The new total amount which has been paid out during this ruleset.
783
- uint256 newUsedPayoutLimitOf =
784
- usedPayoutLimitOf[msg.sender][projectId][accountingContext.token][ruleset.cycleNumber][currency] + amount;
785
-
786
- // Store the new amount.
787
- usedPayoutLimitOf[msg.sender][projectId][accountingContext.token][ruleset.cycleNumber][currency] =
788
- newUsedPayoutLimitOf;
782
+ // Keep a reference to the number of tokens being iterated on.
783
+ uint256 numberOfTokenAccountingContexts = accountingContexts.length;
789
784
 
790
- // Amount must be within what is still available to pay out.
791
- uint256 payoutLimit = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
792
- .payoutLimitOf({
785
+ // Add payout limits from each token.
786
+ for (uint256 i; i < numberOfTokenAccountingContexts; i++) {
787
+ uint256 tokenSurplus = _tokenSurplusFrom({
788
+ terminal: terminal,
793
789
  projectId: projectId,
794
- rulesetId: ruleset.id,
795
- terminal: msg.sender,
796
- token: accountingContext.token,
797
- currency: currency
790
+ accountingContext: accountingContexts[i],
791
+ ruleset: ruleset,
792
+ targetDecimals: targetDecimals,
793
+ targetCurrency: targetCurrency
798
794
  });
799
-
800
- // Make sure the new used amount is within the payout limit.
801
- if (newUsedPayoutLimitOf > payoutLimit || payoutLimit == 0) {
802
- revert JBTerminalStore_InadequateControllerPayoutLimit(newUsedPayoutLimitOf, payoutLimit);
803
- }
804
- }
805
-
806
- /// @notice Records the migration of funds from this store.
807
- /// @param projectId The ID of the project being migrated.
808
- /// @param token The token being migrated.
809
- /// @return balance The project's current balance (which is being migrated), as a fixed point number with the same
810
- /// amount of decimals as its relative terminal.
811
- function recordTerminalMigration(uint256 projectId, address token) external override returns (uint256 balance) {
812
- // Get a reference to the project's current ruleset.
813
- JBRuleset memory ruleset = RULESETS.currentOf(projectId);
814
-
815
- // Terminal migration must be allowed.
816
- if (!ruleset.allowTerminalMigration()) {
817
- revert JBTerminalStore_TerminalMigrationNotAllowed();
795
+ // Increment the surplus with any remaining balance.
796
+ if (tokenSurplus > 0) surplus += tokenSurplus;
818
797
  }
819
-
820
- // Return the current balance, which is the amount being migrated.
821
- balance = balanceOf[msg.sender][projectId][token];
822
-
823
- // Set the balance to 0.
824
- balanceOf[msg.sender][projectId][token] = 0;
825
798
  }
826
799
 
827
- /// @notice Records a use of a project's surplus allowance.
828
- /// @dev When surplus allowance is "used", it is taken out of the project's surplus within a terminal.
829
- /// @param projectId The ID of the project to use the surplus allowance of.
830
- /// @param accountingContext The accounting context of the token whose balances should contribute to the surplus
831
- /// allowance being reclaimed from.
832
- /// @param amount The amount to use from the surplus allowance, as a fixed point number.
833
- /// @param currency The currency of the `amount`. Must match the currency of the surplus allowance.
834
- /// @return ruleset The ruleset during the surplus allowance is being used during, as a `JBRuleset` struct.
835
- /// @return usedAmount The amount of terminal tokens used, as a fixed point number with the same amount of decimals
836
- /// as its relative terminal.
837
- function recordUsedAllowanceOf(
800
+ /// @notice Get a project's surplus amount of a specific token in a given terminal as measured by a given ruleset
801
+ /// (one specific accounting context).
802
+ /// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure
803
+ /// the payout limits.
804
+ /// @param terminal The terminal the surplus is being calculated for.
805
+ /// @param projectId The ID of the project to get the surplus of.
806
+ /// @param accountingContext The accounting context of the token whose balance should contribute to the surplus
807
+ /// being measured.
808
+ /// @param ruleset The ID of the ruleset to base the surplus calculation on.
809
+ /// @param targetDecimals The number of decimals to include in the resulting fixed point number.
810
+ /// @param targetCurrency The currency that the reported surplus is expected to be in terms of.
811
+ /// @return surplus The surplus of funds in terms of `targetCurrency`, as a fixed point number with
812
+ /// `targetDecimals` decimals.
813
+ function _tokenSurplusFrom(
814
+ address terminal,
838
815
  uint256 projectId,
839
- JBAccountingContext calldata accountingContext,
840
- uint256 amount,
841
- uint256 currency
816
+ JBAccountingContext memory accountingContext,
817
+ JBRuleset memory ruleset,
818
+ uint256 targetDecimals,
819
+ uint256 targetCurrency
842
820
  )
843
- external
844
- override
845
- returns (JBRuleset memory ruleset, uint256 usedAmount)
821
+ internal
822
+ view
823
+ returns (uint256 surplus)
846
824
  {
847
- // Get a reference to the project's current ruleset.
848
- ruleset = RULESETS.currentOf(projectId);
825
+ // Keep a reference to the balance.
826
+ surplus = balanceOf[terminal][projectId][accountingContext.token];
849
827
 
850
- // Convert the amount to this store's terminal's token.
851
- usedAmount = currency == accountingContext.currency
852
- ? amount
828
+ // If needed, adjust the decimals of the fixed point number to have the correct decimals.
829
+ surplus = accountingContext.decimals == targetDecimals
830
+ ? surplus
831
+ : JBFixedPointNumber.adjustDecimals({
832
+ value: surplus, decimals: accountingContext.decimals, targetDecimals: targetDecimals
833
+ });
834
+
835
+ // Add up all the balances.
836
+ surplus = (surplus == 0 || accountingContext.currency == targetCurrency)
837
+ ? surplus
853
838
  : mulDiv(
854
- amount,
855
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the `amount`'s
856
- // fidelity as possible when converting.
839
+ surplus,
840
+ 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
841
+ // `_payoutLimitRemaining`'s fidelity as possible when converting.
857
842
  PRICES.pricePerUnitOf({
858
843
  projectId: projectId,
859
- pricingCurrency: currency,
860
- unitCurrency: accountingContext.currency,
844
+ pricingCurrency: accountingContext.currency,
845
+ unitCurrency: targetCurrency,
861
846
  decimals: _MAX_FIXED_POINT_FIDELITY
862
847
  })
863
848
  );
864
849
 
865
- // Set the token being used as the only one to look for surplus within.
866
- JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
867
- accountingContexts[0] = accountingContext;
868
-
869
- uint256 surplus = _surplusFrom({
870
- terminal: msg.sender,
871
- projectId: projectId,
872
- accountingContexts: accountingContexts,
873
- ruleset: ruleset,
874
- targetDecimals: accountingContext.decimals,
875
- targetCurrency: accountingContext.currency
876
- });
850
+ // Get a reference to the payout limit during the ruleset for the token.
851
+ JBCurrencyAmount[] memory payoutLimits = IJBController(address(DIRECTORY.controllerOf(projectId)))
852
+ .FUND_ACCESS_LIMITS()
853
+ .payoutLimitsOf({
854
+ projectId: projectId, rulesetId: ruleset.id, terminal: address(terminal), token: accountingContext.token
855
+ });
877
856
 
878
- // The amount being used must be available in the surplus.
879
- if (usedAmount > surplus) revert JBTerminalStore_InadequateTerminalStoreBalance(usedAmount, surplus);
857
+ // Keep a reference to the number of payout limits being iterated on.
858
+ uint256 numberOfPayoutLimits = payoutLimits.length;
880
859
 
881
- // Update the project's balance.
882
- balanceOf[msg.sender][projectId][accountingContext.token] =
883
- balanceOf[msg.sender][projectId][accountingContext.token] - usedAmount;
860
+ // Loop through each payout limit to determine the cumulative normalized payout limit remaining.
861
+ for (uint256 i; i < numberOfPayoutLimits; i++) {
862
+ JBCurrencyAmount memory payoutLimit = payoutLimits[i];
884
863
 
885
- // Get a reference to the new used surplus allowance for this ruleset ID.
886
- uint256 newUsedSurplusAllowanceOf =
887
- usedSurplusAllowanceOf[msg.sender][projectId][accountingContext.token][ruleset.id][currency] + amount;
864
+ // Set the payout limit value to the amount still available to pay out during the ruleset.
865
+ {
866
+ uint256 remaining = payoutLimit.amount
867
+ - usedPayoutLimitOf[
868
+ terminal
869
+ ][projectId][accountingContext.token][ruleset.cycleNumber][payoutLimit.currency];
870
+ if (remaining > type(uint224).max) revert JBTerminalStore_Uint224Overflow(remaining);
871
+ payoutLimit.amount = uint224(remaining);
872
+ }
888
873
 
889
- // Store the incremented value.
890
- usedSurplusAllowanceOf[msg.sender][projectId][accountingContext.token][ruleset.id][currency] =
891
- newUsedSurplusAllowanceOf;
874
+ // Adjust the decimals of the fixed point number if needed to have the correct decimals.
875
+ if (accountingContext.decimals != targetDecimals) {
876
+ uint256 adjusted = JBFixedPointNumber.adjustDecimals({
877
+ value: payoutLimit.amount, decimals: accountingContext.decimals, targetDecimals: targetDecimals
878
+ });
879
+ if (adjusted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(adjusted);
880
+ payoutLimit.amount = uint224(adjusted);
881
+ }
892
882
 
893
- // There must be sufficient surplus allowance available.
894
- uint256 surplusAllowance = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
895
- .surplusAllowanceOf({
896
- projectId: projectId,
897
- rulesetId: ruleset.id,
898
- terminal: msg.sender,
899
- token: accountingContext.token,
900
- currency: currency
901
- });
883
+ // Convert the `payoutLimit`'s amount to be in terms of the provided currency.
884
+ if (payoutLimit.amount != 0 && payoutLimit.currency != targetCurrency) {
885
+ uint256 converted = mulDiv(
886
+ payoutLimit.amount,
887
+ 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
888
+ // `payoutLimitRemaining`'s fidelity as possible when converting.
889
+ PRICES.pricePerUnitOf({
890
+ projectId: projectId,
891
+ pricingCurrency: payoutLimit.currency,
892
+ unitCurrency: targetCurrency,
893
+ decimals: _MAX_FIXED_POINT_FIDELITY
894
+ })
895
+ );
896
+ if (converted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(converted);
897
+ payoutLimit.amount = uint224(converted);
898
+ }
902
899
 
903
- // Make sure the new used amount is within the allowance.
904
- if (newUsedSurplusAllowanceOf > surplusAllowance || surplusAllowance == 0) {
905
- revert JBTerminalStore_InadequateControllerAllowance(newUsedSurplusAllowanceOf, surplusAllowance);
900
+ // Decrement from the balance until it reaches zero.
901
+ if (surplus > payoutLimit.amount) {
902
+ surplus -= payoutLimit.amount;
903
+ } else {
904
+ return 0;
905
+ }
906
906
  }
907
907
  }
908
908
  }