@bananapus/core-v6 0.0.53 → 0.0.55
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/CHANGELOG.md +0 -48
- package/foundry.toml +0 -1
- package/package.json +1 -1
- package/script/helpers/CoreDeploymentLib.sol +2 -3
- package/src/JBController.sol +7 -3
- package/src/JBERC20.sol +7 -7
- package/src/JBFeelessAddresses.sol +50 -5
- package/src/JBMultiTerminal.sol +336 -564
- package/src/JBPrices.sol +1 -1
- package/src/JBRulesets.sol +6 -6
- package/src/JBTokens.sol +1 -1
- package/src/interfaces/IJBCashOutTerminal.sol +0 -68
- package/src/interfaces/IJBFeeTerminal.sol +1 -1
- package/src/interfaces/IJBFeelessAddresses.sol +19 -3
- package/src/interfaces/IJBFeelessHook.sol +18 -0
- package/src/interfaces/IJBToken.sol +2 -2
- package/src/libraries/JBCashOuts.sol +9 -8
- package/src/libraries/JBConstants.sol +3 -6
- package/src/libraries/JBFees.sol +23 -27
- package/src/libraries/JBPayoutSplitGroupLib.sol +3 -3
- package/src/libraries/JBRulesetMetadataResolver.sol +12 -20
- package/src/structs/JBRulesetMetadata.sol +1 -4
- package/test/helpers/JBTest.sol +3 -6
- package/test/mock/MockFeelessHook.sol +59 -0
- package/src/libraries/JBCashOutHookSpecsLib.sol +0 -176
- package/src/libraries/JBHeldFeesLib.sol +0 -288
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -28,21 +28,18 @@ import {IJBSplits} from "./interfaces/IJBSplits.sol";
|
|
|
28
28
|
import {IJBTerminal} from "./interfaces/IJBTerminal.sol";
|
|
29
29
|
import {IJBTerminalStore} from "./interfaces/IJBTerminalStore.sol";
|
|
30
30
|
import {IJBTokens} from "./interfaces/IJBTokens.sol";
|
|
31
|
-
import {JBCashOutHookSpecsLib} from "./libraries/JBCashOutHookSpecsLib.sol";
|
|
32
31
|
import {JBConstants} from "./libraries/JBConstants.sol";
|
|
33
32
|
import {JBFees} from "./libraries/JBFees.sol";
|
|
34
|
-
import {JBHeldFeesLib} from "./libraries/JBHeldFeesLib.sol";
|
|
35
33
|
import {JBMetadataResolver} from "./libraries/JBMetadataResolver.sol";
|
|
36
34
|
import {JBPayoutSplitGroupLib} from "./libraries/JBPayoutSplitGroupLib.sol";
|
|
37
35
|
import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
|
|
38
36
|
import {JBAccountingContext} from "./structs/JBAccountingContext.sol";
|
|
39
|
-
import {JBAfterPayRecordedContext} from "./structs/JBAfterPayRecordedContext.sol";
|
|
40
37
|
import {JBAfterCashOutRecordedContext} from "./structs/JBAfterCashOutRecordedContext.sol";
|
|
38
|
+
import {JBAfterPayRecordedContext} from "./structs/JBAfterPayRecordedContext.sol";
|
|
41
39
|
import {JBCashOutHookSpecification} from "./structs/JBCashOutHookSpecification.sol";
|
|
42
40
|
import {JBFee} from "./structs/JBFee.sol";
|
|
43
41
|
import {JBPayHookSpecification} from "./structs/JBPayHookSpecification.sol";
|
|
44
42
|
import {JBRuleset} from "./structs/JBRuleset.sol";
|
|
45
|
-
import {JBRulesetMetadata} from "./structs/JBRulesetMetadata.sol";
|
|
46
43
|
import {JBSingleAllowance} from "./structs/JBSingleAllowance.sol";
|
|
47
44
|
import {JBSplit} from "./structs/JBSplit.sol";
|
|
48
45
|
import {JBSplitHookContext} from "./structs/JBSplitHookContext.sol";
|
|
@@ -66,9 +63,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
66
63
|
// --------------------------- custom errors ------------------------- //
|
|
67
64
|
//*********************************************************************//
|
|
68
65
|
|
|
69
|
-
error JBMultiTerminal_BeneficiaryProjectFeeFreeInflowsPaused(uint256 projectId);
|
|
70
|
-
error JBMultiTerminal_BeneficiaryProjectHasNoAccountingContexts(uint256 projectId);
|
|
71
|
-
error JBMultiTerminal_BeneficiaryProjectNotPaid(uint256 projectId);
|
|
72
66
|
error JBMultiTerminal_FeeTerminalNotFound(address token);
|
|
73
67
|
error JBMultiTerminal_MintNotAllowed(uint256 projectId, uint256 splitProjectId, address terminal);
|
|
74
68
|
error JBMultiTerminal_NoMsgValueAllowed(uint256 value);
|
|
@@ -85,6 +79,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
85
79
|
// ------------------------ internal constants ----------------------- //
|
|
86
80
|
//*********************************************************************//
|
|
87
81
|
|
|
82
|
+
/// @notice Project ID #1 receives fees. It should be the first project launched during the deployment process.
|
|
83
|
+
uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
84
|
+
|
|
88
85
|
/// @notice The number of seconds fees can be held for.
|
|
89
86
|
uint256 internal constant _FEE_HOLDING_SECONDS = 2_419_200; // 28 days
|
|
90
87
|
|
|
@@ -214,94 +211,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
214
211
|
}
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
/// @notice Atomically cash out `holder`'s tokens of `projectId` and add the reclaim to
|
|
218
|
-
/// `beneficiaryProjectId`'s balance (no project tokens minted on the destination side).
|
|
219
|
-
/// @dev Equivalent to calling `cashOutTokensOf` followed by `addToBalanceOf` on the destination project,
|
|
220
|
-
/// except the source-side cash out fee is skipped. The equivalent fee is bound on the destination project's
|
|
221
|
-
/// side instead: `_feeFreeSurplusOf[beneficiaryProjectId]` is credited by the first of the destination
|
|
222
|
-
/// project's accounting contexts on this terminal whose balance grows during the routing.
|
|
223
|
-
/// @dev The destination terminal is `DIRECTORY.primaryTerminalOf(beneficiaryProjectId, tokenToReclaim)` —
|
|
224
|
-
/// which may itself be a router that swaps before adding to balance.
|
|
225
|
-
/// @dev Held-fee return on the destination side is hardcoded to `false`. This entrypoint is for cross-project
|
|
226
|
-
/// balance top-ups only, not for unlocking the destination's held fees. Callers that want to combine
|
|
227
|
-
/// cash-out → add-to-balance with `shouldReturnHeldFees: true` must do it explicitly via two separate calls.
|
|
228
|
-
/// @dev The destination project's current ruleset can set `pauseCrossProjectFeeFreeInflows` to opt out — the
|
|
229
|
-
/// call then reverts. If no delivery to the destination project lands on this terminal under any of the
|
|
230
|
-
/// destination project's accounting contexts, the call also reverts so the source-side fee skip never
|
|
231
|
-
/// becomes a leak.
|
|
232
|
-
/// @param holder The account whose project tokens are being burned.
|
|
233
|
-
/// @param projectId The ID of the source project being cashed out from.
|
|
234
|
-
/// @param cashOutCount The number of source-project tokens to burn, as a fixed point number with 18 decimals.
|
|
235
|
-
/// @param tokenToReclaim The terminal token reclaimed from the source project's surplus.
|
|
236
|
-
/// @param beneficiaryProjectId The destination project receiving the reclaim.
|
|
237
|
-
/// @param cashOutMetadata Bytes forwarded to the source project's data hook and any cashout hook specifications.
|
|
238
|
-
/// @param addToBalanceMetadata Bytes forwarded to the destination project's `addToBalanceOf` event.
|
|
239
|
-
/// @return reclaimAmount The gross reclaim amount returned by the store.
|
|
240
|
-
function addToBalanceAfterCashOutTokensOf(
|
|
241
|
-
address holder,
|
|
242
|
-
uint256 projectId,
|
|
243
|
-
uint256 cashOutCount,
|
|
244
|
-
address tokenToReclaim,
|
|
245
|
-
uint256 beneficiaryProjectId,
|
|
246
|
-
bytes calldata cashOutMetadata,
|
|
247
|
-
bytes calldata addToBalanceMetadata
|
|
248
|
-
)
|
|
249
|
-
external
|
|
250
|
-
override
|
|
251
|
-
returns (uint256 reclaimAmount)
|
|
252
|
-
{
|
|
253
|
-
// Caller must hold (or be operator with) `CASH_OUT_TOKENS` permission on the source `holder`/`projectId`.
|
|
254
|
-
// Burning A's tokens is the load-bearing side effect — gating it stays at the same authority bar as the
|
|
255
|
-
// direct `cashOutTokensOf` entrypoint.
|
|
256
|
-
_requireCashOutPermissionFrom({holder: holder, projectId: projectId});
|
|
257
|
-
|
|
258
|
-
// Destination opt-out check: B's current ruleset can set `pauseCrossProjectFeeFreeInflows = true` to
|
|
259
|
-
// refuse cross-project fee-free credits. Without this gate, anyone holding A's tokens could push a
|
|
260
|
-
// deferred-fee credit onto `_feeFreeSurplusOf[B]` without B consenting.
|
|
261
|
-
_requireBeneficiaryAcceptsFeeFreeInflows(beneficiaryProjectId);
|
|
262
|
-
|
|
263
|
-
// Burn source-project tokens, run cashout-side hooks, take any hook fees, and cap source fee-free.
|
|
264
|
-
// No separate destination beneficiary exists — the caller is the only address attached to this flow,
|
|
265
|
-
// used as the `CashOutTokens` event slot and credited any hook-fee project tokens.
|
|
266
|
-
reclaimAmount = _executeCrossProjectCashOut({
|
|
267
|
-
holder: holder,
|
|
268
|
-
projectId: projectId,
|
|
269
|
-
cashOutCount: cashOutCount,
|
|
270
|
-
tokenToReclaim: tokenToReclaim,
|
|
271
|
-
beneficiary: _msgSender(),
|
|
272
|
-
cashOutMetadata: cashOutMetadata
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Nothing to route if the data hook returned zero reclaim.
|
|
276
|
-
if (reclaimAmount == 0) return 0;
|
|
277
|
-
|
|
278
|
-
// Resolve B's primary terminal for the reclaim token. Could be this terminal (same-terminal short-
|
|
279
|
-
// circuit) or a router that swaps before depositing. Reverts if no primary terminal is registered.
|
|
280
|
-
IJBTerminal destinationTerminal = _resolveBeneficiaryTerminal(beneficiaryProjectId, tokenToReclaim);
|
|
281
|
-
|
|
282
|
-
// Snapshot B's per-context balances on this terminal BEFORE routing. The post-routing comparison
|
|
283
|
-
// identifies the bucket the reclaim actually landed in (matters for cross-token routes where a router
|
|
284
|
-
// swaps to a different token in B's accounting-context list).
|
|
285
|
-
(JBAccountingContext[] memory contexts, uint256[] memory balancesBefore) =
|
|
286
|
-
_snapshotBeneficiaryContextBalances(beneficiaryProjectId);
|
|
287
|
-
|
|
288
|
-
// Route via `_efficientAddToBalance` — handles same-terminal vs cross-terminal (with the standard
|
|
289
|
-
// `_beforeTransferTo`/`_afterTransferTo` allowance dance) and hardcodes `shouldReturnHeldFees: false`,
|
|
290
|
-
// so this entrypoint cannot be used to unlock B's held fees on top of the source-side fee skip.
|
|
291
|
-
_efficientAddToBalance({
|
|
292
|
-
terminal: destinationTerminal,
|
|
293
|
-
projectId: beneficiaryProjectId,
|
|
294
|
-
token: tokenToReclaim,
|
|
295
|
-
amount: reclaimAmount,
|
|
296
|
-
metadata: addToBalanceMetadata
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// Credit `_feeFreeSurplusOf[B]` on the first of B's contexts whose balance grew. This binds the
|
|
300
|
-
// skipped source-side fee on the destination side: B's next non-feeless cashout pays it. Reverts if
|
|
301
|
-
// no context grew (no delivery landed) — without delivery, the fee skip would leak.
|
|
302
|
-
_creditFirstGrowingBeneficiaryContext(beneficiaryProjectId, contexts, balancesBefore);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
214
|
/// @notice Adds funds (terminal tokens) to a project's balance without minting project tokens. Useful for topping
|
|
306
215
|
/// up a project or returning funds. Can also unlock previously held fees by returning them to the project's
|
|
307
216
|
/// balance.
|
|
@@ -369,7 +278,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
369
278
|
override
|
|
370
279
|
returns (uint256 reclaimAmount)
|
|
371
280
|
{
|
|
372
|
-
|
|
281
|
+
// Enforce permissions.
|
|
282
|
+
_requirePermissionFrom({account: holder, projectId: projectId, permissionId: JBPermissionIds.CASH_OUT_TOKENS});
|
|
373
283
|
|
|
374
284
|
reclaimAmount = _cashOutTokensOf({
|
|
375
285
|
holder: holder,
|
|
@@ -457,6 +367,31 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
457
367
|
|
|
458
368
|
// Track the fee-free payout amount. During cashout at zero tax rate, fees apply
|
|
459
369
|
// only up to this accumulated amount, preventing round-trip fee bypass.
|
|
370
|
+
// Revert on any self-referencing payout (the source project paying itself via a split),
|
|
371
|
+
// regardless of which terminal receives the call or which branch (pay vs add-to-balance)
|
|
372
|
+
// is taken. Both shapes are disguised owner actions that the payout pipeline must not
|
|
373
|
+
// silently authorize:
|
|
374
|
+
// - pay branch: the destination terminal's `pay()` mints new project tokens against
|
|
375
|
+
// the project's own surplus, diluting holders out-of-cycle and bypassing the
|
|
376
|
+
// ruleset's `allowOwnerMinting=false` guarantee. This holds even when the
|
|
377
|
+
// destination terminal is a different instance owned by the same project, because
|
|
378
|
+
// every registered terminal can mint via the terminal-as-minter pathway.
|
|
379
|
+
// - addToBalance branch: a same-project add-balance split shuffles surplus between
|
|
380
|
+
// the project's own terminals through the payout pipeline. The same effect is
|
|
381
|
+
// available via `addToBalanceOf` directly without the side effects (locked-split
|
|
382
|
+
// consumption, payout-limit drawdown, fee-free-surplus accounting); routing it
|
|
383
|
+
// through `sendPayoutsOf` is never the right surface.
|
|
384
|
+
// The try-catch in the split group lib catches this revert and restores the balance.
|
|
385
|
+
if (split.projectId == projectId) {
|
|
386
|
+
revert JBMultiTerminal_MintNotAllowed({
|
|
387
|
+
projectId: projectId, splitProjectId: split.projectId, terminal: address(terminal)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Track the fee-free payout amount. During cashout at zero tax rate, fees apply
|
|
392
|
+
// only up to this accumulated amount, preventing round-trip fee bypass. Same-project
|
|
393
|
+
// splits would inflate this counter against the project's own future zero-tax cashouts
|
|
394
|
+
// but are already excluded by the self-reference revert above.
|
|
460
395
|
if (terminal == this) {
|
|
461
396
|
_feeFreeSurplusOf[split.projectId][token] += netPayoutAmount;
|
|
462
397
|
}
|
|
@@ -474,17 +409,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
474
409
|
metadata: metadata
|
|
475
410
|
});
|
|
476
411
|
} else {
|
|
477
|
-
// Revert if this is a self-referencing payout (project paying itself via a split).
|
|
478
|
-
// Same-project pay splits would mint tokens against existing balance without new funds entering.
|
|
479
|
-
// Projects that want to mint should do so explicitly via the controller.
|
|
480
|
-
// Cross-project pay splits on the same terminal are allowed (different project receives the funds).
|
|
481
|
-
// The try-catch in the split group lib catches this revert and restores the balance.
|
|
482
|
-
if (terminal == this && split.projectId == projectId) {
|
|
483
|
-
revert JBMultiTerminal_MintNotAllowed({
|
|
484
|
-
projectId: projectId, splitProjectId: split.projectId, terminal: address(terminal)
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
412
|
// Keep a reference to the beneficiary of the payment.
|
|
489
413
|
address beneficiary = split.beneficiary != address(0) ? split.beneficiary : originalMessageSender;
|
|
490
414
|
|
|
@@ -493,7 +417,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
493
417
|
projectId: split.projectId,
|
|
494
418
|
token: token,
|
|
495
419
|
amount: netPayoutAmount,
|
|
496
|
-
payer: address(this),
|
|
497
420
|
beneficiary: beneficiary,
|
|
498
421
|
metadata: metadata
|
|
499
422
|
});
|
|
@@ -530,7 +453,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
530
453
|
/// @dev Only accepts calls from this terminal itself.
|
|
531
454
|
/// @param projectId The ID of the project paying the fee.
|
|
532
455
|
/// @param token The token the fee is paid in.
|
|
533
|
-
/// @param amount The fee amount, as a fixed point number with
|
|
456
|
+
/// @param amount The fee amount, as a fixed point number with the same number of decimals as the token's
|
|
457
|
+
/// accounting context.
|
|
534
458
|
/// @param beneficiary The address to mint tokens to (from the project which receives fees), and pass along to the
|
|
535
459
|
/// ruleset's data hook and pay hook if applicable.
|
|
536
460
|
/// @param feeTerminal The terminal that'll receive the fees.
|
|
@@ -555,10 +479,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
555
479
|
|
|
556
480
|
_efficientPay({
|
|
557
481
|
terminal: feeTerminal,
|
|
558
|
-
projectId:
|
|
482
|
+
projectId: _FEE_BENEFICIARY_PROJECT_ID,
|
|
559
483
|
token: token,
|
|
560
484
|
amount: amount,
|
|
561
|
-
payer: address(this),
|
|
562
485
|
beneficiary: beneficiary,
|
|
563
486
|
metadata: metadata
|
|
564
487
|
});
|
|
@@ -621,10 +544,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
621
544
|
// Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
|
|
622
545
|
// This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
|
|
623
546
|
uint256 feeAmount;
|
|
624
|
-
if (
|
|
625
|
-
!_isFeeless({addr: address(to), projectId: projectId})
|
|
626
|
-
&& projectId != JBConstants.FEE_BENEFICIARY_PROJECT_ID
|
|
627
|
-
) {
|
|
547
|
+
if (!_isFeeless({addr: address(to), projectId: projectId}) && projectId != _FEE_BENEFICIARY_PROJECT_ID) {
|
|
628
548
|
feeAmount = _takeFeeFrom({
|
|
629
549
|
projectId: projectId,
|
|
630
550
|
token: token,
|
|
@@ -706,78 +626,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
706
626
|
_checkMin({value: beneficiaryTokenCount, min: minReturnedTokens});
|
|
707
627
|
}
|
|
708
628
|
|
|
709
|
-
/// @notice Atomically cash out `holder`'s tokens of `projectId` and pay the reclaim into `beneficiaryProjectId`.
|
|
710
|
-
/// @dev Equivalent to calling `cashOutTokensOf` followed by `pay` on the destination project, except the
|
|
711
|
-
/// source-side cash out fee is skipped. The equivalent fee is bound on the destination project's side instead:
|
|
712
|
-
/// `_feeFreeSurplusOf[beneficiaryProjectId]` is credited by the first of the destination project's accounting
|
|
713
|
-
/// contexts on this terminal whose balance grows during the routing.
|
|
714
|
-
/// @dev The destination terminal is `DIRECTORY.primaryTerminalOf(beneficiaryProjectId, tokenToReclaim)` —
|
|
715
|
-
/// which may itself be a router that swaps before paying the destination.
|
|
716
|
-
/// @dev The destination project's current ruleset can set `pauseCrossProjectFeeFreeInflows` to opt out — the
|
|
717
|
-
/// call then reverts. If no delivery to the destination project lands on this terminal under any of the
|
|
718
|
-
/// destination project's accounting contexts, the call also reverts so the source-side fee skip never becomes
|
|
719
|
-
/// a leak.
|
|
720
|
-
/// @param holder The account whose project tokens are being burned.
|
|
721
|
-
/// @param projectId The ID of the source project being cashed out from.
|
|
722
|
-
/// @param cashOutCount The number of source-project tokens to burn, as a fixed point number with 18 decimals.
|
|
723
|
-
/// @param tokenToReclaim The terminal token reclaimed from the source project's surplus.
|
|
724
|
-
/// @param beneficiaryProjectId The destination project receiving the reclaim.
|
|
725
|
-
/// @param beneficiary The address that receives the newly minted destination-project tokens.
|
|
726
|
-
/// @param minTokensOut The minimum number of destination-project tokens that must be minted, otherwise revert.
|
|
727
|
-
/// @param cashOutMetadata Bytes forwarded to the source project's data hook and any cashout hook specifications.
|
|
728
|
-
/// @param payMetadata Bytes forwarded to the destination project's pay flow.
|
|
729
|
-
/// @return reclaimAmount The gross reclaim amount returned by the store.
|
|
730
|
-
/// @return beneficiaryTokenCount The number of destination-project tokens minted to `beneficiary`.
|
|
731
|
-
function payAfterCashOutTokensOf(
|
|
732
|
-
address holder,
|
|
733
|
-
uint256 projectId,
|
|
734
|
-
uint256 cashOutCount,
|
|
735
|
-
address tokenToReclaim,
|
|
736
|
-
uint256 beneficiaryProjectId,
|
|
737
|
-
address beneficiary,
|
|
738
|
-
uint256 minTokensOut,
|
|
739
|
-
bytes calldata cashOutMetadata,
|
|
740
|
-
bytes calldata payMetadata
|
|
741
|
-
)
|
|
742
|
-
external
|
|
743
|
-
override
|
|
744
|
-
returns (uint256 reclaimAmount, uint256 beneficiaryTokenCount)
|
|
745
|
-
{
|
|
746
|
-
// Caller must hold (or be operator with) `CASH_OUT_TOKENS` permission on the source `holder`/`projectId`.
|
|
747
|
-
// Burning A's tokens is the load-bearing side effect — gating it stays at the same authority bar as the
|
|
748
|
-
// direct `cashOutTokensOf` entrypoint.
|
|
749
|
-
_requireCashOutPermissionFrom({holder: holder, projectId: projectId});
|
|
750
|
-
|
|
751
|
-
// Destination opt-out check: B's current ruleset can set `pauseCrossProjectFeeFreeInflows = true` to
|
|
752
|
-
// refuse cross-project fee-free credits. Without this gate, anyone holding A's tokens could push a
|
|
753
|
-
// deferred-fee credit onto `_feeFreeSurplusOf[B]` without B consenting.
|
|
754
|
-
_requireBeneficiaryAcceptsFeeFreeInflows(beneficiaryProjectId);
|
|
755
|
-
|
|
756
|
-
// Burn source-project tokens, run cashout-side hooks, take any hook fees, and cap source fee-free.
|
|
757
|
-
reclaimAmount = _executeCrossProjectCashOut({
|
|
758
|
-
holder: holder,
|
|
759
|
-
projectId: projectId,
|
|
760
|
-
cashOutCount: cashOutCount,
|
|
761
|
-
tokenToReclaim: tokenToReclaim,
|
|
762
|
-
beneficiary: beneficiary,
|
|
763
|
-
cashOutMetadata: cashOutMetadata
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
// Nothing to route if the data hook returned zero reclaim.
|
|
767
|
-
if (reclaimAmount != 0) {
|
|
768
|
-
beneficiaryTokenCount = _routeReclaimToBeneficiaryProject({
|
|
769
|
-
tokenToReclaim: tokenToReclaim,
|
|
770
|
-
reclaimAmount: reclaimAmount,
|
|
771
|
-
beneficiaryProjectId: beneficiaryProjectId,
|
|
772
|
-
beneficiary: beneficiary,
|
|
773
|
-
payMetadata: payMetadata
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Mint floor: how many destination-project tokens were issued for the inbound pay.
|
|
778
|
-
_checkMin({value: beneficiaryTokenCount, min: minTokensOut});
|
|
779
|
-
}
|
|
780
|
-
|
|
781
629
|
/// @notice Processes held fees for a project, sending them to the protocol's fee project. Fees are held for 28 days
|
|
782
630
|
/// after a payout — processing them finalizes the fee payment.
|
|
783
631
|
/// @dev Only processes fees whose `unlockTimestamp` has passed. Stops early if it encounters a still-locked fee.
|
|
@@ -789,15 +637,59 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
789
637
|
/// @param token The token to process held fees for.
|
|
790
638
|
/// @param count The number of fees to process.
|
|
791
639
|
function processHeldFeesOf(uint256 projectId, address token, uint256 count) external override {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
640
|
+
// Keep a reference to the terminal that'll receive the fees.
|
|
641
|
+
IJBTerminal feeTerminal = _primaryTerminalOf({projectId: _FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
642
|
+
|
|
643
|
+
// Process each fee. Re-read the index and array length from storage each iteration to account for reentrant
|
|
644
|
+
// calls that may have already advanced the index or cleaned up the array.
|
|
645
|
+
for (uint256 i; i < count;) {
|
|
646
|
+
// Read the current index from storage (not a cached value) to prevent reentrancy from
|
|
647
|
+
// causing double-processing.
|
|
648
|
+
uint256 currentIndex = _nextHeldFeeIndexOf[projectId][token];
|
|
649
|
+
|
|
650
|
+
// If all fees have been processed, break to cleanup.
|
|
651
|
+
if (currentIndex >= _heldFeesOf[projectId][token].length) break;
|
|
652
|
+
|
|
653
|
+
// Keep a reference to the held fee being iterated on.
|
|
654
|
+
JBFee memory heldFee = _heldFeesOf[projectId][token][currentIndex];
|
|
655
|
+
|
|
656
|
+
// Can't process fees that aren't yet unlocked. Fees unlock sequentially in the array, so nothing left to do
|
|
657
|
+
// if the current fee isn't yet unlocked.
|
|
658
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
659
|
+
if (heldFee.unlockTimestamp > block.timestamp) break;
|
|
660
|
+
|
|
661
|
+
// Delete the entry and advance the index *before* the external call. This is intentional:
|
|
662
|
+
// 1. It prevents reentrancy from reprocessing the same fee.
|
|
663
|
+
// 2. If `_processFee` fails (try-catch), the fee amount is returned to the project's balance via
|
|
664
|
+
// `_recordAddedBalanceFor` — the fee is forgiven rather than retried. This is a deliberate design
|
|
665
|
+
// choice: projects should not have funds permanently stuck because the fee route is misconfigured or
|
|
666
|
+
// reverting.
|
|
667
|
+
// A `FeeReverted` event is emitted so the forgiveness is observable off-chain.
|
|
668
|
+
delete _heldFeesOf[projectId][token][currentIndex];
|
|
669
|
+
_nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
|
|
670
|
+
|
|
671
|
+
// Process the fee.
|
|
672
|
+
_processFee({
|
|
673
|
+
projectId: projectId,
|
|
674
|
+
token: token,
|
|
675
|
+
amount: _feeAmountFrom(heldFee.amount),
|
|
676
|
+
beneficiary: heldFee.beneficiary,
|
|
677
|
+
feeTerminal: feeTerminal,
|
|
678
|
+
wasHeld: true
|
|
679
|
+
});
|
|
680
|
+
unchecked {
|
|
681
|
+
++i;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// If all held fees have been processed, reset the array and index entirely to bound storage growth.
|
|
686
|
+
if (
|
|
687
|
+
_nextHeldFeeIndexOf[projectId][token] >= _heldFeesOf[projectId][token].length
|
|
688
|
+
&& _heldFeesOf[projectId][token].length > 0
|
|
689
|
+
) {
|
|
690
|
+
delete _heldFeesOf[projectId][token];
|
|
691
|
+
delete _nextHeldFeeIndexOf[projectId][token];
|
|
692
|
+
}
|
|
801
693
|
}
|
|
802
694
|
|
|
803
695
|
/// @notice Distributes funds from a project's balance to its payout split recipients, up to the current ruleset's
|
|
@@ -1268,15 +1160,25 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1268
1160
|
// Cache whether the beneficiary is feeless.
|
|
1269
1161
|
bool beneficiaryIsFeeless = _isFeeless({addr: beneficiary, projectId: projectId});
|
|
1270
1162
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1163
|
+
{
|
|
1164
|
+
// Cache the controller to avoid a redundant external call (also used inside STORE.recordCashOutFor).
|
|
1165
|
+
IJBController controller = _controllerOf(projectId);
|
|
1166
|
+
|
|
1167
|
+
// Record the cash out.
|
|
1168
|
+
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
|
|
1169
|
+
holder: holder,
|
|
1170
|
+
projectId: projectId,
|
|
1171
|
+
cashOutCount: cashOutCount,
|
|
1172
|
+
tokenToReclaim: tokenToReclaim,
|
|
1173
|
+
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
1174
|
+
metadata: metadata
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// Burn the project tokens.
|
|
1178
|
+
if (cashOutCount != 0) {
|
|
1179
|
+
controller.burnTokensOf({holder: holder, projectId: projectId, tokenCount: cashOutCount, memo: ""});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1280
1182
|
|
|
1281
1183
|
// Keep a reference to the amount being reclaimed that is subject to fees.
|
|
1282
1184
|
uint256 amountEligibleForFees;
|
|
@@ -1313,24 +1215,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1313
1215
|
}
|
|
1314
1216
|
}
|
|
1315
1217
|
|
|
1316
|
-
// If the data hook returned cash out hook specifications, fulfill them.
|
|
1317
|
-
// field-by-field rather than as a struct literal so each assignment uses only one stack slot at a
|
|
1318
|
-
// time, keeping the call site under solc 0.8.28's non-via-ir Yul stack ceiling (a 10-field struct
|
|
1319
|
-
// literal would otherwise trip it from inside `_cashOutTokensOf`).
|
|
1218
|
+
// If the data hook returned cash out hook specifications, fulfill them.
|
|
1320
1219
|
if (hookSpecifications.length != 0) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1220
|
+
// Fulfill the cash out hook specifications.
|
|
1221
|
+
amountEligibleForFees += _fulfillCashOutHookSpecificationsFor({
|
|
1222
|
+
projectId: projectId,
|
|
1223
|
+
holder: holder,
|
|
1224
|
+
cashOutCount: cashOutCount,
|
|
1225
|
+
ruleset: ruleset,
|
|
1226
|
+
cashOutTaxRate: cashOutTaxRate,
|
|
1227
|
+
beneficiary: beneficiary,
|
|
1228
|
+
beneficiaryReclaimAmount: _tokenAmountOf({
|
|
1229
|
+
projectId: projectId, token: tokenToReclaim, value: reclaimAmount
|
|
1230
|
+
}),
|
|
1231
|
+
specifications: hookSpecifications,
|
|
1232
|
+
metadata: metadata
|
|
1233
|
+
});
|
|
1334
1234
|
}
|
|
1335
1235
|
|
|
1336
1236
|
// Cap fee-free surplus at remaining balance.
|
|
@@ -1374,62 +1274,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1374
1274
|
if (value < min) revert JBMultiTerminal_UnderMin(value, min);
|
|
1375
1275
|
}
|
|
1376
1276
|
|
|
1377
|
-
/// @notice Find the first of B's accounting contexts whose balance grew during the routing, credit
|
|
1378
|
-
/// `_feeFreeSurplusOf[beneficiaryProjectId][token]` by the delta, cap to remaining balance, and return.
|
|
1379
|
-
/// Reverts with `JBMultiTerminal_BeneficiaryProjectNotPaid` if no context grew — without delivery, the
|
|
1380
|
-
/// skipped source-side fee can't be bound and would leak.
|
|
1381
|
-
/// @dev Shared by `_routeReclaimToBeneficiaryProject` and `_routeReclaimAsAddToBalance`. The "first
|
|
1382
|
-
/// growing context wins" rule matches a well-behaved router that picks one of B's tokens (the post-swap
|
|
1383
|
-
/// token) and deposits into that single bucket.
|
|
1384
|
-
/// @param beneficiaryProjectId The destination project.
|
|
1385
|
-
/// @param contexts Accounting contexts captured before the routing.
|
|
1386
|
-
/// @param balancesBefore Pre-routing balances aligned to `contexts` by index.
|
|
1387
|
-
function _creditFirstGrowingBeneficiaryContext(
|
|
1388
|
-
uint256 beneficiaryProjectId,
|
|
1389
|
-
JBAccountingContext[] memory contexts,
|
|
1390
|
-
uint256[] memory balancesBefore
|
|
1391
|
-
)
|
|
1392
|
-
internal
|
|
1393
|
-
{
|
|
1394
|
-
// Walk B's accounting contexts in declared order. The first context whose balance grew is treated as
|
|
1395
|
-
// the bucket the router chose to deposit into (typically the post-swap token in a cross-token route).
|
|
1396
|
-
// We bind the deferred source-side fee to that bucket and stop — subsequent grown contexts are ignored
|
|
1397
|
-
// by design, capping the credit at one delivery delta even if a misbehaving router somehow split the
|
|
1398
|
-
// deposit across multiple buckets.
|
|
1399
|
-
for (uint256 i; i < contexts.length;) {
|
|
1400
|
-
// Read B's post-routing balance for this context's token. Compared against the pre-routing snapshot
|
|
1401
|
-
// captured by `_snapshotBeneficiaryContextBalances` to detect the delivery delta.
|
|
1402
|
-
uint256 balanceAfter =
|
|
1403
|
-
STORE.balanceOf({terminal: address(this), projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
1404
|
-
|
|
1405
|
-
if (balanceAfter > balancesBefore[i]) {
|
|
1406
|
-
// Credit the delivery delta into B's fee-free counter for this token. `unchecked` is safe:
|
|
1407
|
-
// `balanceAfter > balancesBefore[i]` is the loop condition, so the subtraction can't underflow,
|
|
1408
|
-
// and the addition can't overflow before the underlying balance does (terminal balance is the
|
|
1409
|
-
// upper bound on any cumulative credit — see the cap call below).
|
|
1410
|
-
unchecked {
|
|
1411
|
-
_feeFreeSurplusOf[beneficiaryProjectId][contexts[i].token] += balanceAfter - balancesBefore[i];
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Cap the credit at B's current balance for this token. If pay/addToBalance hooks pulled funds
|
|
1415
|
-
// back out during routing, the post-balance read inside `_capFeeFreeSurplus` clamps the
|
|
1416
|
-
// counter so it never exceeds what's actually sitting in B's bucket. Without this, B's later
|
|
1417
|
-
// zero-tax cashouts would over-fee phantom amounts.
|
|
1418
|
-
_capFeeFreeSurplus({projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
unchecked {
|
|
1423
|
-
++i;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// No context grew. Either the destination terminal silently dropped the funds or routed them
|
|
1428
|
-
// elsewhere (e.g. to a different terminal not registered as B's primary). Revert the entire
|
|
1429
|
-
// cross-project flow so the source-side fee skip never becomes a leak — A's burn is undone too.
|
|
1430
|
-
revert JBMultiTerminal_BeneficiaryProjectNotPaid(beneficiaryProjectId);
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
1277
|
/// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
|
|
1434
1278
|
/// recipient terminal's `addToBalance` function.
|
|
1435
1279
|
/// @param terminal The terminal on which the project is expecting to receive funds.
|
|
@@ -1479,134 +1323,40 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1479
1323
|
uint256 projectId,
|
|
1480
1324
|
address token,
|
|
1481
1325
|
uint256 amount,
|
|
1482
|
-
address payer,
|
|
1483
1326
|
address beneficiary,
|
|
1484
1327
|
bytes memory metadata
|
|
1485
1328
|
)
|
|
1486
1329
|
internal
|
|
1487
|
-
returns (uint256 newlyIssuedTokenCount)
|
|
1488
1330
|
{
|
|
1489
1331
|
if (terminal == IJBTerminal(address(this))) {
|
|
1490
|
-
|
|
1332
|
+
_pay({
|
|
1491
1333
|
projectId: projectId,
|
|
1492
1334
|
token: token,
|
|
1493
1335
|
amount: amount,
|
|
1494
|
-
payer:
|
|
1336
|
+
payer: address(this),
|
|
1495
1337
|
beneficiary: beneficiary,
|
|
1496
1338
|
memo: "",
|
|
1497
1339
|
metadata: metadata
|
|
1498
1340
|
});
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1504
|
-
|
|
1505
|
-
newlyIssuedTokenCount = terminal.pay{value: payValue}({
|
|
1506
|
-
projectId: projectId,
|
|
1507
|
-
token: token,
|
|
1508
|
-
amount: amount,
|
|
1509
|
-
beneficiary: beneficiary,
|
|
1510
|
-
minReturnedTokens: 0,
|
|
1511
|
-
memo: "",
|
|
1512
|
-
metadata: metadata
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
_afterTransferTo({to: address(terminal), token: token});
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
/// @notice Shared cashout-prep step for both cross-project entrypoints (`_payAfterCashOutTokensOf` and
|
|
1519
|
-
/// `_addToBalanceAfterCashOutTokensOf`). Records the cashout with `beneficiaryIsFeeless: true`, burns the
|
|
1520
|
-
/// holder's project tokens, runs cashout-side hook specs, caps the source's fee-free surplus, and takes
|
|
1521
|
-
/// hook fees. Returns the gross reclaim amount that the caller must then route to the destination project.
|
|
1522
|
-
/// @dev `beneficiary` is recorded in the `CashOutTokens` event and credited any fee-project tokens minted
|
|
1523
|
-
/// from hook fees. For pay it's the user-supplied destination beneficiary; for addToBalance the caller
|
|
1524
|
-
/// passes `_msgSender()` (no separate recipient exists).
|
|
1525
|
-
/// @param holder The account whose source-project tokens are being burned.
|
|
1526
|
-
/// @param projectId The ID of the source project being cashed out from.
|
|
1527
|
-
/// @param cashOutCount The number of source-project tokens to burn.
|
|
1528
|
-
/// @param tokenToReclaim The terminal token reclaimed from the source project's surplus.
|
|
1529
|
-
/// @param beneficiary The address recorded in the event slot and credited any hook-fee project tokens.
|
|
1530
|
-
/// @param cashOutMetadata Bytes forwarded to the source project's data hook and any cashout hook specs.
|
|
1531
|
-
/// @return reclaimAmount The gross reclaim amount returned by the store.
|
|
1532
|
-
function _executeCrossProjectCashOut(
|
|
1533
|
-
address holder,
|
|
1534
|
-
uint256 projectId,
|
|
1535
|
-
uint256 cashOutCount,
|
|
1536
|
-
address tokenToReclaim,
|
|
1537
|
-
address beneficiary,
|
|
1538
|
-
bytes memory cashOutMetadata
|
|
1539
|
-
)
|
|
1540
|
-
internal
|
|
1541
|
-
returns (uint256 reclaimAmount)
|
|
1542
|
-
{
|
|
1543
|
-
// Record the cash out and burn the project tokens. `beneficiaryIsFeeless: true` — the equivalent fee
|
|
1544
|
-
// is bound on the destination side via the `_feeFreeSurplusOf[beneficiaryProjectId]` credit computed
|
|
1545
|
-
// from the delivery delta in the routing step. The external entrypoint reverts if delivery falls short,
|
|
1546
|
-
// so this can never become a leak.
|
|
1547
|
-
(
|
|
1548
|
-
JBRuleset memory ruleset,
|
|
1549
|
-
uint256 _reclaim,
|
|
1550
|
-
uint256 cashOutTaxRate,
|
|
1551
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
1552
|
-
) = _recordAndBurnCashOut({
|
|
1553
|
-
holder: holder,
|
|
1554
|
-
projectId: projectId,
|
|
1555
|
-
cashOutCount: cashOutCount,
|
|
1556
|
-
tokenToReclaim: tokenToReclaim,
|
|
1557
|
-
beneficiaryIsFeeless: true,
|
|
1558
|
-
metadata: cashOutMetadata
|
|
1559
|
-
});
|
|
1560
|
-
reclaimAmount = _reclaim;
|
|
1561
|
-
|
|
1562
|
-
emit CashOutTokens({
|
|
1563
|
-
rulesetId: ruleset.id,
|
|
1564
|
-
rulesetCycleNumber: ruleset.cycleNumber,
|
|
1565
|
-
projectId: projectId,
|
|
1566
|
-
holder: holder,
|
|
1567
|
-
beneficiary: beneficiary,
|
|
1568
|
-
cashOutCount: cashOutCount,
|
|
1569
|
-
cashOutTaxRate: cashOutTaxRate,
|
|
1570
|
-
reclaimAmount: reclaimAmount,
|
|
1571
|
-
metadata: cashOutMetadata,
|
|
1572
|
-
caller: _msgSender()
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
// Only hook-spec amounts are fee-eligible here; the destination portion is intentionally feeless.
|
|
1576
|
-
uint256 amountEligibleForFees;
|
|
1577
|
-
|
|
1578
|
-
// Hook fees still apply (those funds leave the protocol to external hooks). Hook context sees
|
|
1579
|
-
// `address(this)` as the beneficiary since the terminal is custodying the reclaim mid-flow.
|
|
1580
|
-
if (hookSpecifications.length != 0) {
|
|
1581
|
-
JBTokenAmount memory reclaimTokenAmount =
|
|
1582
|
-
_tokenAmountOf({projectId: projectId, token: tokenToReclaim, value: reclaimAmount});
|
|
1583
|
-
JBAfterCashOutRecordedContext memory ctx;
|
|
1584
|
-
ctx.holder = holder;
|
|
1585
|
-
ctx.projectId = projectId;
|
|
1586
|
-
ctx.rulesetId = ruleset.id;
|
|
1587
|
-
ctx.cashOutCount = cashOutCount;
|
|
1588
|
-
ctx.reclaimedAmount = reclaimTokenAmount;
|
|
1589
|
-
ctx.forwardedAmount = reclaimTokenAmount;
|
|
1590
|
-
ctx.cashOutTaxRate = cashOutTaxRate;
|
|
1591
|
-
ctx.beneficiary = payable(address(this));
|
|
1592
|
-
ctx.cashOutMetadata = cashOutMetadata;
|
|
1593
|
-
amountEligibleForFees = JBCashOutHookSpecsLib.fulfill(FEELESS_ADDRESSES, ctx, hookSpecifications);
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// Cap the source project's fee-free surplus at remaining balance after the outflow. Same invariant as
|
|
1597
|
-
// `_cashOutTokensOf`: every cashout path keeps `_feeFreeSurplusOf[projectId]` consistent with the
|
|
1598
|
-
// post-outflow balance so later zero-tax cashouts from A don't fee phantom amounts.
|
|
1599
|
-
_capFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
|
|
1341
|
+
} else {
|
|
1342
|
+
// Trigger any inherited pre-transfer logic.
|
|
1343
|
+
// Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
1344
|
+
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1600
1345
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1346
|
+
// Send the fee.
|
|
1347
|
+
// If this terminal's token is ETH, send it in msg.value.
|
|
1348
|
+
terminal.pay{value: payValue}({
|
|
1604
1349
|
projectId: projectId,
|
|
1605
|
-
token:
|
|
1606
|
-
amount:
|
|
1350
|
+
token: token,
|
|
1351
|
+
amount: amount,
|
|
1607
1352
|
beneficiary: beneficiary,
|
|
1608
|
-
|
|
1353
|
+
minReturnedTokens: 0,
|
|
1354
|
+
memo: "",
|
|
1355
|
+
metadata: metadata
|
|
1609
1356
|
});
|
|
1357
|
+
|
|
1358
|
+
// Revoke the temporary pull allowance now that the recipient terminal call has finished.
|
|
1359
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1610
1360
|
}
|
|
1611
1361
|
}
|
|
1612
1362
|
|
|
@@ -1644,6 +1394,106 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1644
1394
|
_afterTransferTo({to: address(terminal), token: token});
|
|
1645
1395
|
}
|
|
1646
1396
|
|
|
1397
|
+
/// @notice Fulfills a list of cash out hook specifications.
|
|
1398
|
+
/// @param projectId The ID of the project to cash out from.
|
|
1399
|
+
/// @param beneficiaryReclaimAmount The number of tokens to cash out from the project.
|
|
1400
|
+
/// @param holder The address holding the tokens to cash out.
|
|
1401
|
+
/// @param cashOutCount The number of tokens to cash out.
|
|
1402
|
+
/// @param metadata Bytes to send along to the emitted event and cash out hooks as applicable.
|
|
1403
|
+
/// @param ruleset The ruleset active during this cash out as a `JBRuleset` struct.
|
|
1404
|
+
/// @param cashOutTaxRate The cash out tax rate influencing the reclaim amount, out of
|
|
1405
|
+
/// `JBConstants.MAX_CASH_OUT_TAX_RATE`. @param beneficiary The address which will receive any terminal tokens that
|
|
1406
|
+
/// are cashed out.
|
|
1407
|
+
/// @param specifications The hook specifications to fulfill.
|
|
1408
|
+
/// @return amountEligibleForFees The amount of funds which were allocated to cash out hooks and are eligible for
|
|
1409
|
+
/// fees.
|
|
1410
|
+
function _fulfillCashOutHookSpecificationsFor(
|
|
1411
|
+
uint256 projectId,
|
|
1412
|
+
JBTokenAmount memory beneficiaryReclaimAmount,
|
|
1413
|
+
address holder,
|
|
1414
|
+
uint256 cashOutCount,
|
|
1415
|
+
bytes memory metadata,
|
|
1416
|
+
JBRuleset memory ruleset,
|
|
1417
|
+
uint256 cashOutTaxRate,
|
|
1418
|
+
address payable beneficiary,
|
|
1419
|
+
JBCashOutHookSpecification[] memory specifications
|
|
1420
|
+
)
|
|
1421
|
+
internal
|
|
1422
|
+
returns (uint256 amountEligibleForFees)
|
|
1423
|
+
{
|
|
1424
|
+
// Keep a reference to cash out context for the cash out hooks.
|
|
1425
|
+
JBAfterCashOutRecordedContext memory context = JBAfterCashOutRecordedContext({
|
|
1426
|
+
holder: holder,
|
|
1427
|
+
projectId: projectId,
|
|
1428
|
+
rulesetId: ruleset.id,
|
|
1429
|
+
cashOutCount: cashOutCount,
|
|
1430
|
+
reclaimedAmount: beneficiaryReclaimAmount,
|
|
1431
|
+
forwardedAmount: beneficiaryReclaimAmount,
|
|
1432
|
+
cashOutTaxRate: cashOutTaxRate,
|
|
1433
|
+
beneficiary: beneficiary,
|
|
1434
|
+
hookMetadata: "",
|
|
1435
|
+
cashOutMetadata: metadata
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
for (uint256 i; i < specifications.length;) {
|
|
1439
|
+
// Set the specification being iterated on.
|
|
1440
|
+
JBCashOutHookSpecification memory specification = specifications[i];
|
|
1441
|
+
|
|
1442
|
+
// A noop specification is informational only and doesn't trigger the hook.
|
|
1443
|
+
if (specification.noop) {
|
|
1444
|
+
unchecked {
|
|
1445
|
+
++i;
|
|
1446
|
+
}
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Get the fee for the specified amount.
|
|
1451
|
+
uint256 specificationAmountFee = _isFeeless({addr: address(specification.hook), projectId: projectId})
|
|
1452
|
+
? 0
|
|
1453
|
+
: _feeAmountFrom(specification.amount);
|
|
1454
|
+
|
|
1455
|
+
// Add the specification's amount to the amount eligible for fees.
|
|
1456
|
+
if (specificationAmountFee != 0) {
|
|
1457
|
+
amountEligibleForFees += specification.amount;
|
|
1458
|
+
specification.amount -= specificationAmountFee;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Pass the correct token `forwardedAmount` to the hook.
|
|
1462
|
+
context.forwardedAmount = JBTokenAmount({
|
|
1463
|
+
value: specification.amount,
|
|
1464
|
+
token: beneficiaryReclaimAmount.token,
|
|
1465
|
+
decimals: beneficiaryReclaimAmount.decimals,
|
|
1466
|
+
currency: beneficiaryReclaimAmount.currency
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Pass the correct metadata from the data hook's specification.
|
|
1470
|
+
context.hookMetadata = specification.metadata;
|
|
1471
|
+
|
|
1472
|
+
// Trigger any inherited pre-transfer logic.
|
|
1473
|
+
// Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
1474
|
+
uint256 payValue = _beforeTransferTo({
|
|
1475
|
+
to: address(specification.hook), token: beneficiaryReclaimAmount.token, amount: specification.amount
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// Fulfill the specification.
|
|
1479
|
+
specification.hook.afterCashOutRecordedWith{value: payValue}(context);
|
|
1480
|
+
|
|
1481
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
1482
|
+
_afterTransferTo({to: address(specification.hook), token: beneficiaryReclaimAmount.token});
|
|
1483
|
+
|
|
1484
|
+
emit HookAfterRecordCashOut({
|
|
1485
|
+
hook: specification.hook,
|
|
1486
|
+
context: context,
|
|
1487
|
+
specificationAmount: specification.amount,
|
|
1488
|
+
fee: specificationAmountFee,
|
|
1489
|
+
caller: _msgSender()
|
|
1490
|
+
});
|
|
1491
|
+
unchecked {
|
|
1492
|
+
++i;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1647
1497
|
/// @notice Fulfills a list of pay hook specifications.
|
|
1648
1498
|
/// @param projectId The ID of the project to pay.
|
|
1649
1499
|
/// @param specifications The pay hook specifications to be fulfilled.
|
|
@@ -1739,7 +1589,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1739
1589
|
/// applicable.
|
|
1740
1590
|
/// @param memo A memo to pass along to the emitted event.
|
|
1741
1591
|
/// @param metadata Bytes to send along to the emitted event, as well as the data hook and pay hook if applicable.
|
|
1742
|
-
/// @return newlyIssuedTokenCount The number of project tokens minted to `beneficiary` as a result of this payment.
|
|
1743
1592
|
function _pay(
|
|
1744
1593
|
uint256 projectId,
|
|
1745
1594
|
address token,
|
|
@@ -1750,7 +1599,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1750
1599
|
bytes memory metadata
|
|
1751
1600
|
)
|
|
1752
1601
|
internal
|
|
1753
|
-
returns (uint256 newlyIssuedTokenCount)
|
|
1754
1602
|
{
|
|
1755
1603
|
// Keep a reference to the token amount to forward to the store.
|
|
1756
1604
|
JBTokenAmount memory tokenAmount = _tokenAmountOf({projectId: projectId, token: token, value: amount});
|
|
@@ -1763,6 +1611,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1763
1611
|
payer: payer, amount: tokenAmount, projectId: projectId, beneficiary: beneficiary, metadata: metadata
|
|
1764
1612
|
});
|
|
1765
1613
|
|
|
1614
|
+
// Keep a reference to the number of tokens issued for the beneficiary.
|
|
1615
|
+
uint256 newlyIssuedTokenCount;
|
|
1616
|
+
|
|
1766
1617
|
// Mint tokens if needed.
|
|
1767
1618
|
if (tokenCount != 0) {
|
|
1768
1619
|
// Set the token count to be the number of tokens minted for the beneficiary instead of the total
|
|
@@ -1822,96 +1673,45 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1822
1673
|
)
|
|
1823
1674
|
internal
|
|
1824
1675
|
{
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1676
|
+
try this.executeProcessFee({
|
|
1677
|
+
projectId: projectId, token: token, amount: amount, beneficiary: beneficiary, feeTerminal: feeTerminal
|
|
1678
|
+
}) {
|
|
1679
|
+
emit ProcessFee({
|
|
1680
|
+
projectId: projectId,
|
|
1681
|
+
token: token,
|
|
1682
|
+
amount: amount,
|
|
1683
|
+
wasHeld: wasHeld,
|
|
1684
|
+
beneficiary: beneficiary,
|
|
1685
|
+
caller: _msgSender()
|
|
1686
|
+
});
|
|
1687
|
+
} catch (bytes memory reason) {
|
|
1688
|
+
// Fee processing failed — intentionally forgive the fee and return the amount to the project.
|
|
1689
|
+
// The held-fee entry (if any) was already deleted by `processHeldFeesOf` before this call, so there is no
|
|
1690
|
+
// retry path. This is by design: a broken or misconfigured fee route should not permanently lock project
|
|
1691
|
+
// funds. The `FeeReverted` event makes this observable off-chain.
|
|
1692
|
+
emit FeeReverted({
|
|
1693
|
+
projectId: projectId,
|
|
1694
|
+
token: token,
|
|
1695
|
+
feeProjectId: _FEE_BENEFICIARY_PROJECT_ID,
|
|
1696
|
+
amount: amount,
|
|
1697
|
+
reason: reason,
|
|
1698
|
+
caller: _msgSender()
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
_recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
|
|
1702
|
+
}
|
|
1834
1703
|
}
|
|
1835
1704
|
|
|
1836
1705
|
/// @notice Records an added balance for a project.
|
|
1837
1706
|
/// @param projectId The ID of the project to record the added balance for.
|
|
1838
1707
|
/// @param token The token to record the added balance for.
|
|
1839
1708
|
/// @param amount The amount of the token to record, as a fixed point number with the same number of decimals as
|
|
1840
|
-
/// this
|
|
1841
|
-
/// terminal.
|
|
1709
|
+
/// this terminal.
|
|
1842
1710
|
function _recordAddedBalanceFor(uint256 projectId, address token, uint256 amount) internal {
|
|
1843
1711
|
STORE.recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
|
|
1844
1712
|
}
|
|
1845
1713
|
|
|
1846
|
-
/// @notice
|
|
1847
|
-
/// @dev Shared between `_cashOutTokensOf` and `_payAfterCashOutTokensOf`. The two flows differ in what
|
|
1848
|
-
/// happens AFTER the burn (where the reclaim goes, how fees are taken), but the record-and-burn step is
|
|
1849
|
-
/// identical.
|
|
1850
|
-
/// @param holder The account whose project tokens are being burned.
|
|
1851
|
-
/// @param projectId The ID of the project the project tokens belong to.
|
|
1852
|
-
/// @param cashOutCount The number of project tokens to burn.
|
|
1853
|
-
/// @param tokenToReclaim The terminal token to reclaim from the project's surplus.
|
|
1854
|
-
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address (passed through
|
|
1855
|
-
/// to the data hook context via the store).
|
|
1856
|
-
/// @param metadata Bytes to send along to the data hook.
|
|
1857
|
-
/// @return ruleset The ruleset the cash out is being made during.
|
|
1858
|
-
/// @return reclaimAmount The amount of terminal tokens to be reclaimed.
|
|
1859
|
-
/// @return cashOutTaxRate The cash out tax rate being used.
|
|
1860
|
-
/// @return hookSpecifications The cash out hook specifications returned by the data hook.
|
|
1861
|
-
function _recordAndBurnCashOut(
|
|
1862
|
-
address holder,
|
|
1863
|
-
uint256 projectId,
|
|
1864
|
-
uint256 cashOutCount,
|
|
1865
|
-
address tokenToReclaim,
|
|
1866
|
-
bool beneficiaryIsFeeless,
|
|
1867
|
-
bytes memory metadata
|
|
1868
|
-
)
|
|
1869
|
-
internal
|
|
1870
|
-
returns (
|
|
1871
|
-
JBRuleset memory ruleset,
|
|
1872
|
-
uint256 reclaimAmount,
|
|
1873
|
-
uint256 cashOutTaxRate,
|
|
1874
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
1875
|
-
)
|
|
1876
|
-
{
|
|
1877
|
-
// Cache the controller to avoid a redundant external call (also used inside STORE.recordCashOutFor).
|
|
1878
|
-
IJBController controller = _controllerOf(projectId);
|
|
1879
|
-
|
|
1880
|
-
// Record the cash out.
|
|
1881
|
-
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
|
|
1882
|
-
holder: holder,
|
|
1883
|
-
projectId: projectId,
|
|
1884
|
-
cashOutCount: cashOutCount,
|
|
1885
|
-
tokenToReclaim: tokenToReclaim,
|
|
1886
|
-
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
1887
|
-
metadata: metadata
|
|
1888
|
-
});
|
|
1889
|
-
|
|
1890
|
-
// Burn the project tokens.
|
|
1891
|
-
if (cashOutCount != 0) {
|
|
1892
|
-
controller.burnTokensOf({holder: holder, projectId: projectId, tokenCount: cashOutCount, memo: ""});
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
/// @notice Resolve B's primary terminal for the reclaim token, reverting if the directory has no entry.
|
|
1897
|
-
/// @dev Shared by `_routeReclaimToBeneficiaryProject` and `_routeReclaimAsAddToBalance`.
|
|
1898
|
-
function _resolveBeneficiaryTerminal(
|
|
1899
|
-
uint256 beneficiaryProjectId,
|
|
1900
|
-
address tokenToReclaim
|
|
1901
|
-
)
|
|
1902
|
-
internal
|
|
1903
|
-
view
|
|
1904
|
-
returns (IJBTerminal destinationTerminal)
|
|
1905
|
-
{
|
|
1906
|
-
destinationTerminal = DIRECTORY.primaryTerminalOf({projectId: beneficiaryProjectId, token: tokenToReclaim});
|
|
1907
|
-
if (address(destinationTerminal) == address(0)) {
|
|
1908
|
-
revert JBMultiTerminal_RecipientProjectTerminalNotFound({
|
|
1909
|
-
projectId: beneficiaryProjectId, token: tokenToReclaim
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
/// @notice Returns held fees to the project who paid them based on the specified amount.
|
|
1714
|
+
/// @notice Returns held fees to the project that paid them based on the specified amount.
|
|
1915
1715
|
/// @dev Partial replenishments use the raw floor calculation so repaying a dust amount cannot both credit the
|
|
1916
1716
|
/// payer project and leave the fee project owed the 1-unit minimum fee.
|
|
1917
1717
|
/// @param projectId The project to return held fees to.
|
|
@@ -1921,52 +1721,66 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1921
1721
|
/// @return returnedFees The amount of held fees that were returned, as a fixed point number with the same number of
|
|
1922
1722
|
/// decimals as the token's accounting context.
|
|
1923
1723
|
function _returnHeldFees(uint256 projectId, address token, uint256 amount) internal returns (uint256 returnedFees) {
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1724
|
+
// Start from the first held fee that has not already been returned, processed, or forgiven.
|
|
1725
|
+
uint256 startIndex = _nextHeldFeeIndexOf[projectId][token];
|
|
1726
|
+
|
|
1727
|
+
// Use the original array length as the upper bound. Returning held fees never appends new entries.
|
|
1728
|
+
uint256 numberOfHeldFees = _heldFeesOf[projectId][token].length;
|
|
1729
|
+
|
|
1730
|
+
if (startIndex >= numberOfHeldFees) return 0;
|
|
1731
|
+
|
|
1732
|
+
// Track how much of the new balance remains available to match against held fees.
|
|
1733
|
+
uint256 leftoverAmount = amount;
|
|
1734
|
+
|
|
1735
|
+
// Move this forward for each fully returned held fee.
|
|
1736
|
+
uint256 newStartIndex = startIndex;
|
|
1737
|
+
|
|
1738
|
+
for (uint256 i = startIndex; i < numberOfHeldFees;) {
|
|
1739
|
+
if (leftoverAmount == 0) break;
|
|
1740
|
+
|
|
1741
|
+
// Held fees store the original gross amount that paid out before its fee was removed.
|
|
1742
|
+
JBFee memory heldFee = _heldFeesOf[projectId][token][i];
|
|
1743
|
+
|
|
1744
|
+
uint256 feeAmount = _feeAmountFrom(heldFee.amount);
|
|
1745
|
+
|
|
1746
|
+
// This is the net amount that originally left the project after the held fee was removed.
|
|
1747
|
+
uint256 amountPaidOut = heldFee.amount - feeAmount;
|
|
1748
|
+
|
|
1749
|
+
if (leftoverAmount >= amountPaidOut) {
|
|
1750
|
+
unchecked {
|
|
1751
|
+
leftoverAmount -= amountPaidOut;
|
|
1752
|
+
returnedFees += feeAmount;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Move the start index forward to the fee after this fully returned one.
|
|
1756
|
+
newStartIndex = i + 1;
|
|
1757
|
+
} else {
|
|
1758
|
+
// Only part of this held fee can be returned. Convert the remaining net replenishment back into
|
|
1759
|
+
// its corresponding gross fee and shrink the stored gross amount.
|
|
1760
|
+
feeAmount = JBFees.standardFeeAmountResultingIn(leftoverAmount);
|
|
1761
|
+
|
|
1762
|
+
unchecked {
|
|
1763
|
+
_heldFeesOf[projectId][token][i].amount -= (leftoverAmount + feeAmount);
|
|
1764
|
+
returnedFees += feeAmount;
|
|
1765
|
+
}
|
|
1766
|
+
leftoverAmount = 0;
|
|
1767
|
+
}
|
|
1768
|
+
unchecked {
|
|
1769
|
+
++i;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Update the next held fee index.
|
|
1774
|
+
if (startIndex != newStartIndex) _nextHeldFeeIndexOf[projectId][token] = newStartIndex;
|
|
1775
|
+
|
|
1776
|
+
emit ReturnHeldFees({
|
|
1927
1777
|
projectId: projectId,
|
|
1928
1778
|
token: token,
|
|
1929
|
-
amount: amount
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
/// @notice Routes the cashout reclaim to B's primary terminal as a `pay` and credits B's fee-free
|
|
1934
|
-
/// surplus by the delivery delta on the first of B's accounting contexts that grew.
|
|
1935
|
-
/// @dev Extracted from the external `payAfterCashOutTokensOf` to keep that function under the
|
|
1936
|
-
/// via-IR-free stack ceiling. Uses `_efficientPay` (handles same-terminal vs cross-terminal with
|
|
1937
|
-
/// `_beforeTransferTo`/`_afterTransferTo`). `minReturnedTokens: 0` is enforced inside `_efficientPay`;
|
|
1938
|
-
/// the user-facing mint floor is `_checkMin(beneficiaryTokenCount, minTokensOut)` in the caller.
|
|
1939
|
-
/// @param tokenToReclaim The token reclaimed from the source project.
|
|
1940
|
-
/// @param reclaimAmount The amount of `tokenToReclaim` being routed.
|
|
1941
|
-
/// @param beneficiaryProjectId The destination project.
|
|
1942
|
-
/// @param beneficiary The address that receives the newly minted destination-project tokens.
|
|
1943
|
-
/// @param payMetadata Bytes forwarded to the destination project's pay flow.
|
|
1944
|
-
/// @return beneficiaryTokenCount The number of destination-project tokens minted to `beneficiary`.
|
|
1945
|
-
function _routeReclaimToBeneficiaryProject(
|
|
1946
|
-
address tokenToReclaim,
|
|
1947
|
-
uint256 reclaimAmount,
|
|
1948
|
-
uint256 beneficiaryProjectId,
|
|
1949
|
-
address beneficiary,
|
|
1950
|
-
bytes memory payMetadata
|
|
1951
|
-
)
|
|
1952
|
-
internal
|
|
1953
|
-
returns (uint256 beneficiaryTokenCount)
|
|
1954
|
-
{
|
|
1955
|
-
IJBTerminal destinationTerminal = _resolveBeneficiaryTerminal(beneficiaryProjectId, tokenToReclaim);
|
|
1956
|
-
(JBAccountingContext[] memory contexts, uint256[] memory balancesBefore) =
|
|
1957
|
-
_snapshotBeneficiaryContextBalances(beneficiaryProjectId);
|
|
1958
|
-
|
|
1959
|
-
beneficiaryTokenCount = _efficientPay({
|
|
1960
|
-
terminal: destinationTerminal,
|
|
1961
|
-
projectId: beneficiaryProjectId,
|
|
1962
|
-
token: tokenToReclaim,
|
|
1963
|
-
amount: reclaimAmount,
|
|
1964
|
-
payer: _msgSender(),
|
|
1965
|
-
beneficiary: beneficiary,
|
|
1966
|
-
metadata: payMetadata
|
|
1779
|
+
amount: amount,
|
|
1780
|
+
returnedFees: returnedFees,
|
|
1781
|
+
leftoverAmount: leftoverAmount,
|
|
1782
|
+
caller: _msgSender()
|
|
1967
1783
|
});
|
|
1968
|
-
|
|
1969
|
-
_creditFirstGrowingBeneficiaryContext(beneficiaryProjectId, contexts, balancesBefore);
|
|
1970
1784
|
}
|
|
1971
1785
|
|
|
1972
1786
|
/// @notice Sends payouts to a project's payout split group using the specified ruleset.
|
|
@@ -2085,34 +1899,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2085
1899
|
});
|
|
2086
1900
|
}
|
|
2087
1901
|
|
|
2088
|
-
/// @notice
|
|
2089
|
-
/// @dev Shared by `_routeReclaimToBeneficiaryProject` and `_routeReclaimAsAddToBalance`. Reverts if B has
|
|
2090
|
-
/// no accounting contexts on this terminal — without buckets to deliver into, nothing can land here.
|
|
2091
|
-
function _snapshotBeneficiaryContextBalances(uint256 beneficiaryProjectId)
|
|
2092
|
-
internal
|
|
2093
|
-
view
|
|
2094
|
-
returns (JBAccountingContext[] memory contexts, uint256[] memory balancesBefore)
|
|
2095
|
-
{
|
|
2096
|
-
contexts = STORE.accountingContextsOf({terminal: address(this), projectId: beneficiaryProjectId});
|
|
2097
|
-
|
|
2098
|
-
if (contexts.length == 0) {
|
|
2099
|
-
revert JBMultiTerminal_BeneficiaryProjectHasNoAccountingContexts(beneficiaryProjectId);
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
balancesBefore = new uint256[](contexts.length);
|
|
2103
|
-
for (uint256 i; i < contexts.length;) {
|
|
2104
|
-
balancesBefore[i] =
|
|
2105
|
-
STORE.balanceOf({terminal: address(this), projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
2106
|
-
unchecked {
|
|
2107
|
-
++i;
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
/// @notice Takes a fee into the platform's project (with the `JBConstants.FEE_BENEFICIARY_PROJECT_ID`).
|
|
1902
|
+
/// @notice Takes a fee into the platform's project (with the `_FEE_BENEFICIARY_PROJECT_ID`).
|
|
2113
1903
|
/// @param projectId The ID of the project paying the fee.
|
|
2114
1904
|
/// @param token The address of the token that the fee is paid in.
|
|
2115
|
-
/// @param amount The fee's token amount, as a fixed point number with
|
|
1905
|
+
/// @param amount The fee's token amount, as a fixed point number with the same number of decimals as the token's
|
|
1906
|
+
/// accounting context.
|
|
2116
1907
|
/// @param beneficiary The address to mint the platform's project's tokens for.
|
|
2117
1908
|
/// @param shouldHoldFees If fees should be tracked and held instead of processing them immediately.
|
|
2118
1909
|
/// @return feeAmount The amount of the fee taken.
|
|
@@ -2126,11 +1917,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2126
1917
|
internal
|
|
2127
1918
|
returns (uint256 feeAmount)
|
|
2128
1919
|
{
|
|
2129
|
-
//
|
|
1920
|
+
// Calculate the standard fee from the gross amount.
|
|
2130
1921
|
feeAmount = _feeAmountFrom(amount);
|
|
2131
1922
|
|
|
2132
1923
|
if (shouldHoldFees) {
|
|
2133
|
-
// Store the
|
|
1924
|
+
// Store the gross amount so future repayments can recover the corresponding fee.
|
|
2134
1925
|
_heldFeesOf[projectId][token].push(
|
|
2135
1926
|
JBFee({
|
|
2136
1927
|
amount: amount,
|
|
@@ -2144,16 +1935,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2144
1935
|
projectId: projectId,
|
|
2145
1936
|
token: token,
|
|
2146
1937
|
amount: amount,
|
|
2147
|
-
fee: JBConstants.
|
|
1938
|
+
fee: JBConstants.STANDARD_FEE,
|
|
2148
1939
|
beneficiary: beneficiary,
|
|
2149
1940
|
caller: _msgSender()
|
|
2150
1941
|
});
|
|
2151
1942
|
} else {
|
|
2152
|
-
//
|
|
2153
|
-
IJBTerminal feeTerminal =
|
|
2154
|
-
_primaryTerminalOf({projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
1943
|
+
// Resolve the fee project's terminal for this token and process the fee immediately.
|
|
1944
|
+
IJBTerminal feeTerminal = _primaryTerminalOf({projectId: _FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
2155
1945
|
|
|
2156
|
-
// Process the fee.
|
|
2157
1946
|
_processFee({
|
|
2158
1947
|
projectId: projectId,
|
|
2159
1948
|
token: token,
|
|
@@ -2357,23 +2146,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2357
2146
|
return DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
|
|
2358
2147
|
}
|
|
2359
2148
|
|
|
2360
|
-
/// @notice Revert if the destination project's current ruleset has opted out of cross-project fee-free
|
|
2361
|
-
/// inflows (`pauseCrossProjectFeeFreeInflows == true`). Shared by `payAfterCashOutTokensOf` and
|
|
2362
|
-
/// `addToBalanceAfterCashOutTokensOf`.
|
|
2363
|
-
function _requireBeneficiaryAcceptsFeeFreeInflows(uint256 beneficiaryProjectId) internal view {
|
|
2364
|
-
(, JBRulesetMetadata memory bMetadata) =
|
|
2365
|
-
_controllerOf(beneficiaryProjectId).currentRulesetOf(beneficiaryProjectId);
|
|
2366
|
-
if (bMetadata.pauseCrossProjectFeeFreeInflows) {
|
|
2367
|
-
revert JBMultiTerminal_BeneficiaryProjectFeeFreeInflowsPaused(beneficiaryProjectId);
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
/// @notice Require the caller to have `CASH_OUT_TOKENS` permission for `holder` on `projectId`. Shared by
|
|
2372
|
-
/// `cashOutTokensOf`, `payAfterCashOutTokensOf`, and `addToBalanceAfterCashOutTokensOf`.
|
|
2373
|
-
function _requireCashOutPermissionFrom(address holder, uint256 projectId) internal view {
|
|
2374
|
-
_requirePermissionFrom({account: holder, projectId: projectId, permissionId: JBPermissionIds.CASH_OUT_TOKENS});
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
2149
|
/// @notice Packages a payment amount with the token's accounting context.
|
|
2378
2150
|
/// @param projectId The ID of the project the token amount belongs to.
|
|
2379
2151
|
/// @param token The token to pay with.
|