@bananapus/core-v6 0.0.18 → 0.0.20
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/ADMINISTRATION.md +3 -0
- package/ARCHITECTURE.md +24 -0
- package/AUDIT_INSTRUCTIONS.md +4 -2
- package/CHANGE_LOG.md +29 -1
- package/README.md +12 -2
- package/RISKS.md +10 -2
- package/SKILLS.md +9 -0
- package/USER_JOURNEYS.md +6 -0
- package/foundry.toml +1 -0
- package/package.json +1 -1
- package/src/JBController.sol +52 -5
- package/src/JBMultiTerminal.sol +197 -179
- package/src/JBTerminalStore.sol +367 -171
- package/src/interfaces/IJBCashOutTerminal.sol +30 -0
- package/src/interfaces/IJBController.sol +15 -0
- package/src/interfaces/IJBTerminal.sol +28 -0
- package/src/interfaces/IJBTerminalStore.sol +66 -0
- package/src/libraries/JBPayoutSplitGroupLib.sol +157 -0
- package/src/structs/JBCashOutHookSpecification.sol +2 -0
- package/src/structs/JBPayHookSpecification.sol +2 -0
- package/test/CoreExploitTests.t.sol +21 -10
- package/test/TestCashOutHooks.sol +6 -4
- package/test/TestDataHookFuzzing.sol +6 -2
- package/test/TestPayHooks.sol +1 -1
- package/test/TestRulesetQueueing.sol +4 -5
- package/test/TestRulesetQueuingStress.sol +5 -3
- package/test/TestTerminalPreviewParity.sol +208 -0
- package/test/fork/TestSequencerPriceFeedFork.sol +168 -0
- package/test/fork/TestTerminalPreviewParityFork.sol +109 -0
- package/test/units/static/JBController/TestPreviewMintOf.sol +116 -0
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +144 -25
- package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +11 -1
- package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +15 -2
- package/test/units/static/JBMultiTerminal/TestPay.sol +64 -2
- package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +116 -0
- package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +98 -0
- package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +11 -2
- package/test/units/static/JBRulesets/TestCurrentOf.sol +8 -6
- package/test/units/static/JBRulesets/TestRulesets.sol +25 -24
- package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +4 -17
- package/test/units/static/JBSurplus/TestSurplusFuzz.sol +49 -2
- package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +215 -0
- package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +475 -0
- package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +464 -0
- package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +113 -2
- package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +227 -5
package/src/JBTerminalStore.sol
CHANGED
|
@@ -10,6 +10,7 @@ import {IJBRulesetDataHook} from "./interfaces/IJBRulesetDataHook.sol";
|
|
|
10
10
|
import {IJBRulesets} from "./interfaces/IJBRulesets.sol";
|
|
11
11
|
import {IJBTerminal} from "./interfaces/IJBTerminal.sol";
|
|
12
12
|
import {IJBTerminalStore} from "./interfaces/IJBTerminalStore.sol";
|
|
13
|
+
import {JBConstants} from "./libraries/JBConstants.sol";
|
|
13
14
|
import {JBFixedPointNumber} from "./libraries/JBFixedPointNumber.sol";
|
|
14
15
|
import {JBCashOuts} from "./libraries/JBCashOuts.sol";
|
|
15
16
|
import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
|
|
@@ -38,6 +39,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
38
39
|
error JBTerminalStore_InadequateTerminalStoreBalance(uint256 amount, uint256 balance);
|
|
39
40
|
error JBTerminalStore_InsufficientTokens(uint256 count, uint256 totalSupply);
|
|
40
41
|
error JBTerminalStore_InvalidAmountToForwardHook(uint256 amount, uint256 paidAmount);
|
|
42
|
+
error JBTerminalStore_NoopHookSpecHasAmount(uint256 amount);
|
|
41
43
|
error JBTerminalStore_RulesetNotFound(uint256 projectId);
|
|
42
44
|
error JBTerminalStore_RulesetPaymentPaused();
|
|
43
45
|
error JBTerminalStore_TerminalMigrationNotAllowed();
|
|
@@ -184,99 +186,25 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
184
186
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
185
187
|
)
|
|
186
188
|
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
decimals: accountingContext.decimals,
|
|
198
|
-
currency: accountingContext.currency
|
|
199
|
-
})
|
|
200
|
-
: _surplusFrom({
|
|
201
|
-
terminal: msg.sender,
|
|
202
|
-
projectId: projectId,
|
|
203
|
-
accountingContexts: balanceAccountingContexts,
|
|
204
|
-
ruleset: ruleset,
|
|
205
|
-
targetDecimals: accountingContext.decimals,
|
|
206
|
-
targetCurrency: accountingContext.currency
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Scoped to keep `totalSupply` and `context` off the outer stack.
|
|
210
|
-
{
|
|
211
|
-
// Get the total number of outstanding project tokens.
|
|
212
|
-
uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
|
|
213
|
-
.totalTokenSupplyWithReservedTokensOf(projectId);
|
|
214
|
-
|
|
215
|
-
// Can't cash out more tokens than are in the supply.
|
|
216
|
-
if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
|
|
217
|
-
|
|
218
|
-
// SECURITY NOTE: The data hook has absolute control over cash-out economics.
|
|
219
|
-
// It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
|
|
220
|
-
// completely overriding the terminal's bonding curve math. For example, setting
|
|
221
|
-
// totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
|
|
222
|
-
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
223
|
-
|
|
224
|
-
// If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
|
|
225
|
-
if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
|
|
226
|
-
// Build the cash out context field-by-field to avoid stack-too-deep
|
|
227
|
-
// (the struct has 11 fields — a struct literal would require all values on the stack at once).
|
|
228
|
-
JBBeforeCashOutRecordedContext memory context;
|
|
229
|
-
context.terminal = msg.sender;
|
|
230
|
-
context.holder = holder;
|
|
231
|
-
context.projectId = projectId;
|
|
232
|
-
context.rulesetId = ruleset.id;
|
|
233
|
-
context.cashOutCount = cashOutCount;
|
|
234
|
-
context.totalSupply = totalSupply;
|
|
235
|
-
context.surplus = JBTokenAmount({
|
|
236
|
-
token: accountingContext.token,
|
|
237
|
-
value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
|
|
238
|
-
decimals: accountingContext.decimals,
|
|
239
|
-
currency: accountingContext.currency
|
|
240
|
-
});
|
|
241
|
-
context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
|
|
242
|
-
context.cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
243
|
-
context.beneficiaryIsFeeless = beneficiaryIsFeeless;
|
|
244
|
-
context.metadata = metadata;
|
|
245
|
-
|
|
246
|
-
(cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
|
|
247
|
-
IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
|
|
248
|
-
} else {
|
|
249
|
-
cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
|
|
253
|
-
// result.
|
|
254
|
-
if (reclaimAmount != 0) {
|
|
255
|
-
reclaimAmount = JBCashOuts.cashOutFrom({
|
|
256
|
-
surplus: reclaimAmount,
|
|
257
|
-
cashOutCount: cashOutCount,
|
|
258
|
-
totalSupply: totalSupply,
|
|
259
|
-
cashOutTaxRate: cashOutTaxRate
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
189
|
+
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _computeCashOutFrom({
|
|
190
|
+
terminal: msg.sender,
|
|
191
|
+
holder: holder,
|
|
192
|
+
projectId: projectId,
|
|
193
|
+
cashOutCount: cashOutCount,
|
|
194
|
+
accountingContext: accountingContext,
|
|
195
|
+
balanceAccountingContexts: balanceAccountingContexts,
|
|
196
|
+
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
197
|
+
metadata: metadata
|
|
198
|
+
});
|
|
263
199
|
|
|
264
|
-
//
|
|
200
|
+
// Compute the total amount to subtract from the project's balance.
|
|
265
201
|
uint256 balanceDiff = reclaimAmount;
|
|
266
202
|
|
|
267
|
-
// Ensure that the specifications have valid amounts.
|
|
268
203
|
if (hookSpecifications.length != 0) {
|
|
269
|
-
// Keep a reference to the number of cash out hooks specified.
|
|
270
204
|
uint256 numberOfSpecifications = hookSpecifications.length;
|
|
271
|
-
|
|
272
|
-
// Loop through each specification.
|
|
273
205
|
for (uint256 i; i < numberOfSpecifications; i++) {
|
|
274
|
-
// Get a reference to the specification's amount.
|
|
275
206
|
uint256 specificationAmount = hookSpecifications[i].amount;
|
|
276
|
-
|
|
277
|
-
// Ensure the amount is non-zero.
|
|
278
207
|
if (specificationAmount != 0) {
|
|
279
|
-
// Increment the total amount being subtracted from the balance.
|
|
280
208
|
balanceDiff += specificationAmount;
|
|
281
209
|
}
|
|
282
210
|
}
|
|
@@ -323,98 +251,21 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
323
251
|
override
|
|
324
252
|
returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
|
|
325
253
|
{
|
|
326
|
-
|
|
327
|
-
ruleset =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// The weight according to which new tokens are to be minted, as a fixed point number with 18 decimals.
|
|
336
|
-
uint256 weight;
|
|
337
|
-
|
|
338
|
-
// SECURITY NOTE: The data hook has absolute control over payment token minting.
|
|
339
|
-
// It can return an arbitrary weight (overriding the ruleset's weight) and hook specifications
|
|
340
|
-
// that divert payment funds to external hooks before they reach the project's balance.
|
|
341
|
-
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
342
|
-
|
|
343
|
-
// If the ruleset has a data hook enabled for payments, use it to derive a weight and memo.
|
|
344
|
-
if (ruleset.useDataHookForPay() && ruleset.dataHook() != address(0)) {
|
|
345
|
-
// Create the pay context that'll be sent to the data hook.
|
|
346
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
347
|
-
terminal: msg.sender,
|
|
348
|
-
payer: payer,
|
|
349
|
-
amount: amount,
|
|
350
|
-
projectId: projectId,
|
|
351
|
-
rulesetId: ruleset.id,
|
|
352
|
-
beneficiary: beneficiary,
|
|
353
|
-
weight: ruleset.weight,
|
|
354
|
-
reservedPercent: ruleset.reservedPercent(),
|
|
355
|
-
metadata: metadata
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
(weight, hookSpecifications) = IJBRulesetDataHook(ruleset.dataHook()).beforePayRecordedWith(context);
|
|
359
|
-
}
|
|
360
|
-
// Otherwise use the ruleset's weight
|
|
361
|
-
else {
|
|
362
|
-
weight = ruleset.weight;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Keep a reference to the amount that should be added to the project's balance.
|
|
366
|
-
uint256 balanceDiff = amount.value;
|
|
367
|
-
|
|
368
|
-
// Scoped section preventing stack too deep.
|
|
369
|
-
{
|
|
370
|
-
// Keep a reference to the number of hook specifications.
|
|
371
|
-
uint256 numberOfSpecifications = hookSpecifications.length;
|
|
372
|
-
|
|
373
|
-
// Ensure that the specifications have valid amounts.
|
|
374
|
-
for (uint256 i; i < numberOfSpecifications; i++) {
|
|
375
|
-
// Get a reference to the specification's amount.
|
|
376
|
-
uint256 specifiedAmount = hookSpecifications[i].amount;
|
|
377
|
-
|
|
378
|
-
// Ensure the amount is non-zero.
|
|
379
|
-
if (specifiedAmount != 0) {
|
|
380
|
-
// Can't send more to hook than was paid.
|
|
381
|
-
if (specifiedAmount > balanceDiff) {
|
|
382
|
-
revert JBTerminalStore_InvalidAmountToForwardHook(specifiedAmount, balanceDiff);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Decrement the total amount being added to the local balance.
|
|
386
|
-
balanceDiff -= specifiedAmount;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// If there's no amount being recorded, there's nothing left to do.
|
|
392
|
-
if (amount.value == 0) return (ruleset, 0, hookSpecifications);
|
|
254
|
+
uint256 balanceDiff;
|
|
255
|
+
(ruleset, tokenCount, hookSpecifications, balanceDiff) = _computePayFrom({
|
|
256
|
+
terminal: msg.sender,
|
|
257
|
+
payer: payer,
|
|
258
|
+
amount: amount,
|
|
259
|
+
projectId: projectId,
|
|
260
|
+
beneficiary: beneficiary,
|
|
261
|
+
metadata: metadata
|
|
262
|
+
});
|
|
393
263
|
|
|
394
264
|
// Add the correct balance difference to the token balance of the project.
|
|
395
265
|
if (balanceDiff != 0) {
|
|
396
266
|
balanceOf[msg.sender][projectId][amount.token] =
|
|
397
267
|
balanceOf[msg.sender][projectId][amount.token] + balanceDiff;
|
|
398
268
|
}
|
|
399
|
-
|
|
400
|
-
// If there's no weight, the token count must be 0, so there's nothing left to do.
|
|
401
|
-
if (weight == 0) return (ruleset, 0, hookSpecifications);
|
|
402
|
-
|
|
403
|
-
// If the terminal should base its weight on a currency other than the terminal's currency, determine the
|
|
404
|
-
// factor. The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the
|
|
405
|
-
// same
|
|
406
|
-
// number of decimals as the `amount`.
|
|
407
|
-
uint256 weightRatio = amount.currency == ruleset.baseCurrency()
|
|
408
|
-
? 10 ** amount.decimals
|
|
409
|
-
: PRICES.pricePerUnitOf({
|
|
410
|
-
projectId: projectId,
|
|
411
|
-
pricingCurrency: amount.currency,
|
|
412
|
-
unitCurrency: ruleset.baseCurrency(),
|
|
413
|
-
decimals: amount.decimals
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
|
|
417
|
-
tokenCount = mulDiv(amount.value, weight, weightRatio);
|
|
418
269
|
}
|
|
419
270
|
|
|
420
271
|
/// @notice Records a payout from a project.
|
|
@@ -699,6 +550,34 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
699
550
|
});
|
|
700
551
|
}
|
|
701
552
|
|
|
553
|
+
/// @notice Returns the number of surplus terminal tokens that would be reclaimed by cashing out a given number of
|
|
554
|
+
/// tokens across all of a project's terminals using all accounting contexts.
|
|
555
|
+
/// @param projectId The ID of the project whose tokens would be cashed out.
|
|
556
|
+
/// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
|
|
557
|
+
/// @param decimals The number of decimals to include in the resulting fixed point number.
|
|
558
|
+
/// @param currency The currency that the resulting number will be in terms of.
|
|
559
|
+
/// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount` tokens.
|
|
560
|
+
function currentTotalReclaimableSurplusOf(
|
|
561
|
+
uint256 projectId,
|
|
562
|
+
uint256 cashOutCount,
|
|
563
|
+
uint256 decimals,
|
|
564
|
+
uint256 currency
|
|
565
|
+
)
|
|
566
|
+
external
|
|
567
|
+
view
|
|
568
|
+
override
|
|
569
|
+
returns (uint256)
|
|
570
|
+
{
|
|
571
|
+
return this.currentReclaimableSurplusOf({
|
|
572
|
+
projectId: projectId,
|
|
573
|
+
cashOutCount: cashOutCount,
|
|
574
|
+
terminals: new IJBTerminal[](0),
|
|
575
|
+
accountingContexts: new JBAccountingContext[](0),
|
|
576
|
+
decimals: decimals,
|
|
577
|
+
currency: currency
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
702
581
|
/// @notice Gets the current surplus amount in a terminal for a specified project.
|
|
703
582
|
/// @dev The surplus is the amount of funds a project has in a terminal in excess of its payout limit.
|
|
704
583
|
/// @dev The surplus is represented as a fixed point number with the same amount of decimals as the specified
|
|
@@ -757,10 +636,327 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
757
636
|
});
|
|
758
637
|
}
|
|
759
638
|
|
|
639
|
+
/// @notice Simulates a cash out without modifying state.
|
|
640
|
+
/// @dev Invokes data hooks if configured, but skips the balance sufficiency check (balance may change between
|
|
641
|
+
/// preview and execution).
|
|
642
|
+
/// @param holder The address cashing out.
|
|
643
|
+
/// @param projectId The ID of the project being cashed out from.
|
|
644
|
+
/// @param cashOutCount The number of project tokens being cashed out.
|
|
645
|
+
/// @param accountingContext The accounting context of the token being reclaimed.
|
|
646
|
+
/// @param balanceAccountingContexts The accounting contexts to include in the balance calculation.
|
|
647
|
+
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
|
|
648
|
+
/// @param metadata Extra data to pass along to the data hook.
|
|
649
|
+
/// @return ruleset The project's current ruleset.
|
|
650
|
+
/// @return reclaimAmount The amount that would be reclaimed.
|
|
651
|
+
/// @return cashOutTaxRate The cash out tax rate that would be applied.
|
|
652
|
+
/// @return hookSpecifications Any cash out hook specifications from the data hook.
|
|
653
|
+
function previewCashOutFrom(
|
|
654
|
+
address holder,
|
|
655
|
+
uint256 projectId,
|
|
656
|
+
uint256 cashOutCount,
|
|
657
|
+
JBAccountingContext calldata accountingContext,
|
|
658
|
+
JBAccountingContext[] calldata balanceAccountingContexts,
|
|
659
|
+
bool beneficiaryIsFeeless,
|
|
660
|
+
bytes calldata metadata
|
|
661
|
+
)
|
|
662
|
+
external
|
|
663
|
+
view
|
|
664
|
+
override
|
|
665
|
+
returns (
|
|
666
|
+
JBRuleset memory ruleset,
|
|
667
|
+
uint256 reclaimAmount,
|
|
668
|
+
uint256 cashOutTaxRate,
|
|
669
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
670
|
+
)
|
|
671
|
+
{
|
|
672
|
+
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _computeCashOutFrom({
|
|
673
|
+
terminal: msg.sender,
|
|
674
|
+
holder: holder,
|
|
675
|
+
projectId: projectId,
|
|
676
|
+
cashOutCount: cashOutCount,
|
|
677
|
+
accountingContext: accountingContext,
|
|
678
|
+
balanceAccountingContexts: balanceAccountingContexts,
|
|
679
|
+
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
680
|
+
metadata: metadata
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/// @notice Simulates a payment without modifying state.
|
|
685
|
+
/// @dev Invokes data hooks if configured. Returns the same token count and hook specifications that
|
|
686
|
+
/// `recordPaymentFrom` would produce.
|
|
687
|
+
/// @param payer The address of the payer.
|
|
688
|
+
/// @param amount The amount being paid.
|
|
689
|
+
/// @param projectId The ID of the project being paid.
|
|
690
|
+
/// @param beneficiary The address to mint project tokens to.
|
|
691
|
+
/// @param metadata Extra data to pass along to the data hook.
|
|
692
|
+
/// @return ruleset The project's current ruleset.
|
|
693
|
+
/// @return tokenCount The number of project tokens that would be minted, including reserved tokens.
|
|
694
|
+
/// @return hookSpecifications Any pay hook specifications from the data hook.
|
|
695
|
+
function previewPayFrom(
|
|
696
|
+
address payer,
|
|
697
|
+
JBTokenAmount memory amount,
|
|
698
|
+
uint256 projectId,
|
|
699
|
+
address beneficiary,
|
|
700
|
+
bytes calldata metadata
|
|
701
|
+
)
|
|
702
|
+
external
|
|
703
|
+
view
|
|
704
|
+
override
|
|
705
|
+
returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
|
|
706
|
+
{
|
|
707
|
+
// Use the caller as the terminal context for the preview.
|
|
708
|
+
(ruleset, tokenCount, hookSpecifications,) = _computePayFrom({
|
|
709
|
+
terminal: msg.sender,
|
|
710
|
+
payer: payer,
|
|
711
|
+
amount: amount,
|
|
712
|
+
projectId: projectId,
|
|
713
|
+
beneficiary: beneficiary,
|
|
714
|
+
metadata: metadata
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
760
718
|
//*********************************************************************//
|
|
761
719
|
// -------------------------- internal views ------------------------- //
|
|
762
720
|
//*********************************************************************//
|
|
763
721
|
|
|
722
|
+
/// @notice Computes payment results without writing state.
|
|
723
|
+
/// @param terminal The terminal recording the payment.
|
|
724
|
+
/// @param payer The address that made the payment.
|
|
725
|
+
/// @param amount The amount of tokens being paid.
|
|
726
|
+
/// @param projectId The ID of the project being paid.
|
|
727
|
+
/// @param beneficiary The beneficiary of the payment.
|
|
728
|
+
/// @param metadata Bytes to send to the data hook.
|
|
729
|
+
/// @return ruleset The ruleset the payment would be made during.
|
|
730
|
+
/// @return tokenCount The number of project tokens that would be minted.
|
|
731
|
+
/// @return hookSpecifications Pay hook specifications from the data hook.
|
|
732
|
+
/// @return balanceDiff The amount that would be added to the project's balance.
|
|
733
|
+
function _computePayFrom(
|
|
734
|
+
address terminal,
|
|
735
|
+
address payer,
|
|
736
|
+
JBTokenAmount memory amount,
|
|
737
|
+
uint256 projectId,
|
|
738
|
+
address beneficiary,
|
|
739
|
+
bytes memory metadata
|
|
740
|
+
)
|
|
741
|
+
internal
|
|
742
|
+
view
|
|
743
|
+
returns (
|
|
744
|
+
JBRuleset memory ruleset,
|
|
745
|
+
uint256 tokenCount,
|
|
746
|
+
JBPayHookSpecification[] memory hookSpecifications,
|
|
747
|
+
uint256 balanceDiff
|
|
748
|
+
)
|
|
749
|
+
{
|
|
750
|
+
// Get a reference to the project's current ruleset.
|
|
751
|
+
ruleset = RULESETS.currentOf(projectId);
|
|
752
|
+
|
|
753
|
+
// The project must have a ruleset.
|
|
754
|
+
if (ruleset.cycleNumber == 0) revert JBTerminalStore_RulesetNotFound(projectId);
|
|
755
|
+
|
|
756
|
+
// The ruleset must not have payments paused.
|
|
757
|
+
if (ruleset.pausePay()) revert JBTerminalStore_RulesetPaymentPaused();
|
|
758
|
+
|
|
759
|
+
// The weight according to which new tokens are to be minted, as a fixed point number with 18 decimals.
|
|
760
|
+
uint256 weight;
|
|
761
|
+
|
|
762
|
+
// SECURITY NOTE: The data hook has absolute control over payment token minting.
|
|
763
|
+
// It can return an arbitrary weight (overriding the ruleset's weight) and hook specifications
|
|
764
|
+
// that divert payment funds to external hooks before they reach the project's balance.
|
|
765
|
+
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
766
|
+
|
|
767
|
+
// If the ruleset has a data hook enabled for payments, use it to derive a weight and memo.
|
|
768
|
+
if (ruleset.useDataHookForPay() && ruleset.dataHook() != address(0)) {
|
|
769
|
+
// Create the pay context that'll be sent to the data hook.
|
|
770
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
771
|
+
terminal: terminal,
|
|
772
|
+
payer: payer,
|
|
773
|
+
amount: amount,
|
|
774
|
+
projectId: projectId,
|
|
775
|
+
rulesetId: ruleset.id,
|
|
776
|
+
beneficiary: beneficiary,
|
|
777
|
+
weight: ruleset.weight,
|
|
778
|
+
reservedPercent: ruleset.reservedPercent(),
|
|
779
|
+
metadata: metadata
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
(weight, hookSpecifications) = IJBRulesetDataHook(ruleset.dataHook()).beforePayRecordedWith(context);
|
|
783
|
+
}
|
|
784
|
+
// Otherwise use the ruleset's weight
|
|
785
|
+
else {
|
|
786
|
+
weight = ruleset.weight;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Keep a reference to the amount that should be added to the project's balance.
|
|
790
|
+
balanceDiff = amount.value;
|
|
791
|
+
|
|
792
|
+
// Scoped section preventing stack too deep.
|
|
793
|
+
{
|
|
794
|
+
// Keep a reference to the number of hook specifications.
|
|
795
|
+
uint256 numberOfSpecifications = hookSpecifications.length;
|
|
796
|
+
|
|
797
|
+
// Ensure that the specifications have valid amounts.
|
|
798
|
+
for (uint256 i; i < numberOfSpecifications; i++) {
|
|
799
|
+
// Get a reference to the specification's amount.
|
|
800
|
+
if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
|
|
801
|
+
revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
uint256 specifiedAmount = hookSpecifications[i].amount;
|
|
805
|
+
|
|
806
|
+
// Ensure the amount is non-zero.
|
|
807
|
+
if (specifiedAmount != 0) {
|
|
808
|
+
// Can't send more to hook than was paid.
|
|
809
|
+
if (specifiedAmount > balanceDiff) {
|
|
810
|
+
revert JBTerminalStore_InvalidAmountToForwardHook(specifiedAmount, balanceDiff);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Decrement the total amount being added to the local balance.
|
|
814
|
+
balanceDiff -= specifiedAmount;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// If there's no amount being recorded, there's nothing left to do.
|
|
820
|
+
if (amount.value == 0) return (ruleset, 0, hookSpecifications, 0);
|
|
821
|
+
|
|
822
|
+
// If there's no weight, the token count must be 0, so there's nothing left to do.
|
|
823
|
+
if (weight == 0) return (ruleset, 0, hookSpecifications, balanceDiff);
|
|
824
|
+
|
|
825
|
+
// If the terminal should base its weight on a currency other than the terminal's currency, determine the
|
|
826
|
+
// factor. The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the
|
|
827
|
+
// same
|
|
828
|
+
// number of decimals as the `amount`.
|
|
829
|
+
uint256 weightRatio = amount.currency == ruleset.baseCurrency()
|
|
830
|
+
? 10 ** amount.decimals
|
|
831
|
+
: PRICES.pricePerUnitOf({
|
|
832
|
+
projectId: projectId,
|
|
833
|
+
pricingCurrency: amount.currency,
|
|
834
|
+
unitCurrency: ruleset.baseCurrency(),
|
|
835
|
+
decimals: amount.decimals
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
|
|
839
|
+
tokenCount = mulDiv(amount.value, weight, weightRatio);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/// @notice Computes cash out results without writing state.
|
|
843
|
+
/// @param terminal The terminal recording the cash out.
|
|
844
|
+
/// @param holder The account that is cashing out tokens.
|
|
845
|
+
/// @param projectId The ID of the project being cashed out from.
|
|
846
|
+
/// @param cashOutCount The number of project tokens to cash out.
|
|
847
|
+
/// @param accountingContext The accounting context of the token being reclaimed.
|
|
848
|
+
/// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
|
|
849
|
+
/// surplus being reclaimed from.
|
|
850
|
+
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
|
|
851
|
+
/// @param metadata Bytes to send to the data hook.
|
|
852
|
+
/// @return ruleset The ruleset during the cash out.
|
|
853
|
+
/// @return reclaimAmount The amount of tokens reclaimed.
|
|
854
|
+
/// @return cashOutTaxRate The cash out tax rate applied.
|
|
855
|
+
/// @return hookSpecifications Cash out hook specifications from the data hook.
|
|
856
|
+
function _computeCashOutFrom(
|
|
857
|
+
address terminal,
|
|
858
|
+
address holder,
|
|
859
|
+
uint256 projectId,
|
|
860
|
+
uint256 cashOutCount,
|
|
861
|
+
JBAccountingContext calldata accountingContext,
|
|
862
|
+
JBAccountingContext[] calldata balanceAccountingContexts,
|
|
863
|
+
bool beneficiaryIsFeeless,
|
|
864
|
+
bytes memory metadata
|
|
865
|
+
)
|
|
866
|
+
internal
|
|
867
|
+
view
|
|
868
|
+
returns (
|
|
869
|
+
JBRuleset memory ruleset,
|
|
870
|
+
uint256 reclaimAmount,
|
|
871
|
+
uint256 cashOutTaxRate,
|
|
872
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
873
|
+
)
|
|
874
|
+
{
|
|
875
|
+
// Get a reference to the project's current ruleset.
|
|
876
|
+
ruleset = RULESETS.currentOf(projectId);
|
|
877
|
+
|
|
878
|
+
// Store the current surplus in `reclaimAmount` temporarily to avoid allocating a separate local variable
|
|
879
|
+
// (saves one stack slot, which is needed to fit the 7th parameter without hitting stack-too-deep).
|
|
880
|
+
reclaimAmount = ruleset.useTotalSurplusForCashOuts()
|
|
881
|
+
? JBSurplus.currentSurplusOf({
|
|
882
|
+
projectId: projectId,
|
|
883
|
+
terminals: DIRECTORY.terminalsOf(projectId),
|
|
884
|
+
accountingContexts: new JBAccountingContext[](0),
|
|
885
|
+
decimals: accountingContext.decimals,
|
|
886
|
+
currency: accountingContext.currency
|
|
887
|
+
})
|
|
888
|
+
: _surplusFrom({
|
|
889
|
+
terminal: terminal,
|
|
890
|
+
projectId: projectId,
|
|
891
|
+
accountingContexts: balanceAccountingContexts,
|
|
892
|
+
ruleset: ruleset,
|
|
893
|
+
targetDecimals: accountingContext.decimals,
|
|
894
|
+
targetCurrency: accountingContext.currency
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Scoped to keep `totalSupply` and `context` off the outer stack.
|
|
898
|
+
{
|
|
899
|
+
// Get the total number of outstanding project tokens.
|
|
900
|
+
uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
|
|
901
|
+
.totalTokenSupplyWithReservedTokensOf(projectId);
|
|
902
|
+
|
|
903
|
+
// Can't cash out more tokens than are in the supply.
|
|
904
|
+
if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
|
|
905
|
+
|
|
906
|
+
// SECURITY NOTE: The data hook has absolute control over cash-out economics.
|
|
907
|
+
// It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
|
|
908
|
+
// completely overriding the terminal's bonding curve math. For example, setting
|
|
909
|
+
// totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
|
|
910
|
+
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
911
|
+
|
|
912
|
+
// If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
|
|
913
|
+
if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
|
|
914
|
+
// Build the cash out context field-by-field to avoid stack-too-deep
|
|
915
|
+
// (the struct has 11 fields — a struct literal would require all values on the stack at once).
|
|
916
|
+
JBBeforeCashOutRecordedContext memory context;
|
|
917
|
+
context.terminal = terminal;
|
|
918
|
+
context.holder = holder;
|
|
919
|
+
context.projectId = projectId;
|
|
920
|
+
context.rulesetId = ruleset.id;
|
|
921
|
+
context.cashOutCount = cashOutCount;
|
|
922
|
+
context.totalSupply = totalSupply;
|
|
923
|
+
context.surplus = JBTokenAmount({
|
|
924
|
+
token: accountingContext.token,
|
|
925
|
+
value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
|
|
926
|
+
decimals: accountingContext.decimals,
|
|
927
|
+
currency: accountingContext.currency
|
|
928
|
+
});
|
|
929
|
+
context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
|
|
930
|
+
context.cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
931
|
+
context.beneficiaryIsFeeless = beneficiaryIsFeeless;
|
|
932
|
+
context.metadata = metadata;
|
|
933
|
+
|
|
934
|
+
(cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
|
|
935
|
+
IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
|
|
936
|
+
|
|
937
|
+
// Noop specifications are informational only, so they can't also request forwarded funds.
|
|
938
|
+
for (uint256 i; i < hookSpecifications.length; i++) {
|
|
939
|
+
if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
|
|
940
|
+
revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
|
|
948
|
+
// result.
|
|
949
|
+
if (reclaimAmount != 0) {
|
|
950
|
+
reclaimAmount = JBCashOuts.cashOutFrom({
|
|
951
|
+
surplus: reclaimAmount,
|
|
952
|
+
cashOutCount: cashOutCount,
|
|
953
|
+
totalSupply: totalSupply,
|
|
954
|
+
cashOutTaxRate: cashOutTaxRate
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
764
960
|
/// @notice Gets a project's surplus amount in a terminal as measured by a given ruleset, across multiple accounting
|
|
765
961
|
/// contexts.
|
|
766
962
|
/// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure
|
|
@@ -4,6 +4,8 @@ pragma solidity ^0.8.0;
|
|
|
4
4
|
import {IJBCashOutHook} from "./IJBCashOutHook.sol";
|
|
5
5
|
import {IJBTerminal} from "./IJBTerminal.sol";
|
|
6
6
|
import {JBAfterCashOutRecordedContext} from "../structs/JBAfterCashOutRecordedContext.sol";
|
|
7
|
+
import {JBCashOutHookSpecification} from "../structs/JBCashOutHookSpecification.sol";
|
|
8
|
+
import {JBRuleset} from "../structs/JBRuleset.sol";
|
|
7
9
|
|
|
8
10
|
/// @notice A terminal that can be cashed out from.
|
|
9
11
|
interface IJBCashOutTerminal is IJBTerminal {
|
|
@@ -45,6 +47,34 @@ interface IJBCashOutTerminal is IJBTerminal {
|
|
|
45
47
|
address caller
|
|
46
48
|
);
|
|
47
49
|
|
|
50
|
+
/// @notice Simulates cashing out project tokens from this terminal without modifying state.
|
|
51
|
+
/// @param holder The address whose tokens are being cashed out.
|
|
52
|
+
/// @param projectId The ID of the project whose tokens are being cashed out.
|
|
53
|
+
/// @param cashOutCount The number of project tokens to cash out.
|
|
54
|
+
/// @param tokenToReclaim The token to reclaim from the project's surplus.
|
|
55
|
+
/// @param beneficiary The address that would receive the reclaimed tokens.
|
|
56
|
+
/// @param metadata Extra data to send to the data hook and cash out hooks.
|
|
57
|
+
/// @return ruleset The project's current ruleset.
|
|
58
|
+
/// @return reclaimAmount The amount of tokens that would be reclaimed from the project's surplus.
|
|
59
|
+
/// @return cashOutTaxRate The cash out tax rate that would be applied.
|
|
60
|
+
/// @return hookSpecifications Any cash out hook specifications from the data hook.
|
|
61
|
+
function previewCashOutFrom(
|
|
62
|
+
address holder,
|
|
63
|
+
uint256 projectId,
|
|
64
|
+
uint256 cashOutCount,
|
|
65
|
+
address tokenToReclaim,
|
|
66
|
+
address payable beneficiary,
|
|
67
|
+
bytes calldata metadata
|
|
68
|
+
)
|
|
69
|
+
external
|
|
70
|
+
view
|
|
71
|
+
returns (
|
|
72
|
+
JBRuleset memory ruleset,
|
|
73
|
+
uint256 reclaimAmount,
|
|
74
|
+
uint256 cashOutTaxRate,
|
|
75
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
76
|
+
);
|
|
77
|
+
|
|
48
78
|
/// @notice Cashes out a holder's tokens for a project, reclaiming the token's proportional share of the project's
|
|
49
79
|
/// surplus.
|
|
50
80
|
/// @param holder The address whose tokens are being cashed out.
|
|
@@ -217,6 +217,21 @@ interface IJBController is IERC165, IJBProjectUriRegistry, IJBDirectoryAccessCon
|
|
|
217
217
|
/// @return The pending reserved token balance.
|
|
218
218
|
function pendingReservedTokenBalanceOf(uint256 projectId) external view returns (uint256);
|
|
219
219
|
|
|
220
|
+
/// @notice Previews how many beneficiary and reserved tokens `mintTokensOf(...)` would produce.
|
|
221
|
+
/// @param projectId The ID of the project whose tokens are being minted.
|
|
222
|
+
/// @param tokenCount The number of tokens to mint, including any reserved tokens.
|
|
223
|
+
/// @param useReservedPercent Whether to apply the ruleset's reserved percent.
|
|
224
|
+
/// @return beneficiaryTokenCount The number of tokens that would be minted for the beneficiary.
|
|
225
|
+
/// @return reservedTokenCount The number of tokens that would be reserved.
|
|
226
|
+
function previewMintOf(
|
|
227
|
+
uint256 projectId,
|
|
228
|
+
uint256 tokenCount,
|
|
229
|
+
bool useReservedPercent
|
|
230
|
+
)
|
|
231
|
+
external
|
|
232
|
+
view
|
|
233
|
+
returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount);
|
|
234
|
+
|
|
220
235
|
/// @notice Returns a project's total token supply including pending reserved tokens.
|
|
221
236
|
/// @param projectId The ID of the project to get the total token supply of.
|
|
222
237
|
/// @return The total supply including pending reserved tokens.
|