@bananapus/core-v6 0.0.51 → 0.0.53
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 +16 -0
- package/foundry.toml +1 -1
- package/package.json +1 -1
- package/src/JBMultiTerminal.sol +76 -44
- package/src/interfaces/IJBFeeTerminal.sol +0 -3
- package/src/libraries/JBCashOutHookSpecsLib.sol +80 -85
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,17 @@ This file describes the verified change from `nana-core-v5` to the current `nana
|
|
|
13
13
|
- `JBTokens`
|
|
14
14
|
- the shared core interfaces, structs, and libraries under `src/`
|
|
15
15
|
|
|
16
|
+
## 0.0.53 — Drop the `via_ir` requirement
|
|
17
|
+
|
|
18
|
+
`JBCashOutHookSpecsLib.fulfill` originally took 10 named arguments. When `JBMultiTerminal._cashOutTokensOf` and `_payAfterCashOutTokensOf` called it, the call site ran past solc 0.8.28's 16-slot Yul stack ceiling, which forced every consumer of `@bananapus/core-v6` to enable `via_ir = true` in their own `foundry.toml` profile. That cascaded into stack-too-deep failures in downstream packages whose own functions couldn't tolerate `via_ir` (notably `nana-721-hook-v6`'s `JB721TiersHookStore.tiersOf`).
|
|
19
|
+
|
|
20
|
+
This release reshapes `fulfill` to accept the existing `JBAfterCashOutRecordedContext` directly — the same struct the hook receives — so no parallel arg bundle is needed. The new signature is `fulfill(IJBFeelessAddresses, JBAfterCashOutRecordedContext, JBCashOutHookSpecification[])`. The per-iteration loop body is extracted into a private `_fulfillOne` helper so its locals don't share `fulfill`'s stack frame. Both call sites in `JBMultiTerminal` build the context field-by-field (one stack slot per assignment) instead of via a struct literal (which would push all ten fields onto the stack at once and trip the same ceiling at the caller).
|
|
21
|
+
|
|
22
|
+
Integrator impact:
|
|
23
|
+
- `JBCashOutHookSpecsLib.fulfill(IJBFeelessAddresses, uint256, JBTokenAmount, address, uint256, bytes, JBRuleset, uint256, address payable, JBCashOutHookSpecification[])` → `fulfill(IJBFeelessAddresses, JBAfterCashOutRecordedContext, JBCashOutHookSpecification[])`. The only on-chain caller is `JBMultiTerminal` (this PR updates both call sites), so the public ABI surface of `JBMultiTerminal` is unchanged.
|
|
24
|
+
- Consumers of `@bananapus/core-v6@^0.0.53` can drop `via_ir = true` from their `foundry.toml` profiles if they only enabled it because of `JBCashOutHookSpecsLib`. `nana-core-v6`'s own profile flips `via_ir` to `false` to lock in the property.
|
|
25
|
+
- All 997 unit/non-fork tests pass on the refactored library + call sites.
|
|
26
|
+
|
|
16
27
|
## Summary
|
|
17
28
|
|
|
18
29
|
- v6 adds explicit preview APIs for pay and cash-out flows. Integrations can simulate more of the terminal path directly from the core contracts.
|
|
@@ -57,6 +68,9 @@ This file describes the verified change from `nana-core-v5` to the current `nana
|
|
|
57
68
|
- `IJBCashOutTerminal.previewCashOutFrom(...)` is new.
|
|
58
69
|
- `IJBCashOutTerminal.payAfterCashOutTokensOf(...)` is new.
|
|
59
70
|
- `IJBCashOutTerminal.addToBalanceAfterCashOutTokensOf(...)` is new.
|
|
71
|
+
- `IJBFeeTerminal.FEE()` is REMOVED. The terminal no longer re-exports the protocol fee constant; read
|
|
72
|
+
`JBConstants.FEE` directly. Off-chain integrators that previously called `terminal.FEE()` must switch to
|
|
73
|
+
reading the constant from `JBConstants`.
|
|
60
74
|
- `IJBTerminalStore.previewPayFrom(...)` and `previewCashOutFrom(...)` are new.
|
|
61
75
|
- `IJBTerminal.currentSurplusOf(...)` changed parameter shape.
|
|
62
76
|
- `JBRulesetMetadata` adds `pauseCrossProjectFeeFreeInflows` and narrows `metadata` from 14 to 13 bits.
|
|
@@ -96,6 +110,8 @@ This file describes the verified change from `nana-core-v5` to the current `nana
|
|
|
96
110
|
`HookAfterRecordCashOut` from the terminal address)
|
|
97
111
|
- Renamed functions
|
|
98
112
|
- `IJBController.addPriceFeed(...)` -> `addPriceFeedFor(...)`
|
|
113
|
+
- Removed functions
|
|
114
|
+
- `IJBFeeTerminal.FEE()` (read `JBConstants.FEE` directly)
|
|
99
115
|
- Changed function shapes
|
|
100
116
|
- `IJBTerminal.currentSurplusOf(...)`
|
|
101
117
|
- Added events
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -81,16 +81,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
81
81
|
error JBMultiTerminal_TokenNotAccepted(address token);
|
|
82
82
|
error JBMultiTerminal_UnderMin(uint256 value, uint256 min);
|
|
83
83
|
|
|
84
|
-
//*********************************************************************//
|
|
85
|
-
// ------------------------- public constants ------------------------ //
|
|
86
|
-
//*********************************************************************//
|
|
87
|
-
|
|
88
|
-
/// @notice This terminal's fee (as a fraction out of `JBConstants.MAX_FEE`).
|
|
89
|
-
/// @dev Fees are charged on payouts to addresses and surplus allowance usage, as well as cash outs while the
|
|
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;
|
|
93
|
-
|
|
94
84
|
//*********************************************************************//
|
|
95
85
|
// ------------------------ internal constants ----------------------- //
|
|
96
86
|
//*********************************************************************//
|
|
@@ -260,7 +250,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
260
250
|
override
|
|
261
251
|
returns (uint256 reclaimAmount)
|
|
262
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.
|
|
263
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.
|
|
264
261
|
_requireBeneficiaryAcceptsFeeFreeInflows(beneficiaryProjectId);
|
|
265
262
|
|
|
266
263
|
// Burn source-project tokens, run cashout-side hooks, take any hook fees, and cap source fee-free.
|
|
@@ -278,13 +275,19 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
278
275
|
// Nothing to route if the data hook returned zero reclaim.
|
|
279
276
|
if (reclaimAmount == 0) return 0;
|
|
280
277
|
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// hardcodes `shouldReturnHeldFees: false` — this entrypoint cannot unlock B's held fees.
|
|
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.
|
|
284
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
285
|
(JBAccountingContext[] memory contexts, uint256[] memory balancesBefore) =
|
|
286
286
|
_snapshotBeneficiaryContextBalances(beneficiaryProjectId);
|
|
287
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.
|
|
288
291
|
_efficientAddToBalance({
|
|
289
292
|
terminal: destinationTerminal,
|
|
290
293
|
projectId: beneficiaryProjectId,
|
|
@@ -293,6 +296,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
293
296
|
metadata: addToBalanceMetadata
|
|
294
297
|
});
|
|
295
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.
|
|
296
302
|
_creditFirstGrowingBeneficiaryContext(beneficiaryProjectId, contexts, balancesBefore);
|
|
297
303
|
}
|
|
298
304
|
|
|
@@ -737,7 +743,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
737
743
|
override
|
|
738
744
|
returns (uint256 reclaimAmount, uint256 beneficiaryTokenCount)
|
|
739
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.
|
|
740
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.
|
|
741
754
|
_requireBeneficiaryAcceptsFeeFreeInflows(beneficiaryProjectId);
|
|
742
755
|
|
|
743
756
|
// Burn source-project tokens, run cashout-side hooks, take any hook fees, and cap source fee-free.
|
|
@@ -1300,23 +1313,24 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1300
1313
|
}
|
|
1301
1314
|
}
|
|
1302
1315
|
|
|
1303
|
-
// If the data hook returned cash out hook specifications, fulfill them.
|
|
1316
|
+
// If the data hook returned cash out hook specifications, fulfill them. The context is built
|
|
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`).
|
|
1304
1320
|
if (hookSpecifications.length != 0) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
metadata: metadata
|
|
1319
|
-
});
|
|
1321
|
+
JBTokenAmount memory reclaimTokenAmount =
|
|
1322
|
+
_tokenAmountOf({projectId: projectId, token: tokenToReclaim, value: reclaimAmount});
|
|
1323
|
+
JBAfterCashOutRecordedContext memory ctx;
|
|
1324
|
+
ctx.holder = holder;
|
|
1325
|
+
ctx.projectId = projectId;
|
|
1326
|
+
ctx.rulesetId = ruleset.id;
|
|
1327
|
+
ctx.cashOutCount = cashOutCount;
|
|
1328
|
+
ctx.reclaimedAmount = reclaimTokenAmount;
|
|
1329
|
+
ctx.forwardedAmount = reclaimTokenAmount;
|
|
1330
|
+
ctx.cashOutTaxRate = cashOutTaxRate;
|
|
1331
|
+
ctx.beneficiary = beneficiary;
|
|
1332
|
+
ctx.cashOutMetadata = metadata;
|
|
1333
|
+
amountEligibleForFees += JBCashOutHookSpecsLib.fulfill(FEELESS_ADDRESSES, ctx, hookSpecifications);
|
|
1320
1334
|
}
|
|
1321
1335
|
|
|
1322
1336
|
// Cap fee-free surplus at remaining balance.
|
|
@@ -1377,14 +1391,30 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1377
1391
|
)
|
|
1378
1392
|
internal
|
|
1379
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.
|
|
1380
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.
|
|
1381
1402
|
uint256 balanceAfter =
|
|
1382
1403
|
STORE.balanceOf({terminal: address(this), projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
1383
1404
|
|
|
1384
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).
|
|
1385
1410
|
unchecked {
|
|
1386
1411
|
_feeFreeSurplusOf[beneficiaryProjectId][contexts[i].token] += balanceAfter - balancesBefore[i];
|
|
1387
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.
|
|
1388
1418
|
_capFeeFreeSurplus({projectId: beneficiaryProjectId, token: contexts[i].token});
|
|
1389
1419
|
return;
|
|
1390
1420
|
}
|
|
@@ -1394,6 +1424,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1394
1424
|
}
|
|
1395
1425
|
}
|
|
1396
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.
|
|
1397
1430
|
revert JBMultiTerminal_BeneficiaryProjectNotPaid(beneficiaryProjectId);
|
|
1398
1431
|
}
|
|
1399
1432
|
|
|
@@ -1545,20 +1578,19 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1545
1578
|
// Hook fees still apply (those funds leave the protocol to external hooks). Hook context sees
|
|
1546
1579
|
// `address(this)` as the beneficiary since the terminal is custodying the reclaim mid-flow.
|
|
1547
1580
|
if (hookSpecifications.length != 0) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
});
|
|
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);
|
|
1562
1594
|
}
|
|
1563
1595
|
|
|
1564
1596
|
// Cap the source project's fee-free surplus at remaining balance after the outflow. Same invariant as
|
|
@@ -2112,7 +2144,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2112
2144
|
projectId: projectId,
|
|
2113
2145
|
token: token,
|
|
2114
2146
|
amount: amount,
|
|
2115
|
-
fee: FEE,
|
|
2147
|
+
fee: JBConstants.FEE,
|
|
2116
2148
|
beneficiary: beneficiary,
|
|
2117
2149
|
caller: _msgSender()
|
|
2118
2150
|
});
|
|
@@ -71,9 +71,6 @@ interface IJBFeeTerminal is IJBTerminal {
|
|
|
71
71
|
address caller
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
-
/// @notice The terminal's fee as a fraction of `JBConstants.MAX_FEE`.
|
|
75
|
-
function FEE() external view returns (uint256);
|
|
76
|
-
|
|
77
74
|
/// @notice The contract that tracks feeless addresses.
|
|
78
75
|
function FEELESS_ADDRESSES() external view returns (IJBFeelessAddresses);
|
|
79
76
|
|
|
@@ -8,7 +8,6 @@ import {IJBCashOutHook} from "../interfaces/IJBCashOutHook.sol";
|
|
|
8
8
|
import {IJBFeelessAddresses} from "../interfaces/IJBFeelessAddresses.sol";
|
|
9
9
|
import {JBAfterCashOutRecordedContext} from "../structs/JBAfterCashOutRecordedContext.sol";
|
|
10
10
|
import {JBCashOutHookSpecification} from "../structs/JBCashOutHookSpecification.sol";
|
|
11
|
-
import {JBRuleset} from "../structs/JBRuleset.sol";
|
|
12
11
|
import {JBTokenAmount} from "../structs/JBTokenAmount.sol";
|
|
13
12
|
import {JBConstants} from "./JBConstants.sol";
|
|
14
13
|
import {JBFees} from "./JBFees.sol";
|
|
@@ -58,108 +57,104 @@ library JBCashOutHookSpecsLib {
|
|
|
58
57
|
/// @dev For each spec: if the hook is feeless, it gets the full spec amount; otherwise the hook gets
|
|
59
58
|
/// `amount - feeAmountFrom(amount)` and the gross spec amount is added to the eligible-for-fees total
|
|
60
59
|
/// (the caller takes the fee separately via `_takeFeeFrom`). Cross-token semantics: the hook context's
|
|
61
|
-
/// `forwardedAmount` carries the post-fee amount in the same token as `
|
|
60
|
+
/// `forwardedAmount` carries the post-fee amount in the same token as `context.reclaimedAmount`.
|
|
61
|
+
/// @dev The caller builds `context` once (`JBAfterCashOutRecordedContext`) and passes it in; the
|
|
62
|
+
/// library mutates `context.forwardedAmount` and `context.hookMetadata` per spec. Bundling the
|
|
63
|
+
/// caller-side state into the existing hook-context struct keeps the call site under solc 0.8.28's
|
|
64
|
+
/// non-via-ir 16-slot Yul stack ceiling (the prior 10-arg shape tripped it from
|
|
65
|
+
/// `JBMultiTerminal._cashOutTokensOf`).
|
|
62
66
|
/// @param feelessAddresses Registry of fee-exempt addresses (consulted per-hook).
|
|
63
|
-
/// @param
|
|
64
|
-
///
|
|
65
|
-
///
|
|
66
|
-
/// @param cashOutCount The number of project tokens burned.
|
|
67
|
-
/// @param metadata Bytes forwarded to each hook as `cashOutMetadata`.
|
|
68
|
-
/// @param ruleset The ruleset active during the cash out.
|
|
69
|
-
/// @param cashOutTaxRate The cash out tax rate applied.
|
|
70
|
-
/// @param beneficiary The address forwarded as the hook context's `beneficiary` (typically the user-supplied
|
|
71
|
-
/// recipient or `address(this)` for cross-project flows where the terminal custodies the reclaim mid-flow).
|
|
67
|
+
/// @param context Cash-out context to forward into each hook. `context.reclaimedAmount` doubles as the
|
|
68
|
+
/// token-amount reference for per-spec transfers; `context.forwardedAmount` / `context.hookMetadata`
|
|
69
|
+
/// are rewritten per iteration.
|
|
72
70
|
/// @param specifications The hook specifications returned by the data hook.
|
|
73
71
|
/// @return amountEligibleForFees Total spec amounts (gross) from non-feeless hooks, used by the caller to
|
|
74
72
|
/// charge fees in a single pass.
|
|
75
73
|
function fulfill(
|
|
76
74
|
IJBFeelessAddresses feelessAddresses,
|
|
77
|
-
|
|
78
|
-
JBTokenAmount memory beneficiaryReclaimAmount,
|
|
79
|
-
address holder,
|
|
80
|
-
uint256 cashOutCount,
|
|
81
|
-
bytes memory metadata,
|
|
82
|
-
JBRuleset memory ruleset,
|
|
83
|
-
uint256 cashOutTaxRate,
|
|
84
|
-
address payable beneficiary,
|
|
75
|
+
JBAfterCashOutRecordedContext memory context,
|
|
85
76
|
JBCashOutHookSpecification[] memory specifications
|
|
86
77
|
)
|
|
87
78
|
external
|
|
88
79
|
returns (uint256 amountEligibleForFees)
|
|
89
80
|
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
projectId: projectId,
|
|
93
|
-
rulesetId: ruleset.id,
|
|
94
|
-
cashOutCount: cashOutCount,
|
|
95
|
-
reclaimedAmount: beneficiaryReclaimAmount,
|
|
96
|
-
forwardedAmount: beneficiaryReclaimAmount,
|
|
97
|
-
cashOutTaxRate: cashOutTaxRate,
|
|
98
|
-
beneficiary: beneficiary,
|
|
99
|
-
hookMetadata: "",
|
|
100
|
-
cashOutMetadata: metadata
|
|
101
|
-
});
|
|
102
|
-
|
|
81
|
+
// Loop body is extracted into a helper to keep `fulfill`'s stack frame shallow enough for solc 0.8.28
|
|
82
|
+
// to compile *without* `via_ir`.
|
|
103
83
|
for (uint256 i; i < specifications.length;) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// A noop specification is informational only and doesn't trigger the hook.
|
|
107
|
-
if (specification.noop) {
|
|
108
|
-
unchecked {
|
|
109
|
-
++i;
|
|
110
|
-
}
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Get the fee for the specified amount.
|
|
115
|
-
uint256 specificationAmountFee = feelessAddresses.isFeelessFor({
|
|
116
|
-
addr: address(specification.hook), projectId: projectId
|
|
117
|
-
})
|
|
118
|
-
? 0
|
|
119
|
-
: JBFees.standardFeeAmountFrom({amountBeforeFee: specification.amount});
|
|
120
|
-
|
|
121
|
-
// Add the specification's amount to the amount eligible for fees.
|
|
122
|
-
if (specificationAmountFee != 0) {
|
|
123
|
-
amountEligibleForFees += specification.amount;
|
|
124
|
-
specification.amount -= specificationAmountFee;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Pass the correct token `forwardedAmount` to the hook.
|
|
128
|
-
context.forwardedAmount = JBTokenAmount({
|
|
129
|
-
value: specification.amount,
|
|
130
|
-
token: beneficiaryReclaimAmount.token,
|
|
131
|
-
decimals: beneficiaryReclaimAmount.decimals,
|
|
132
|
-
currency: beneficiaryReclaimAmount.currency
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Pass the correct metadata from the data hook's specification.
|
|
136
|
-
context.hookMetadata = specification.metadata;
|
|
137
|
-
|
|
138
|
-
// Trigger any inherited pre-transfer logic.
|
|
139
|
-
// Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
140
|
-
uint256 payValue = _beforeTransferTo({
|
|
141
|
-
to: address(specification.hook), token: beneficiaryReclaimAmount.token, amount: specification.amount
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Fulfill the specification.
|
|
145
|
-
specification.hook.afterCashOutRecordedWith{value: payValue}(context);
|
|
146
|
-
|
|
147
|
-
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
148
|
-
_afterTransferTo({to: address(specification.hook), token: beneficiaryReclaimAmount.token});
|
|
149
|
-
|
|
150
|
-
emit HookAfterRecordCashOut({
|
|
151
|
-
hook: specification.hook,
|
|
152
|
-
context: context,
|
|
153
|
-
specificationAmount: specification.amount,
|
|
154
|
-
fee: specificationAmountFee,
|
|
155
|
-
caller: msg.sender
|
|
156
|
-
});
|
|
84
|
+
amountEligibleForFees += _fulfillOne(feelessAddresses, context, specifications[i]);
|
|
157
85
|
unchecked {
|
|
158
86
|
++i;
|
|
159
87
|
}
|
|
160
88
|
}
|
|
161
89
|
}
|
|
162
90
|
|
|
91
|
+
/// @notice Fulfill a single hook specification: charge the protocol fee (if non-feeless), set up the hook
|
|
92
|
+
/// call context, transfer the post-fee amount to the hook, and emit the after-cash-out event.
|
|
93
|
+
/// @dev Returns the gross spec amount when the hook is non-feeless (so the caller can accumulate it into
|
|
94
|
+
/// `amountEligibleForFees`), or `0` when the hook is feeless / noop. Mutates `context.forwardedAmount`
|
|
95
|
+
/// and `context.hookMetadata` so the caller's next iteration can overwrite them.
|
|
96
|
+
/// @param feelessAddresses Registry of fee-exempt addresses (consulted to skip the per-hook fee).
|
|
97
|
+
/// @param context Cash-out context forwarded into the hook. `context.reclaimedAmount` is used as the
|
|
98
|
+
/// token-amount reference for the per-spec transfer; `context.forwardedAmount` and
|
|
99
|
+
/// `context.hookMetadata` are overwritten by this call.
|
|
100
|
+
/// @param specification The hook specification to fulfill (hook address, amount, metadata, noop flag).
|
|
101
|
+
/// @return grossSpecAmount The gross spec amount when the hook is non-feeless (for caller accumulation
|
|
102
|
+
/// into `amountEligibleForFees`); `0` for feeless or noop specifications.
|
|
103
|
+
function _fulfillOne(
|
|
104
|
+
IJBFeelessAddresses feelessAddresses,
|
|
105
|
+
JBAfterCashOutRecordedContext memory context,
|
|
106
|
+
JBCashOutHookSpecification memory specification
|
|
107
|
+
)
|
|
108
|
+
private
|
|
109
|
+
returns (uint256 grossSpecAmount)
|
|
110
|
+
{
|
|
111
|
+
// A noop specification is informational only and doesn't trigger the hook.
|
|
112
|
+
if (specification.noop) return 0;
|
|
113
|
+
|
|
114
|
+
// Get the fee for the specified amount.
|
|
115
|
+
uint256 specificationAmountFee = feelessAddresses.isFeelessFor({
|
|
116
|
+
addr: address(specification.hook), projectId: context.projectId
|
|
117
|
+
})
|
|
118
|
+
? 0
|
|
119
|
+
: JBFees.standardFeeAmountFrom({amountBeforeFee: specification.amount});
|
|
120
|
+
|
|
121
|
+
// Surface the gross spec amount to the caller so it can be accumulated into `amountEligibleForFees`.
|
|
122
|
+
if (specificationAmountFee != 0) {
|
|
123
|
+
grossSpecAmount = specification.amount;
|
|
124
|
+
specification.amount -= specificationAmountFee;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Pass the correct token `forwardedAmount` to the hook.
|
|
128
|
+
context.forwardedAmount = JBTokenAmount({
|
|
129
|
+
value: specification.amount,
|
|
130
|
+
token: context.reclaimedAmount.token,
|
|
131
|
+
decimals: context.reclaimedAmount.decimals,
|
|
132
|
+
currency: context.reclaimedAmount.currency
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Pass the correct metadata from the data hook's specification.
|
|
136
|
+
context.hookMetadata = specification.metadata;
|
|
137
|
+
|
|
138
|
+
// Trigger any inherited pre-transfer logic. Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
139
|
+
uint256 payValue = _beforeTransferTo({
|
|
140
|
+
to: address(specification.hook), token: context.reclaimedAmount.token, amount: specification.amount
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Fulfill the specification.
|
|
144
|
+
specification.hook.afterCashOutRecordedWith{value: payValue}(context);
|
|
145
|
+
|
|
146
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
147
|
+
_afterTransferTo({to: address(specification.hook), token: context.reclaimedAmount.token});
|
|
148
|
+
|
|
149
|
+
emit HookAfterRecordCashOut({
|
|
150
|
+
hook: specification.hook,
|
|
151
|
+
context: context,
|
|
152
|
+
specificationAmount: specification.amount,
|
|
153
|
+
fee: specificationAmountFee,
|
|
154
|
+
caller: msg.sender
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
163
158
|
//*********************************************************************//
|
|
164
159
|
// ----------------------- private helpers --------------------------- //
|
|
165
160
|
//*********************************************************************//
|