@bananapus/core-v6 0.0.48 → 0.0.51
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 +32 -0
- package/foundry.toml +2 -0
- package/package.json +1 -1
- package/src/JBMultiTerminal.sol +490 -296
- package/src/interfaces/IJBCashOutTerminal.sol +68 -0
- package/src/libraries/JBCashOutHookSpecsLib.sol +181 -0
- package/src/libraries/JBConstants.sol +6 -0
- package/src/libraries/JBFees.sol +25 -0
- package/src/libraries/JBHeldFeesLib.sol +288 -0
- package/src/libraries/JBPayoutSplitGroupLib.sol +26 -13
- package/src/libraries/JBRulesetMetadataResolver.sol +20 -12
- package/src/structs/JBRulesetMetadata.sol +4 -1
- package/test/helpers/JBTest.sol +6 -3
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -28,8 +28,10 @@ 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";
|
|
31
32
|
import {JBConstants} from "./libraries/JBConstants.sol";
|
|
32
33
|
import {JBFees} from "./libraries/JBFees.sol";
|
|
34
|
+
import {JBHeldFeesLib} from "./libraries/JBHeldFeesLib.sol";
|
|
33
35
|
import {JBMetadataResolver} from "./libraries/JBMetadataResolver.sol";
|
|
34
36
|
import {JBPayoutSplitGroupLib} from "./libraries/JBPayoutSplitGroupLib.sol";
|
|
35
37
|
import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
|
|
@@ -40,6 +42,7 @@ import {JBCashOutHookSpecification} from "./structs/JBCashOutHookSpecification.s
|
|
|
40
42
|
import {JBFee} from "./structs/JBFee.sol";
|
|
41
43
|
import {JBPayHookSpecification} from "./structs/JBPayHookSpecification.sol";
|
|
42
44
|
import {JBRuleset} from "./structs/JBRuleset.sol";
|
|
45
|
+
import {JBRulesetMetadata} from "./structs/JBRulesetMetadata.sol";
|
|
43
46
|
import {JBSingleAllowance} from "./structs/JBSingleAllowance.sol";
|
|
44
47
|
import {JBSplit} from "./structs/JBSplit.sol";
|
|
45
48
|
import {JBSplitHookContext} from "./structs/JBSplitHookContext.sol";
|
|
@@ -63,6 +66,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
63
66
|
// --------------------------- custom errors ------------------------- //
|
|
64
67
|
//*********************************************************************//
|
|
65
68
|
|
|
69
|
+
error JBMultiTerminal_BeneficiaryProjectFeeFreeInflowsPaused(uint256 projectId);
|
|
70
|
+
error JBMultiTerminal_BeneficiaryProjectHasNoAccountingContexts(uint256 projectId);
|
|
71
|
+
error JBMultiTerminal_BeneficiaryProjectNotPaid(uint256 projectId);
|
|
66
72
|
error JBMultiTerminal_FeeTerminalNotFound(address token);
|
|
67
73
|
error JBMultiTerminal_MintNotAllowed(uint256 projectId, uint256 splitProjectId, address terminal);
|
|
68
74
|
error JBMultiTerminal_NoMsgValueAllowed(uint256 value);
|
|
@@ -81,16 +87,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
81
87
|
|
|
82
88
|
/// @notice This terminal's fee (as a fraction out of `JBConstants.MAX_FEE`).
|
|
83
89
|
/// @dev Fees are charged on payouts to addresses and surplus allowance usage, as well as cash outs while the
|
|
84
|
-
/// cash out tax rate is less than 100%.
|
|
85
|
-
|
|
90
|
+
/// cash out tax rate is less than 100%. Re-exports `JBConstants.FEE` so external callers can read it through
|
|
91
|
+
/// the `IJBFeeTerminal` interface.
|
|
92
|
+
uint256 public constant override FEE = JBConstants.FEE;
|
|
86
93
|
|
|
87
94
|
//*********************************************************************//
|
|
88
95
|
// ------------------------ internal constants ----------------------- //
|
|
89
96
|
//*********************************************************************//
|
|
90
97
|
|
|
91
|
-
/// @notice Project ID #1 receives fees. It should be the first project launched during the deployment process.
|
|
92
|
-
uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
93
|
-
|
|
94
98
|
/// @notice The number of seconds fees can be held for.
|
|
95
99
|
uint256 internal constant _FEE_HOLDING_SECONDS = 2_419_200; // 28 days
|
|
96
100
|
|
|
@@ -220,6 +224,78 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
/// @notice Atomically cash out `holder`'s tokens of `projectId` and add the reclaim to
|
|
228
|
+
/// `beneficiaryProjectId`'s balance (no project tokens minted on the destination side).
|
|
229
|
+
/// @dev Equivalent to calling `cashOutTokensOf` followed by `addToBalanceOf` on the destination project,
|
|
230
|
+
/// except the source-side cash out fee is skipped. The equivalent fee is bound on the destination project's
|
|
231
|
+
/// side instead: `_feeFreeSurplusOf[beneficiaryProjectId]` is credited by the first of the destination
|
|
232
|
+
/// project's accounting contexts on this terminal whose balance grows during the routing.
|
|
233
|
+
/// @dev The destination terminal is `DIRECTORY.primaryTerminalOf(beneficiaryProjectId, tokenToReclaim)` —
|
|
234
|
+
/// which may itself be a router that swaps before adding to balance.
|
|
235
|
+
/// @dev Held-fee return on the destination side is hardcoded to `false`. This entrypoint is for cross-project
|
|
236
|
+
/// balance top-ups only, not for unlocking the destination's held fees. Callers that want to combine
|
|
237
|
+
/// cash-out → add-to-balance with `shouldReturnHeldFees: true` must do it explicitly via two separate calls.
|
|
238
|
+
/// @dev The destination project's current ruleset can set `pauseCrossProjectFeeFreeInflows` to opt out — the
|
|
239
|
+
/// call then reverts. If no delivery to the destination project lands on this terminal under any of the
|
|
240
|
+
/// destination project's accounting contexts, the call also reverts so the source-side fee skip never
|
|
241
|
+
/// becomes a leak.
|
|
242
|
+
/// @param holder The account whose project tokens are being burned.
|
|
243
|
+
/// @param projectId The ID of the source project being cashed out from.
|
|
244
|
+
/// @param cashOutCount The number of source-project tokens to burn, as a fixed point number with 18 decimals.
|
|
245
|
+
/// @param tokenToReclaim The terminal token reclaimed from the source project's surplus.
|
|
246
|
+
/// @param beneficiaryProjectId The destination project receiving the reclaim.
|
|
247
|
+
/// @param cashOutMetadata Bytes forwarded to the source project's data hook and any cashout hook specifications.
|
|
248
|
+
/// @param addToBalanceMetadata Bytes forwarded to the destination project's `addToBalanceOf` event.
|
|
249
|
+
/// @return reclaimAmount The gross reclaim amount returned by the store.
|
|
250
|
+
function addToBalanceAfterCashOutTokensOf(
|
|
251
|
+
address holder,
|
|
252
|
+
uint256 projectId,
|
|
253
|
+
uint256 cashOutCount,
|
|
254
|
+
address tokenToReclaim,
|
|
255
|
+
uint256 beneficiaryProjectId,
|
|
256
|
+
bytes calldata cashOutMetadata,
|
|
257
|
+
bytes calldata addToBalanceMetadata
|
|
258
|
+
)
|
|
259
|
+
external
|
|
260
|
+
override
|
|
261
|
+
returns (uint256 reclaimAmount)
|
|
262
|
+
{
|
|
263
|
+
_requireCashOutPermissionFrom({holder: holder, projectId: projectId});
|
|
264
|
+
_requireBeneficiaryAcceptsFeeFreeInflows(beneficiaryProjectId);
|
|
265
|
+
|
|
266
|
+
// Burn source-project tokens, run cashout-side hooks, take any hook fees, and cap source fee-free.
|
|
267
|
+
// No separate destination beneficiary exists — the caller is the only address attached to this flow,
|
|
268
|
+
// used as the `CashOutTokens` event slot and credited any hook-fee project tokens.
|
|
269
|
+
reclaimAmount = _executeCrossProjectCashOut({
|
|
270
|
+
holder: holder,
|
|
271
|
+
projectId: projectId,
|
|
272
|
+
cashOutCount: cashOutCount,
|
|
273
|
+
tokenToReclaim: tokenToReclaim,
|
|
274
|
+
beneficiary: _msgSender(),
|
|
275
|
+
cashOutMetadata: cashOutMetadata
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Nothing to route if the data hook returned zero reclaim.
|
|
279
|
+
if (reclaimAmount == 0) return 0;
|
|
280
|
+
|
|
281
|
+
// Route the reclaim to B's primary terminal as an `addToBalanceOf`, then credit B's fee-free surplus
|
|
282
|
+
// by the delivery delta. `_efficientAddToBalance` handles same-terminal vs cross-terminal and
|
|
283
|
+
// hardcodes `shouldReturnHeldFees: false` — this entrypoint cannot unlock B's held fees.
|
|
284
|
+
IJBTerminal destinationTerminal = _resolveBeneficiaryTerminal(beneficiaryProjectId, tokenToReclaim);
|
|
285
|
+
(JBAccountingContext[] memory contexts, uint256[] memory balancesBefore) =
|
|
286
|
+
_snapshotBeneficiaryContextBalances(beneficiaryProjectId);
|
|
287
|
+
|
|
288
|
+
_efficientAddToBalance({
|
|
289
|
+
terminal: destinationTerminal,
|
|
290
|
+
projectId: beneficiaryProjectId,
|
|
291
|
+
token: tokenToReclaim,
|
|
292
|
+
amount: reclaimAmount,
|
|
293
|
+
metadata: addToBalanceMetadata
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
_creditFirstGrowingBeneficiaryContext(beneficiaryProjectId, contexts, balancesBefore);
|
|
297
|
+
}
|
|
298
|
+
|
|
223
299
|
/// @notice Adds funds (terminal tokens) to a project's balance without minting project tokens. Useful for topping
|
|
224
300
|
/// up a project or returning funds. Can also unlock previously held fees by returning them to the project's
|
|
225
301
|
/// balance.
|
|
@@ -287,8 +363,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
287
363
|
override
|
|
288
364
|
returns (uint256 reclaimAmount)
|
|
289
365
|
{
|
|
290
|
-
|
|
291
|
-
_requirePermissionFrom({account: holder, projectId: projectId, permissionId: JBPermissionIds.CASH_OUT_TOKENS});
|
|
366
|
+
_requireCashOutPermissionFrom({holder: holder, projectId: projectId});
|
|
292
367
|
|
|
293
368
|
reclaimAmount = _cashOutTokensOf({
|
|
294
369
|
holder: holder,
|
|
@@ -412,6 +487,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
412
487
|
projectId: split.projectId,
|
|
413
488
|
token: token,
|
|
414
489
|
amount: netPayoutAmount,
|
|
490
|
+
payer: address(this),
|
|
415
491
|
beneficiary: beneficiary,
|
|
416
492
|
metadata: metadata
|
|
417
493
|
});
|
|
@@ -473,9 +549,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
473
549
|
|
|
474
550
|
_efficientPay({
|
|
475
551
|
terminal: feeTerminal,
|
|
476
|
-
projectId:
|
|
552
|
+
projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID,
|
|
477
553
|
token: token,
|
|
478
554
|
amount: amount,
|
|
555
|
+
payer: address(this),
|
|
479
556
|
beneficiary: beneficiary,
|
|
480
557
|
metadata: metadata
|
|
481
558
|
});
|
|
@@ -538,7 +615,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
538
615
|
// Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
|
|
539
616
|
// This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
|
|
540
617
|
uint256 feeAmount;
|
|
541
|
-
if (
|
|
618
|
+
if (
|
|
619
|
+
!_isFeeless({addr: address(to), projectId: projectId})
|
|
620
|
+
&& projectId != JBConstants.FEE_BENEFICIARY_PROJECT_ID
|
|
621
|
+
) {
|
|
542
622
|
feeAmount = _takeFeeFrom({
|
|
543
623
|
projectId: projectId,
|
|
544
624
|
token: token,
|
|
@@ -620,6 +700,71 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
620
700
|
_checkMin({value: beneficiaryTokenCount, min: minReturnedTokens});
|
|
621
701
|
}
|
|
622
702
|
|
|
703
|
+
/// @notice Atomically cash out `holder`'s tokens of `projectId` and pay the reclaim into `beneficiaryProjectId`.
|
|
704
|
+
/// @dev Equivalent to calling `cashOutTokensOf` followed by `pay` on the destination project, except the
|
|
705
|
+
/// source-side cash out fee is skipped. The equivalent fee is bound on the destination project's side instead:
|
|
706
|
+
/// `_feeFreeSurplusOf[beneficiaryProjectId]` is credited by the first of the destination project's accounting
|
|
707
|
+
/// contexts on this terminal whose balance grows during the routing.
|
|
708
|
+
/// @dev The destination terminal is `DIRECTORY.primaryTerminalOf(beneficiaryProjectId, tokenToReclaim)` —
|
|
709
|
+
/// which may itself be a router that swaps before paying the destination.
|
|
710
|
+
/// @dev The destination project's current ruleset can set `pauseCrossProjectFeeFreeInflows` to opt out — the
|
|
711
|
+
/// call then reverts. If no delivery to the destination project lands on this terminal under any of the
|
|
712
|
+
/// destination project's accounting contexts, the call also reverts so the source-side fee skip never becomes
|
|
713
|
+
/// a leak.
|
|
714
|
+
/// @param holder The account whose project tokens are being burned.
|
|
715
|
+
/// @param projectId The ID of the source project being cashed out from.
|
|
716
|
+
/// @param cashOutCount The number of source-project tokens to burn, as a fixed point number with 18 decimals.
|
|
717
|
+
/// @param tokenToReclaim The terminal token reclaimed from the source project's surplus.
|
|
718
|
+
/// @param beneficiaryProjectId The destination project receiving the reclaim.
|
|
719
|
+
/// @param beneficiary The address that receives the newly minted destination-project tokens.
|
|
720
|
+
/// @param minTokensOut The minimum number of destination-project tokens that must be minted, otherwise revert.
|
|
721
|
+
/// @param cashOutMetadata Bytes forwarded to the source project's data hook and any cashout hook specifications.
|
|
722
|
+
/// @param payMetadata Bytes forwarded to the destination project's pay flow.
|
|
723
|
+
/// @return reclaimAmount The gross reclaim amount returned by the store.
|
|
724
|
+
/// @return beneficiaryTokenCount The number of destination-project tokens minted to `beneficiary`.
|
|
725
|
+
function payAfterCashOutTokensOf(
|
|
726
|
+
address holder,
|
|
727
|
+
uint256 projectId,
|
|
728
|
+
uint256 cashOutCount,
|
|
729
|
+
address tokenToReclaim,
|
|
730
|
+
uint256 beneficiaryProjectId,
|
|
731
|
+
address beneficiary,
|
|
732
|
+
uint256 minTokensOut,
|
|
733
|
+
bytes calldata cashOutMetadata,
|
|
734
|
+
bytes calldata payMetadata
|
|
735
|
+
)
|
|
736
|
+
external
|
|
737
|
+
override
|
|
738
|
+
returns (uint256 reclaimAmount, uint256 beneficiaryTokenCount)
|
|
739
|
+
{
|
|
740
|
+
_requireCashOutPermissionFrom({holder: holder, projectId: projectId});
|
|
741
|
+
_requireBeneficiaryAcceptsFeeFreeInflows(beneficiaryProjectId);
|
|
742
|
+
|
|
743
|
+
// Burn source-project tokens, run cashout-side hooks, take any hook fees, and cap source fee-free.
|
|
744
|
+
reclaimAmount = _executeCrossProjectCashOut({
|
|
745
|
+
holder: holder,
|
|
746
|
+
projectId: projectId,
|
|
747
|
+
cashOutCount: cashOutCount,
|
|
748
|
+
tokenToReclaim: tokenToReclaim,
|
|
749
|
+
beneficiary: beneficiary,
|
|
750
|
+
cashOutMetadata: cashOutMetadata
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Nothing to route if the data hook returned zero reclaim.
|
|
754
|
+
if (reclaimAmount != 0) {
|
|
755
|
+
beneficiaryTokenCount = _routeReclaimToBeneficiaryProject({
|
|
756
|
+
tokenToReclaim: tokenToReclaim,
|
|
757
|
+
reclaimAmount: reclaimAmount,
|
|
758
|
+
beneficiaryProjectId: beneficiaryProjectId,
|
|
759
|
+
beneficiary: beneficiary,
|
|
760
|
+
payMetadata: payMetadata
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Mint floor: how many destination-project tokens were issued for the inbound pay.
|
|
765
|
+
_checkMin({value: beneficiaryTokenCount, min: minTokensOut});
|
|
766
|
+
}
|
|
767
|
+
|
|
623
768
|
/// @notice Processes held fees for a project, sending them to the protocol's fee project. Fees are held for 28 days
|
|
624
769
|
/// after a payout — processing them finalizes the fee payment.
|
|
625
770
|
/// @dev Only processes fees whose `unlockTimestamp` has passed. Stops early if it encounters a still-locked fee.
|
|
@@ -631,59 +776,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
631
776
|
/// @param token The token to process held fees for.
|
|
632
777
|
/// @param count The number of fees to process.
|
|
633
778
|
function processHeldFeesOf(uint256 projectId, address token, uint256 count) external override {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
// If all fees have been processed, break to cleanup.
|
|
645
|
-
if (currentIndex >= _heldFeesOf[projectId][token].length) break;
|
|
646
|
-
|
|
647
|
-
// Keep a reference to the held fee being iterated on.
|
|
648
|
-
JBFee memory heldFee = _heldFeesOf[projectId][token][currentIndex];
|
|
649
|
-
|
|
650
|
-
// Can't process fees that aren't yet unlocked. Fees unlock sequentially in the array, so nothing left to do
|
|
651
|
-
// if the current fee isn't yet unlocked.
|
|
652
|
-
// forge-lint: disable-next-line(block-timestamp)
|
|
653
|
-
if (heldFee.unlockTimestamp > block.timestamp) break;
|
|
654
|
-
|
|
655
|
-
// Delete the entry and advance the index *before* the external call. This is intentional:
|
|
656
|
-
// 1. It prevents reentrancy from reprocessing the same fee.
|
|
657
|
-
// 2. If `_processFee` fails (try-catch), the fee amount is returned to the project's balance via
|
|
658
|
-
// `_recordAddedBalanceFor` — the fee is forgiven rather than retried. This is a deliberate design
|
|
659
|
-
// choice: projects should not have funds permanently stuck because the fee route is misconfigured or
|
|
660
|
-
// reverting.
|
|
661
|
-
// A `FeeReverted` event is emitted so the forgiveness is observable off-chain.
|
|
662
|
-
delete _heldFeesOf[projectId][token][currentIndex];
|
|
663
|
-
_nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
|
|
664
|
-
|
|
665
|
-
// Process the fee.
|
|
666
|
-
_processFee({
|
|
667
|
-
projectId: projectId,
|
|
668
|
-
token: token,
|
|
669
|
-
amount: _feeAmountFrom(heldFee.amount),
|
|
670
|
-
beneficiary: heldFee.beneficiary,
|
|
671
|
-
feeTerminal: feeTerminal,
|
|
672
|
-
wasHeld: true
|
|
673
|
-
});
|
|
674
|
-
unchecked {
|
|
675
|
-
++i;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// If all held fees have been processed, reset the array and index entirely to bound storage growth.
|
|
680
|
-
if (
|
|
681
|
-
_nextHeldFeeIndexOf[projectId][token] >= _heldFeesOf[projectId][token].length
|
|
682
|
-
&& _heldFeesOf[projectId][token].length > 0
|
|
683
|
-
) {
|
|
684
|
-
delete _heldFeesOf[projectId][token];
|
|
685
|
-
delete _nextHeldFeeIndexOf[projectId][token];
|
|
686
|
-
}
|
|
779
|
+
JBHeldFeesLib.processHeldFees({
|
|
780
|
+
heldFeesOf: _heldFeesOf,
|
|
781
|
+
nextHeldFeeIndexOf: _nextHeldFeeIndexOf,
|
|
782
|
+
directory: DIRECTORY,
|
|
783
|
+
store: STORE,
|
|
784
|
+
projectId: projectId,
|
|
785
|
+
token: token,
|
|
786
|
+
count: count
|
|
787
|
+
});
|
|
687
788
|
}
|
|
688
789
|
|
|
689
790
|
/// @notice Distributes funds from a project's balance to its payout split recipients, up to the current ruleset's
|
|
@@ -1154,25 +1255,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1154
1255
|
// Cache whether the beneficiary is feeless.
|
|
1155
1256
|
bool beneficiaryIsFeeless = _isFeeless({addr: beneficiary, projectId: projectId});
|
|
1156
1257
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
tokenToReclaim: tokenToReclaim,
|
|
1167
|
-
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
1168
|
-
metadata: metadata
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// Burn the project tokens.
|
|
1172
|
-
if (cashOutCount != 0) {
|
|
1173
|
-
controller.burnTokensOf({holder: holder, projectId: projectId, tokenCount: cashOutCount, memo: ""});
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1258
|
+
// Record the cash out and burn the project tokens.
|
|
1259
|
+
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _recordAndBurnCashOut({
|
|
1260
|
+
holder: holder,
|
|
1261
|
+
projectId: projectId,
|
|
1262
|
+
cashOutCount: cashOutCount,
|
|
1263
|
+
tokenToReclaim: tokenToReclaim,
|
|
1264
|
+
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
1265
|
+
metadata: metadata
|
|
1266
|
+
});
|
|
1176
1267
|
|
|
1177
1268
|
// Keep a reference to the amount being reclaimed that is subject to fees.
|
|
1178
1269
|
uint256 amountEligibleForFees;
|
|
@@ -1212,7 +1303,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1212
1303
|
// If the data hook returned cash out hook specifications, fulfill them.
|
|
1213
1304
|
if (hookSpecifications.length != 0) {
|
|
1214
1305
|
// Fulfill the cash out hook specifications.
|
|
1215
|
-
amountEligibleForFees +=
|
|
1306
|
+
amountEligibleForFees += JBCashOutHookSpecsLib.fulfill({
|
|
1307
|
+
feelessAddresses: FEELESS_ADDRESSES,
|
|
1216
1308
|
projectId: projectId,
|
|
1217
1309
|
holder: holder,
|
|
1218
1310
|
cashOutCount: cashOutCount,
|
|
@@ -1268,6 +1360,43 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1268
1360
|
if (value < min) revert JBMultiTerminal_UnderMin(value, min);
|
|
1269
1361
|
}
|
|
1270
1362
|
|
|
1363
|
+
/// @notice Find the first of B's accounting contexts whose balance grew during the routing, credit
|
|
1364
|
+
/// `_feeFreeSurplusOf[beneficiaryProjectId][token]` by the delta, cap to remaining balance, and return.
|
|
1365
|
+
/// Reverts with `JBMultiTerminal_BeneficiaryProjectNotPaid` if no context grew — without delivery, the
|
|
1366
|
+
/// skipped source-side fee can't be bound and would leak.
|
|
1367
|
+
/// @dev Shared by `_routeReclaimToBeneficiaryProject` and `_routeReclaimAsAddToBalance`. The "first
|
|
1368
|
+
/// growing context wins" rule matches a well-behaved router that picks one of B's tokens (the post-swap
|
|
1369
|
+
/// token) and deposits into that single bucket.
|
|
1370
|
+
/// @param beneficiaryProjectId The destination project.
|
|
1371
|
+
/// @param contexts Accounting contexts captured before the routing.
|
|
1372
|
+
/// @param balancesBefore Pre-routing balances aligned to `contexts` by index.
|
|
1373
|
+
function _creditFirstGrowingBeneficiaryContext(
|
|
1374
|
+
uint256 beneficiaryProjectId,
|
|
1375
|
+
JBAccountingContext[] memory contexts,
|
|
1376
|
+
uint256[] memory balancesBefore
|
|
1377
|
+
)
|
|
1378
|
+
internal
|
|
1379
|
+
{
|
|
1380
|
+
for (uint256 i; i < contexts.length;) {
|
|
1381
|
+
uint256 balanceAfter =
|
|
1382
|
+
STORE.balanceOf({terminal: address(this), projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
1383
|
+
|
|
1384
|
+
if (balanceAfter > balancesBefore[i]) {
|
|
1385
|
+
unchecked {
|
|
1386
|
+
_feeFreeSurplusOf[beneficiaryProjectId][contexts[i].token] += balanceAfter - balancesBefore[i];
|
|
1387
|
+
}
|
|
1388
|
+
_capFeeFreeSurplus({projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
unchecked {
|
|
1393
|
+
++i;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
revert JBMultiTerminal_BeneficiaryProjectNotPaid(beneficiaryProjectId);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1271
1400
|
/// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
|
|
1272
1401
|
/// recipient terminal's `addToBalance` function.
|
|
1273
1402
|
/// @param terminal The terminal on which the project is expecting to receive funds.
|
|
@@ -1317,40 +1446,135 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1317
1446
|
uint256 projectId,
|
|
1318
1447
|
address token,
|
|
1319
1448
|
uint256 amount,
|
|
1449
|
+
address payer,
|
|
1320
1450
|
address beneficiary,
|
|
1321
1451
|
bytes memory metadata
|
|
1322
1452
|
)
|
|
1323
1453
|
internal
|
|
1454
|
+
returns (uint256 newlyIssuedTokenCount)
|
|
1324
1455
|
{
|
|
1325
1456
|
if (terminal == IJBTerminal(address(this))) {
|
|
1326
|
-
_pay({
|
|
1457
|
+
return _pay({
|
|
1327
1458
|
projectId: projectId,
|
|
1328
1459
|
token: token,
|
|
1329
1460
|
amount: amount,
|
|
1330
|
-
payer:
|
|
1461
|
+
payer: payer,
|
|
1331
1462
|
beneficiary: beneficiary,
|
|
1332
1463
|
memo: "",
|
|
1333
1464
|
metadata: metadata
|
|
1334
1465
|
});
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Cross-terminal: standard pre/post transfer + external `pay()`. `minReturnedTokens: 0` because callers
|
|
1469
|
+
// own their own slippage gate (e.g. `_checkMin(beneficiaryTokenCount, minTokensOut)`).
|
|
1470
|
+
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1471
|
+
|
|
1472
|
+
newlyIssuedTokenCount = terminal.pay{value: payValue}({
|
|
1473
|
+
projectId: projectId,
|
|
1474
|
+
token: token,
|
|
1475
|
+
amount: amount,
|
|
1476
|
+
beneficiary: beneficiary,
|
|
1477
|
+
minReturnedTokens: 0,
|
|
1478
|
+
memo: "",
|
|
1479
|
+
metadata: metadata
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/// @notice Shared cashout-prep step for both cross-project entrypoints (`_payAfterCashOutTokensOf` and
|
|
1486
|
+
/// `_addToBalanceAfterCashOutTokensOf`). Records the cashout with `beneficiaryIsFeeless: true`, burns the
|
|
1487
|
+
/// holder's project tokens, runs cashout-side hook specs, caps the source's fee-free surplus, and takes
|
|
1488
|
+
/// hook fees. Returns the gross reclaim amount that the caller must then route to the destination project.
|
|
1489
|
+
/// @dev `beneficiary` is recorded in the `CashOutTokens` event and credited any fee-project tokens minted
|
|
1490
|
+
/// from hook fees. For pay it's the user-supplied destination beneficiary; for addToBalance the caller
|
|
1491
|
+
/// passes `_msgSender()` (no separate recipient exists).
|
|
1492
|
+
/// @param holder The account whose source-project tokens are being burned.
|
|
1493
|
+
/// @param projectId The ID of the source project being cashed out from.
|
|
1494
|
+
/// @param cashOutCount The number of source-project tokens to burn.
|
|
1495
|
+
/// @param tokenToReclaim The terminal token reclaimed from the source project's surplus.
|
|
1496
|
+
/// @param beneficiary The address recorded in the event slot and credited any hook-fee project tokens.
|
|
1497
|
+
/// @param cashOutMetadata Bytes forwarded to the source project's data hook and any cashout hook specs.
|
|
1498
|
+
/// @return reclaimAmount The gross reclaim amount returned by the store.
|
|
1499
|
+
function _executeCrossProjectCashOut(
|
|
1500
|
+
address holder,
|
|
1501
|
+
uint256 projectId,
|
|
1502
|
+
uint256 cashOutCount,
|
|
1503
|
+
address tokenToReclaim,
|
|
1504
|
+
address beneficiary,
|
|
1505
|
+
bytes memory cashOutMetadata
|
|
1506
|
+
)
|
|
1507
|
+
internal
|
|
1508
|
+
returns (uint256 reclaimAmount)
|
|
1509
|
+
{
|
|
1510
|
+
// Record the cash out and burn the project tokens. `beneficiaryIsFeeless: true` — the equivalent fee
|
|
1511
|
+
// is bound on the destination side via the `_feeFreeSurplusOf[beneficiaryProjectId]` credit computed
|
|
1512
|
+
// from the delivery delta in the routing step. The external entrypoint reverts if delivery falls short,
|
|
1513
|
+
// so this can never become a leak.
|
|
1514
|
+
(
|
|
1515
|
+
JBRuleset memory ruleset,
|
|
1516
|
+
uint256 _reclaim,
|
|
1517
|
+
uint256 cashOutTaxRate,
|
|
1518
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
1519
|
+
) = _recordAndBurnCashOut({
|
|
1520
|
+
holder: holder,
|
|
1521
|
+
projectId: projectId,
|
|
1522
|
+
cashOutCount: cashOutCount,
|
|
1523
|
+
tokenToReclaim: tokenToReclaim,
|
|
1524
|
+
beneficiaryIsFeeless: true,
|
|
1525
|
+
metadata: cashOutMetadata
|
|
1526
|
+
});
|
|
1527
|
+
reclaimAmount = _reclaim;
|
|
1528
|
+
|
|
1529
|
+
emit CashOutTokens({
|
|
1530
|
+
rulesetId: ruleset.id,
|
|
1531
|
+
rulesetCycleNumber: ruleset.cycleNumber,
|
|
1532
|
+
projectId: projectId,
|
|
1533
|
+
holder: holder,
|
|
1534
|
+
beneficiary: beneficiary,
|
|
1535
|
+
cashOutCount: cashOutCount,
|
|
1536
|
+
cashOutTaxRate: cashOutTaxRate,
|
|
1537
|
+
reclaimAmount: reclaimAmount,
|
|
1538
|
+
metadata: cashOutMetadata,
|
|
1539
|
+
caller: _msgSender()
|
|
1540
|
+
});
|
|
1339
1541
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1542
|
+
// Only hook-spec amounts are fee-eligible here; the destination portion is intentionally feeless.
|
|
1543
|
+
uint256 amountEligibleForFees;
|
|
1544
|
+
|
|
1545
|
+
// Hook fees still apply (those funds leave the protocol to external hooks). Hook context sees
|
|
1546
|
+
// `address(this)` as the beneficiary since the terminal is custodying the reclaim mid-flow.
|
|
1547
|
+
if (hookSpecifications.length != 0) {
|
|
1548
|
+
amountEligibleForFees = JBCashOutHookSpecsLib.fulfill({
|
|
1549
|
+
feelessAddresses: FEELESS_ADDRESSES,
|
|
1343
1550
|
projectId: projectId,
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
metadata:
|
|
1551
|
+
beneficiaryReclaimAmount: _tokenAmountOf({
|
|
1552
|
+
projectId: projectId, token: tokenToReclaim, value: reclaimAmount
|
|
1553
|
+
}),
|
|
1554
|
+
holder: holder,
|
|
1555
|
+
cashOutCount: cashOutCount,
|
|
1556
|
+
metadata: cashOutMetadata,
|
|
1557
|
+
ruleset: ruleset,
|
|
1558
|
+
cashOutTaxRate: cashOutTaxRate,
|
|
1559
|
+
beneficiary: payable(address(this)),
|
|
1560
|
+
specifications: hookSpecifications
|
|
1350
1561
|
});
|
|
1562
|
+
}
|
|
1351
1563
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1564
|
+
// Cap the source project's fee-free surplus at remaining balance after the outflow. Same invariant as
|
|
1565
|
+
// `_cashOutTokensOf`: every cashout path keeps `_feeFreeSurplusOf[projectId]` consistent with the
|
|
1566
|
+
// post-outflow balance so later zero-tax cashouts from A don't fee phantom amounts.
|
|
1567
|
+
_capFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
|
|
1568
|
+
|
|
1569
|
+
// Take the fee on the hook amounts.
|
|
1570
|
+
if (amountEligibleForFees != 0) {
|
|
1571
|
+
_takeFeeFrom({
|
|
1572
|
+
projectId: projectId,
|
|
1573
|
+
token: tokenToReclaim,
|
|
1574
|
+
amount: amountEligibleForFees,
|
|
1575
|
+
beneficiary: beneficiary,
|
|
1576
|
+
shouldHoldFees: false
|
|
1577
|
+
});
|
|
1354
1578
|
}
|
|
1355
1579
|
}
|
|
1356
1580
|
|
|
@@ -1388,106 +1612,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1388
1612
|
_afterTransferTo({to: address(terminal), token: token});
|
|
1389
1613
|
}
|
|
1390
1614
|
|
|
1391
|
-
/// @notice Fulfills a list of cash out hook specifications.
|
|
1392
|
-
/// @param projectId The ID of the project to cash out from.
|
|
1393
|
-
/// @param beneficiaryReclaimAmount The number of tokens to cash out from the project.
|
|
1394
|
-
/// @param holder The address holding the tokens to cash out.
|
|
1395
|
-
/// @param cashOutCount The number of tokens to cash out.
|
|
1396
|
-
/// @param metadata Bytes to send along to the emitted event and cash out hooks as applicable.
|
|
1397
|
-
/// @param ruleset The ruleset active during this cash out as a `JBRuleset` struct.
|
|
1398
|
-
/// @param cashOutTaxRate The cash out tax rate influencing the reclaim amount, out of
|
|
1399
|
-
/// `JBConstants.MAX_CASH_OUT_TAX_RATE`. @param beneficiary The address which will receive any terminal tokens that
|
|
1400
|
-
/// are cashed out.
|
|
1401
|
-
/// @param specifications The hook specifications to fulfill.
|
|
1402
|
-
/// @return amountEligibleForFees The amount of funds which were allocated to cash out hooks and are eligible for
|
|
1403
|
-
/// fees.
|
|
1404
|
-
function _fulfillCashOutHookSpecificationsFor(
|
|
1405
|
-
uint256 projectId,
|
|
1406
|
-
JBTokenAmount memory beneficiaryReclaimAmount,
|
|
1407
|
-
address holder,
|
|
1408
|
-
uint256 cashOutCount,
|
|
1409
|
-
bytes memory metadata,
|
|
1410
|
-
JBRuleset memory ruleset,
|
|
1411
|
-
uint256 cashOutTaxRate,
|
|
1412
|
-
address payable beneficiary,
|
|
1413
|
-
JBCashOutHookSpecification[] memory specifications
|
|
1414
|
-
)
|
|
1415
|
-
internal
|
|
1416
|
-
returns (uint256 amountEligibleForFees)
|
|
1417
|
-
{
|
|
1418
|
-
// Keep a reference to cash out context for the cash out hooks.
|
|
1419
|
-
JBAfterCashOutRecordedContext memory context = JBAfterCashOutRecordedContext({
|
|
1420
|
-
holder: holder,
|
|
1421
|
-
projectId: projectId,
|
|
1422
|
-
rulesetId: ruleset.id,
|
|
1423
|
-
cashOutCount: cashOutCount,
|
|
1424
|
-
reclaimedAmount: beneficiaryReclaimAmount,
|
|
1425
|
-
forwardedAmount: beneficiaryReclaimAmount,
|
|
1426
|
-
cashOutTaxRate: cashOutTaxRate,
|
|
1427
|
-
beneficiary: beneficiary,
|
|
1428
|
-
hookMetadata: "",
|
|
1429
|
-
cashOutMetadata: metadata
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
for (uint256 i; i < specifications.length;) {
|
|
1433
|
-
// Set the specification being iterated on.
|
|
1434
|
-
JBCashOutHookSpecification memory specification = specifications[i];
|
|
1435
|
-
|
|
1436
|
-
// A noop specification is informational only and doesn't trigger the hook.
|
|
1437
|
-
if (specification.noop) {
|
|
1438
|
-
unchecked {
|
|
1439
|
-
++i;
|
|
1440
|
-
}
|
|
1441
|
-
continue;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Get the fee for the specified amount.
|
|
1445
|
-
uint256 specificationAmountFee = _isFeeless({addr: address(specification.hook), projectId: projectId})
|
|
1446
|
-
? 0
|
|
1447
|
-
: _feeAmountFrom(specification.amount);
|
|
1448
|
-
|
|
1449
|
-
// Add the specification's amount to the amount eligible for fees.
|
|
1450
|
-
if (specificationAmountFee != 0) {
|
|
1451
|
-
amountEligibleForFees += specification.amount;
|
|
1452
|
-
specification.amount -= specificationAmountFee;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Pass the correct token `forwardedAmount` to the hook.
|
|
1456
|
-
context.forwardedAmount = JBTokenAmount({
|
|
1457
|
-
value: specification.amount,
|
|
1458
|
-
token: beneficiaryReclaimAmount.token,
|
|
1459
|
-
decimals: beneficiaryReclaimAmount.decimals,
|
|
1460
|
-
currency: beneficiaryReclaimAmount.currency
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
// Pass the correct metadata from the data hook's specification.
|
|
1464
|
-
context.hookMetadata = specification.metadata;
|
|
1465
|
-
|
|
1466
|
-
// Trigger any inherited pre-transfer logic.
|
|
1467
|
-
// Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
1468
|
-
uint256 payValue = _beforeTransferTo({
|
|
1469
|
-
to: address(specification.hook), token: beneficiaryReclaimAmount.token, amount: specification.amount
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
// Fulfill the specification.
|
|
1473
|
-
specification.hook.afterCashOutRecordedWith{value: payValue}(context);
|
|
1474
|
-
|
|
1475
|
-
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
1476
|
-
_afterTransferTo({to: address(specification.hook), token: beneficiaryReclaimAmount.token});
|
|
1477
|
-
|
|
1478
|
-
emit HookAfterRecordCashOut({
|
|
1479
|
-
hook: specification.hook,
|
|
1480
|
-
context: context,
|
|
1481
|
-
specificationAmount: specification.amount,
|
|
1482
|
-
fee: specificationAmountFee,
|
|
1483
|
-
caller: _msgSender()
|
|
1484
|
-
});
|
|
1485
|
-
unchecked {
|
|
1486
|
-
++i;
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
1615
|
/// @notice Fulfills a list of pay hook specifications.
|
|
1492
1616
|
/// @param projectId The ID of the project to pay.
|
|
1493
1617
|
/// @param specifications The pay hook specifications to be fulfilled.
|
|
@@ -1583,6 +1707,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1583
1707
|
/// applicable.
|
|
1584
1708
|
/// @param memo A memo to pass along to the emitted event.
|
|
1585
1709
|
/// @param metadata Bytes to send along to the emitted event, as well as the data hook and pay hook if applicable.
|
|
1710
|
+
/// @return newlyIssuedTokenCount The number of project tokens minted to `beneficiary` as a result of this payment.
|
|
1586
1711
|
function _pay(
|
|
1587
1712
|
uint256 projectId,
|
|
1588
1713
|
address token,
|
|
@@ -1593,6 +1718,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1593
1718
|
bytes memory metadata
|
|
1594
1719
|
)
|
|
1595
1720
|
internal
|
|
1721
|
+
returns (uint256 newlyIssuedTokenCount)
|
|
1596
1722
|
{
|
|
1597
1723
|
// Keep a reference to the token amount to forward to the store.
|
|
1598
1724
|
JBTokenAmount memory tokenAmount = _tokenAmountOf({projectId: projectId, token: token, value: amount});
|
|
@@ -1605,9 +1731,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1605
1731
|
payer: payer, amount: tokenAmount, projectId: projectId, beneficiary: beneficiary, metadata: metadata
|
|
1606
1732
|
});
|
|
1607
1733
|
|
|
1608
|
-
// Keep a reference to the number of tokens issued for the beneficiary.
|
|
1609
|
-
uint256 newlyIssuedTokenCount;
|
|
1610
|
-
|
|
1611
1734
|
// Mint tokens if needed.
|
|
1612
1735
|
if (tokenCount != 0) {
|
|
1613
1736
|
// Set the token count to be the number of tokens minted for the beneficiary instead of the total
|
|
@@ -1667,33 +1790,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1667
1790
|
)
|
|
1668
1791
|
internal
|
|
1669
1792
|
{
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
caller: _msgSender()
|
|
1680
|
-
});
|
|
1681
|
-
} catch (bytes memory reason) {
|
|
1682
|
-
// Fee processing failed — intentionally forgive the fee and return the amount to the project.
|
|
1683
|
-
// The held-fee entry (if any) was already deleted by `processHeldFeesOf` before this call, so there is no
|
|
1684
|
-
// retry path. This is by design: a broken or misconfigured fee route should not permanently lock project
|
|
1685
|
-
// funds. The `FeeReverted` event makes this observable off-chain.
|
|
1686
|
-
emit FeeReverted({
|
|
1687
|
-
projectId: projectId,
|
|
1688
|
-
token: token,
|
|
1689
|
-
feeProjectId: _FEE_BENEFICIARY_PROJECT_ID,
|
|
1690
|
-
amount: amount,
|
|
1691
|
-
reason: reason,
|
|
1692
|
-
caller: _msgSender()
|
|
1693
|
-
});
|
|
1694
|
-
|
|
1695
|
-
_recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
|
|
1696
|
-
}
|
|
1793
|
+
JBHeldFeesLib.processFee({
|
|
1794
|
+
store: STORE,
|
|
1795
|
+
projectId: projectId,
|
|
1796
|
+
token: token,
|
|
1797
|
+
amount: amount,
|
|
1798
|
+
beneficiary: beneficiary,
|
|
1799
|
+
feeTerminal: feeTerminal,
|
|
1800
|
+
wasHeld: wasHeld
|
|
1801
|
+
});
|
|
1697
1802
|
}
|
|
1698
1803
|
|
|
1699
1804
|
/// @notice Records an added balance for a project.
|
|
@@ -1706,6 +1811,74 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1706
1811
|
STORE.recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
|
|
1707
1812
|
}
|
|
1708
1813
|
|
|
1814
|
+
/// @notice Records a cash out in the terminal store and burns the holder's project tokens.
|
|
1815
|
+
/// @dev Shared between `_cashOutTokensOf` and `_payAfterCashOutTokensOf`. The two flows differ in what
|
|
1816
|
+
/// happens AFTER the burn (where the reclaim goes, how fees are taken), but the record-and-burn step is
|
|
1817
|
+
/// identical.
|
|
1818
|
+
/// @param holder The account whose project tokens are being burned.
|
|
1819
|
+
/// @param projectId The ID of the project the project tokens belong to.
|
|
1820
|
+
/// @param cashOutCount The number of project tokens to burn.
|
|
1821
|
+
/// @param tokenToReclaim The terminal token to reclaim from the project's surplus.
|
|
1822
|
+
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address (passed through
|
|
1823
|
+
/// to the data hook context via the store).
|
|
1824
|
+
/// @param metadata Bytes to send along to the data hook.
|
|
1825
|
+
/// @return ruleset The ruleset the cash out is being made during.
|
|
1826
|
+
/// @return reclaimAmount The amount of terminal tokens to be reclaimed.
|
|
1827
|
+
/// @return cashOutTaxRate The cash out tax rate being used.
|
|
1828
|
+
/// @return hookSpecifications The cash out hook specifications returned by the data hook.
|
|
1829
|
+
function _recordAndBurnCashOut(
|
|
1830
|
+
address holder,
|
|
1831
|
+
uint256 projectId,
|
|
1832
|
+
uint256 cashOutCount,
|
|
1833
|
+
address tokenToReclaim,
|
|
1834
|
+
bool beneficiaryIsFeeless,
|
|
1835
|
+
bytes memory metadata
|
|
1836
|
+
)
|
|
1837
|
+
internal
|
|
1838
|
+
returns (
|
|
1839
|
+
JBRuleset memory ruleset,
|
|
1840
|
+
uint256 reclaimAmount,
|
|
1841
|
+
uint256 cashOutTaxRate,
|
|
1842
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
1843
|
+
)
|
|
1844
|
+
{
|
|
1845
|
+
// Cache the controller to avoid a redundant external call (also used inside STORE.recordCashOutFor).
|
|
1846
|
+
IJBController controller = _controllerOf(projectId);
|
|
1847
|
+
|
|
1848
|
+
// Record the cash out.
|
|
1849
|
+
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
|
|
1850
|
+
holder: holder,
|
|
1851
|
+
projectId: projectId,
|
|
1852
|
+
cashOutCount: cashOutCount,
|
|
1853
|
+
tokenToReclaim: tokenToReclaim,
|
|
1854
|
+
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
1855
|
+
metadata: metadata
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
// Burn the project tokens.
|
|
1859
|
+
if (cashOutCount != 0) {
|
|
1860
|
+
controller.burnTokensOf({holder: holder, projectId: projectId, tokenCount: cashOutCount, memo: ""});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/// @notice Resolve B's primary terminal for the reclaim token, reverting if the directory has no entry.
|
|
1865
|
+
/// @dev Shared by `_routeReclaimToBeneficiaryProject` and `_routeReclaimAsAddToBalance`.
|
|
1866
|
+
function _resolveBeneficiaryTerminal(
|
|
1867
|
+
uint256 beneficiaryProjectId,
|
|
1868
|
+
address tokenToReclaim
|
|
1869
|
+
)
|
|
1870
|
+
internal
|
|
1871
|
+
view
|
|
1872
|
+
returns (IJBTerminal destinationTerminal)
|
|
1873
|
+
{
|
|
1874
|
+
destinationTerminal = DIRECTORY.primaryTerminalOf({projectId: beneficiaryProjectId, token: tokenToReclaim});
|
|
1875
|
+
if (address(destinationTerminal) == address(0)) {
|
|
1876
|
+
revert JBMultiTerminal_RecipientProjectTerminalNotFound({
|
|
1877
|
+
projectId: beneficiaryProjectId, token: tokenToReclaim
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1709
1882
|
/// @notice Returns held fees to the project who paid them based on the specified amount.
|
|
1710
1883
|
/// @dev Partial replenishments use the raw floor calculation so repaying a dust amount cannot both credit the
|
|
1711
1884
|
/// payer project and leave the fee project owed the 1-unit minimum fee.
|
|
@@ -1716,73 +1889,52 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1716
1889
|
/// @return returnedFees The amount of held fees that were returned, as a fixed point number with the same number of
|
|
1717
1890
|
/// decimals as the token's accounting context.
|
|
1718
1891
|
function _returnHeldFees(uint256 projectId, address token, uint256 amount) internal returns (uint256 returnedFees) {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
// Get a reference to the project's held fees.
|
|
1723
|
-
uint256 numberOfHeldFees = _heldFeesOf[projectId][token].length;
|
|
1724
|
-
|
|
1725
|
-
// If the start index is greater than or equal to the number of held fees, return 0.
|
|
1726
|
-
if (startIndex >= numberOfHeldFees) return 0;
|
|
1727
|
-
|
|
1728
|
-
// Get a reference to the leftover amount once all fees have been settled.
|
|
1729
|
-
uint256 leftoverAmount = amount;
|
|
1730
|
-
|
|
1731
|
-
// Keep a reference to the number of iterations to perform.
|
|
1732
|
-
uint256 count = numberOfHeldFees - startIndex;
|
|
1733
|
-
|
|
1734
|
-
// Keep a reference to the new start index.
|
|
1735
|
-
uint256 newStartIndex = startIndex;
|
|
1736
|
-
|
|
1737
|
-
// Process each fee.
|
|
1738
|
-
for (uint256 i; i < count;) {
|
|
1739
|
-
// Save the fee being iterated on.
|
|
1740
|
-
JBFee memory heldFee = _heldFeesOf[projectId][token][startIndex + i];
|
|
1741
|
-
|
|
1742
|
-
if (leftoverAmount == 0) {
|
|
1743
|
-
break;
|
|
1744
|
-
} else {
|
|
1745
|
-
// Notice here we take `feeAmountFrom` on the stored `.amount`.
|
|
1746
|
-
uint256 feeAmount = _feeAmountFrom(heldFee.amount);
|
|
1747
|
-
|
|
1748
|
-
// Keep a reference to the amount from which the fee was taken.
|
|
1749
|
-
uint256 amountPaidOut = heldFee.amount - feeAmount;
|
|
1750
|
-
|
|
1751
|
-
if (leftoverAmount >= amountPaidOut) {
|
|
1752
|
-
unchecked {
|
|
1753
|
-
leftoverAmount -= amountPaidOut;
|
|
1754
|
-
returnedFees += feeAmount;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
// Move the start index forward to the held fee after the current one.
|
|
1758
|
-
newStartIndex = startIndex + i + 1;
|
|
1759
|
-
} else {
|
|
1760
|
-
feeAmount = JBFees.feeAmountResultingIn({amountAfterFee: leftoverAmount, feePercent: FEE});
|
|
1761
|
-
|
|
1762
|
-
// Get fee from `leftoverAmount`.
|
|
1763
|
-
unchecked {
|
|
1764
|
-
_heldFeesOf[projectId][token][startIndex + i].amount -= (leftoverAmount + feeAmount);
|
|
1765
|
-
returnedFees += feeAmount;
|
|
1766
|
-
}
|
|
1767
|
-
leftoverAmount = 0;
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
unchecked {
|
|
1771
|
-
++i;
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
// Update the next held fee index.
|
|
1776
|
-
if (startIndex != newStartIndex) _nextHeldFeeIndexOf[projectId][token] = newStartIndex;
|
|
1777
|
-
|
|
1778
|
-
emit ReturnHeldFees({
|
|
1892
|
+
returnedFees = JBHeldFeesLib.returnHeldFees({
|
|
1893
|
+
heldFeesOf: _heldFeesOf,
|
|
1894
|
+
nextHeldFeeIndexOf: _nextHeldFeeIndexOf,
|
|
1779
1895
|
projectId: projectId,
|
|
1780
1896
|
token: token,
|
|
1781
|
-
amount: amount
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1897
|
+
amount: amount
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/// @notice Routes the cashout reclaim to B's primary terminal as a `pay` and credits B's fee-free
|
|
1902
|
+
/// surplus by the delivery delta on the first of B's accounting contexts that grew.
|
|
1903
|
+
/// @dev Extracted from the external `payAfterCashOutTokensOf` to keep that function under the
|
|
1904
|
+
/// via-IR-free stack ceiling. Uses `_efficientPay` (handles same-terminal vs cross-terminal with
|
|
1905
|
+
/// `_beforeTransferTo`/`_afterTransferTo`). `minReturnedTokens: 0` is enforced inside `_efficientPay`;
|
|
1906
|
+
/// the user-facing mint floor is `_checkMin(beneficiaryTokenCount, minTokensOut)` in the caller.
|
|
1907
|
+
/// @param tokenToReclaim The token reclaimed from the source project.
|
|
1908
|
+
/// @param reclaimAmount The amount of `tokenToReclaim` being routed.
|
|
1909
|
+
/// @param beneficiaryProjectId The destination project.
|
|
1910
|
+
/// @param beneficiary The address that receives the newly minted destination-project tokens.
|
|
1911
|
+
/// @param payMetadata Bytes forwarded to the destination project's pay flow.
|
|
1912
|
+
/// @return beneficiaryTokenCount The number of destination-project tokens minted to `beneficiary`.
|
|
1913
|
+
function _routeReclaimToBeneficiaryProject(
|
|
1914
|
+
address tokenToReclaim,
|
|
1915
|
+
uint256 reclaimAmount,
|
|
1916
|
+
uint256 beneficiaryProjectId,
|
|
1917
|
+
address beneficiary,
|
|
1918
|
+
bytes memory payMetadata
|
|
1919
|
+
)
|
|
1920
|
+
internal
|
|
1921
|
+
returns (uint256 beneficiaryTokenCount)
|
|
1922
|
+
{
|
|
1923
|
+
IJBTerminal destinationTerminal = _resolveBeneficiaryTerminal(beneficiaryProjectId, tokenToReclaim);
|
|
1924
|
+
(JBAccountingContext[] memory contexts, uint256[] memory balancesBefore) =
|
|
1925
|
+
_snapshotBeneficiaryContextBalances(beneficiaryProjectId);
|
|
1926
|
+
|
|
1927
|
+
beneficiaryTokenCount = _efficientPay({
|
|
1928
|
+
terminal: destinationTerminal,
|
|
1929
|
+
projectId: beneficiaryProjectId,
|
|
1930
|
+
token: tokenToReclaim,
|
|
1931
|
+
amount: reclaimAmount,
|
|
1932
|
+
payer: _msgSender(),
|
|
1933
|
+
beneficiary: beneficiary,
|
|
1934
|
+
metadata: payMetadata
|
|
1785
1935
|
});
|
|
1936
|
+
|
|
1937
|
+
_creditFirstGrowingBeneficiaryContext(beneficiaryProjectId, contexts, balancesBefore);
|
|
1786
1938
|
}
|
|
1787
1939
|
|
|
1788
1940
|
/// @notice Sends payouts to a project's payout split group using the specified ruleset.
|
|
@@ -1901,7 +2053,31 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1901
2053
|
});
|
|
1902
2054
|
}
|
|
1903
2055
|
|
|
1904
|
-
/// @notice
|
|
2056
|
+
/// @notice Snapshot B's accounting contexts and pre-routing balances on this terminal.
|
|
2057
|
+
/// @dev Shared by `_routeReclaimToBeneficiaryProject` and `_routeReclaimAsAddToBalance`. Reverts if B has
|
|
2058
|
+
/// no accounting contexts on this terminal — without buckets to deliver into, nothing can land here.
|
|
2059
|
+
function _snapshotBeneficiaryContextBalances(uint256 beneficiaryProjectId)
|
|
2060
|
+
internal
|
|
2061
|
+
view
|
|
2062
|
+
returns (JBAccountingContext[] memory contexts, uint256[] memory balancesBefore)
|
|
2063
|
+
{
|
|
2064
|
+
contexts = STORE.accountingContextsOf({terminal: address(this), projectId: beneficiaryProjectId});
|
|
2065
|
+
|
|
2066
|
+
if (contexts.length == 0) {
|
|
2067
|
+
revert JBMultiTerminal_BeneficiaryProjectHasNoAccountingContexts(beneficiaryProjectId);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
balancesBefore = new uint256[](contexts.length);
|
|
2071
|
+
for (uint256 i; i < contexts.length;) {
|
|
2072
|
+
balancesBefore[i] =
|
|
2073
|
+
STORE.balanceOf({terminal: address(this), projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
2074
|
+
unchecked {
|
|
2075
|
+
++i;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/// @notice Takes a fee into the platform's project (with the `JBConstants.FEE_BENEFICIARY_PROJECT_ID`).
|
|
1905
2081
|
/// @param projectId The ID of the project paying the fee.
|
|
1906
2082
|
/// @param token The address of the token that the fee is paid in.
|
|
1907
2083
|
/// @param amount The fee's token amount, as a fixed point number with 18 decimals.
|
|
@@ -1942,7 +2118,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1942
2118
|
});
|
|
1943
2119
|
} else {
|
|
1944
2120
|
// Get the terminal that'll receive the fee if one wasn't provided.
|
|
1945
|
-
IJBTerminal feeTerminal =
|
|
2121
|
+
IJBTerminal feeTerminal =
|
|
2122
|
+
_primaryTerminalOf({projectId: JBConstants.FEE_BENEFICIARY_PROJECT_ID, token: token});
|
|
1946
2123
|
|
|
1947
2124
|
// Process the fee.
|
|
1948
2125
|
_processFee({
|
|
@@ -2148,6 +2325,23 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2148
2325
|
return DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
|
|
2149
2326
|
}
|
|
2150
2327
|
|
|
2328
|
+
/// @notice Revert if the destination project's current ruleset has opted out of cross-project fee-free
|
|
2329
|
+
/// inflows (`pauseCrossProjectFeeFreeInflows == true`). Shared by `payAfterCashOutTokensOf` and
|
|
2330
|
+
/// `addToBalanceAfterCashOutTokensOf`.
|
|
2331
|
+
function _requireBeneficiaryAcceptsFeeFreeInflows(uint256 beneficiaryProjectId) internal view {
|
|
2332
|
+
(, JBRulesetMetadata memory bMetadata) =
|
|
2333
|
+
_controllerOf(beneficiaryProjectId).currentRulesetOf(beneficiaryProjectId);
|
|
2334
|
+
if (bMetadata.pauseCrossProjectFeeFreeInflows) {
|
|
2335
|
+
revert JBMultiTerminal_BeneficiaryProjectFeeFreeInflowsPaused(beneficiaryProjectId);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/// @notice Require the caller to have `CASH_OUT_TOKENS` permission for `holder` on `projectId`. Shared by
|
|
2340
|
+
/// `cashOutTokensOf`, `payAfterCashOutTokensOf`, and `addToBalanceAfterCashOutTokensOf`.
|
|
2341
|
+
function _requireCashOutPermissionFrom(address holder, uint256 projectId) internal view {
|
|
2342
|
+
_requirePermissionFrom({account: holder, projectId: projectId, permissionId: JBPermissionIds.CASH_OUT_TOKENS});
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2151
2345
|
/// @notice Packages a payment amount with the token's accounting context.
|
|
2152
2346
|
/// @param projectId The ID of the project the token amount belongs to.
|
|
2153
2347
|
/// @param token The token to pay with.
|
|
@@ -2178,6 +2372,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2178
2372
|
/// @param amount The amount before the fee is applied.
|
|
2179
2373
|
/// @return The fee amount.
|
|
2180
2374
|
function _feeAmountFrom(uint256 amount) private pure returns (uint256) {
|
|
2181
|
-
return JBFees.
|
|
2375
|
+
return JBFees.standardFeeAmountFrom(amount);
|
|
2182
2376
|
}
|
|
2183
2377
|
}
|