@bananapus/router-terminal-v6 0.0.33 → 0.0.35
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/README.md +2 -2
- package/package.json +20 -10
- package/src/JBPayRouteResolver.sol +126 -13
- package/src/JBRouterTerminal.sol +41 -16
- package/src/JBRouterTerminalRegistry.sol +97 -34
- package/src/libraries/JBSwapLib.sol +4 -3
- package/ADMINISTRATION.md +0 -80
- package/ARCHITECTURE.md +0 -94
- package/AUDIT_INSTRUCTIONS.md +0 -87
- package/RISKS.md +0 -49
- package/SKILLS.md +0 -46
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -181
- package/slither-ci.config.json +0 -10
- package/test/NegativeTickRounding.t.sol +0 -130
- package/test/RouterTerminal.t.sol +0 -2669
- package/test/RouterTerminalBuybackHookFork.t.sol +0 -402
- package/test/RouterTerminalCashOutFork.t.sol +0 -555
- package/test/RouterTerminalCreditCashout.t.sol +0 -776
- package/test/RouterTerminalERC2771.t.sol +0 -374
- package/test/RouterTerminalFeeCashOutFork.t.sol +0 -439
- package/test/RouterTerminalFork.t.sol +0 -501
- package/test/RouterTerminalMultihopFork.t.sol +0 -354
- package/test/RouterTerminalPreviewFork.t.sol +0 -339
- package/test/RouterTerminalReentrancy.t.sol +0 -477
- package/test/RouterTerminalRegistry.t.sol +0 -389
- package/test/RouterTerminalSandwichFork.t.sol +0 -813
- package/test/TestAuditGaps.sol +0 -974
- package/test/audit/CashOutCircularPrimaryTerminal.t.sol +0 -326
- package/test/audit/CashOutFallbackPrefersRecursiveLoop.t.sol +0 -363
- package/test/audit/CodexNemesisPayHookReceiptDoS.t.sol +0 -169
- package/test/audit/CreditCashoutPreferredTokenBypass.t.sol +0 -161
- package/test/audit/CreditCashoutSpoofedPayer.t.sol +0 -249
- package/test/audit/DeployBuybackHookZero.t.sol +0 -421
- package/test/audit/HookDataEncoding.t.sol +0 -154
- package/test/audit/LeftoverRefund.t.sol +0 -525
- package/test/audit/MultiHopCashOutCycle.t.sol +0 -350
- package/test/audit/MultiHopForwardCycle.t.sol +0 -187
- package/test/audit/Pass12M39.t.sol +0 -202
- package/test/audit/Pass13RouterFixes.t.sol +0 -141
- package/test/audit/PayerTrackerRefund.t.sol +0 -187
- package/test/audit/Permit2AllowanceFailed.t.sol +0 -185
- package/test/audit/PreviewCashOutShortcircuitDivergence.t.sol +0 -206
- package/test/audit/PreviewPrimaryTerminalMismatch.t.sol +0 -239
- package/test/audit/RefundToBeneficiary.t.sol +0 -101
- package/test/audit/RegistryAddToBalancePartialFill.t.sol +0 -482
- package/test/audit/RegistryForwardingLossyToken.t.sol +0 -238
- package/test/audit/RegistrySelfLockDoS.t.sol +0 -72
- package/test/audit/RevertingTerminalRouteDiscovery.t.sol +0 -466
- package/test/audit/RouterRegistryReceiptMismatch.t.sol +0 -207
- package/test/audit/V4HookedPoolIgnored.t.sol +0 -101
- package/test/audit/V4WethInputUsesStuckEth.t.sol +0 -330
- package/test/fork/RouterTerminalFOTFork.t.sol +0 -360
- package/test/fork/V4QuoteAndSettlementFork.t.sol +0 -690
- package/test/invariant/RouterTerminalInvariant.t.sol +0 -649
- package/test/regression/CashOutLoopLimit.t.sol +0 -283
- package/test/regression/LockTerminalRace.t.sol +0 -93
- package/test/regression/RouterTerminalEdgeCases.t.sol +0 -495
- package/test/regression/V4SpotPriceSlippage.t.sol +0 -401
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/router-terminal-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Bananapus/nana-router-terminal-v6"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"foundry.toml",
|
|
12
|
+
"references/",
|
|
13
|
+
"remappings.txt",
|
|
14
|
+
"script/Deploy.s.sol",
|
|
15
|
+
"script/helpers/",
|
|
16
|
+
"src/"
|
|
17
|
+
],
|
|
9
18
|
"engines": {
|
|
10
19
|
"node": ">=20.0.0"
|
|
11
20
|
},
|
|
@@ -17,16 +26,17 @@
|
|
|
17
26
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-router-terminal-v6'"
|
|
18
27
|
},
|
|
19
28
|
"dependencies": {
|
|
20
|
-
"@bananapus/buyback-hook-v6": "
|
|
21
|
-
"@bananapus/core-v6": "
|
|
22
|
-
"@bananapus/permission-ids-v6": "
|
|
23
|
-
"@
|
|
24
|
-
"@
|
|
25
|
-
"@uniswap/
|
|
26
|
-
"@uniswap/v3-
|
|
27
|
-
"@uniswap/
|
|
29
|
+
"@bananapus/buyback-hook-v6": "0.0.36",
|
|
30
|
+
"@bananapus/core-v6": "0.0.39",
|
|
31
|
+
"@bananapus/permission-ids-v6": "0.0.22",
|
|
32
|
+
"@bananapus/univ4-router-v6": "0.0.22",
|
|
33
|
+
"@openzeppelin/contracts": "5.6.1",
|
|
34
|
+
"@uniswap/permit2": "github:Uniswap/permit2#cc56ad0f3439c502c246fc5cfcc3db92bb8b7219",
|
|
35
|
+
"@uniswap/v3-core": "github:Uniswap/v3-core#6562c52e8f75f0c10f9deaf44861847585fc8129",
|
|
36
|
+
"@uniswap/v3-periphery": "github:Uniswap/v3-periphery#b325bb0905d922ae61fcc7df85ee802e8df5e96c",
|
|
37
|
+
"@uniswap/v4-core": "1.0.2"
|
|
28
38
|
},
|
|
29
39
|
"devDependencies": {
|
|
30
|
-
"@sphinx-labs/plugins": "
|
|
40
|
+
"@sphinx-labs/plugins": "0.33.3"
|
|
31
41
|
}
|
|
32
42
|
}
|
|
@@ -9,13 +9,15 @@ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataRes
|
|
|
9
9
|
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
10
10
|
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
11
11
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
12
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
12
13
|
|
|
13
14
|
import {JBForwardingCheck} from "./libraries/JBForwardingCheck.sol";
|
|
14
15
|
import {IJBPayRoutePreviewer} from "./interfaces/IJBPayRoutePreviewer.sol";
|
|
15
16
|
import {IJBPayRouteResolver} from "./interfaces/IJBPayRouteResolver.sol";
|
|
16
17
|
import {IWETH9} from "./interfaces/IWETH9.sol";
|
|
17
18
|
|
|
18
|
-
/// @notice
|
|
19
|
+
/// @notice Evaluates every token a destination project accepts and returns the route that yields the most project
|
|
20
|
+
/// tokens for the beneficiary, deployed as a helper to keep `JBRouterTerminal` within runtime size limits.
|
|
19
21
|
contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
20
22
|
//*********************************************************************//
|
|
21
23
|
// --------------------------- custom errors ------------------------- //
|
|
@@ -273,8 +275,17 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
273
275
|
continue;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
|
-
// Decode
|
|
277
|
-
|
|
278
|
+
// Decode the buyback hook's routing metadata. When the hook mints in `afterPayRecordedWith`, the terminal
|
|
279
|
+
// preview returns zero token counts and the router must score the route from the hook's commitments.
|
|
280
|
+
(
|
|
281
|
+
,
|
|
282
|
+
uint256 amountToMintWith,
|
|
283
|
+
uint256 minimumSwapAmountOut,,,
|
|
284
|
+
uint256 tokenCountWithoutHook,,,,,
|
|
285
|
+
uint256 minimumBeneficiaryTokenCount,
|
|
286
|
+
uint256 minimumReservedTokenCount,
|
|
287
|
+
uint256 rawSwapQuote
|
|
288
|
+
) = abi.decode(
|
|
278
289
|
specification.metadata,
|
|
279
290
|
(
|
|
280
291
|
bool,
|
|
@@ -293,14 +304,44 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
293
304
|
)
|
|
294
305
|
);
|
|
295
306
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
// The hook's beneficiary/reserved commitments are only for the AMM leg. If the hook leaves part of the
|
|
308
|
+
// payment to mint directly, estimate that direct-mint leg at the same issuance rate used for the swapped
|
|
309
|
+
// amount so the router compares a whole-route token count against ordinary terminal previews.
|
|
310
|
+
uint256 directMintTokenCount;
|
|
311
|
+
if (amountToMintWith != 0 && specification.amount != 0 && tokenCountWithoutHook != 0) {
|
|
312
|
+
directMintTokenCount =
|
|
313
|
+
mulDiv({x: amountToMintWith, y: tokenCountWithoutHook, denominator: specification.amount});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Score the executable floor first. This supports callers that only provide a minimum and no live quote.
|
|
317
|
+
(uint256 candidateBeneficiaryTokenCount, uint256 candidateReservedTokenCount) = _scaledPreviewPayTokenCounts({
|
|
318
|
+
tokenCount: minimumSwapAmountOut + directMintTokenCount,
|
|
319
|
+
referenceTokenCount: minimumSwapAmountOut,
|
|
320
|
+
referenceBeneficiaryTokenCount: minimumBeneficiaryTokenCount,
|
|
321
|
+
referenceReservedTokenCount: minimumReservedTokenCount
|
|
322
|
+
});
|
|
323
|
+
(effectiveBeneficiaryTokenCount, effectiveReservedTokenCount) = _strongerPreviewPayTokenCounts({
|
|
324
|
+
currentBeneficiaryTokenCount: effectiveBeneficiaryTokenCount,
|
|
325
|
+
currentReservedTokenCount: effectiveReservedTokenCount,
|
|
326
|
+
candidateBeneficiaryTokenCount: candidateBeneficiaryTokenCount,
|
|
327
|
+
candidateReservedTokenCount: candidateReservedTokenCount
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// If the hook also surfaced a stronger live quote, score it too. This lets programmatic buyback routes win
|
|
331
|
+
// when the expected executable output is better than the conservative minimum.
|
|
332
|
+
if (rawSwapQuote > minimumSwapAmountOut) {
|
|
333
|
+
(candidateBeneficiaryTokenCount, candidateReservedTokenCount) = _scaledPreviewPayTokenCounts({
|
|
334
|
+
tokenCount: rawSwapQuote + directMintTokenCount,
|
|
335
|
+
referenceTokenCount: minimumSwapAmountOut,
|
|
336
|
+
referenceBeneficiaryTokenCount: minimumBeneficiaryTokenCount,
|
|
337
|
+
referenceReservedTokenCount: minimumReservedTokenCount
|
|
338
|
+
});
|
|
339
|
+
(effectiveBeneficiaryTokenCount, effectiveReservedTokenCount) = _strongerPreviewPayTokenCounts({
|
|
340
|
+
currentBeneficiaryTokenCount: effectiveBeneficiaryTokenCount,
|
|
341
|
+
currentReservedTokenCount: effectiveReservedTokenCount,
|
|
342
|
+
candidateBeneficiaryTokenCount: candidateBeneficiaryTokenCount,
|
|
343
|
+
candidateReservedTokenCount: candidateReservedTokenCount
|
|
344
|
+
});
|
|
304
345
|
}
|
|
305
346
|
unchecked {
|
|
306
347
|
++i;
|
|
@@ -538,8 +579,8 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
538
579
|
/// @param amount The current route input amount.
|
|
539
580
|
/// @param metadata Metadata that may include a cashout-source override.
|
|
540
581
|
/// @param preferredToken The preferred token to target during any previewed cashout loop.
|
|
541
|
-
/// @return resolvedTerminal The terminal found by the
|
|
542
|
-
///
|
|
582
|
+
/// @return resolvedTerminal The terminal found by the cashout loop, or address(0) if conversion continues.
|
|
583
|
+
/// @return routedTokenIn The token that remains to be routed after the previewed cashout step.
|
|
543
584
|
/// @return routedAmountIn The amount of `routedTokenIn` that remains to be routed.
|
|
544
585
|
function _previewRouteInputFromSource(
|
|
545
586
|
IJBPayRoutePreviewer router,
|
|
@@ -668,6 +709,45 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
668
709
|
return abi.decode(data, (IJBTerminal[]));
|
|
669
710
|
}
|
|
670
711
|
|
|
712
|
+
/// @notice Scale a known beneficiary/reserved token split to a different total token count.
|
|
713
|
+
/// @param tokenCount The total token count being scored.
|
|
714
|
+
/// @param referenceTokenCount The total token count the reference split was computed from.
|
|
715
|
+
/// @param referenceBeneficiaryTokenCount The beneficiary share of the reference split.
|
|
716
|
+
/// @param referenceReservedTokenCount The reserved share of the reference split.
|
|
717
|
+
/// @return beneficiaryTokenCount The scaled beneficiary token count.
|
|
718
|
+
/// @return reservedTokenCount The scaled reserved token count.
|
|
719
|
+
function _scaledPreviewPayTokenCounts(
|
|
720
|
+
uint256 tokenCount,
|
|
721
|
+
uint256 referenceTokenCount,
|
|
722
|
+
uint256 referenceBeneficiaryTokenCount,
|
|
723
|
+
uint256 referenceReservedTokenCount
|
|
724
|
+
)
|
|
725
|
+
internal
|
|
726
|
+
pure
|
|
727
|
+
returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
|
|
728
|
+
{
|
|
729
|
+
// A zero candidate means there is no stronger route output to scale, so preserve the known reference split.
|
|
730
|
+
if (tokenCount == 0) {
|
|
731
|
+
return (referenceBeneficiaryTokenCount, referenceReservedTokenCount);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Prefer the already-previewed beneficiary/reserved total because it includes the destination's reserve logic.
|
|
735
|
+
uint256 referenceTotal = referenceBeneficiaryTokenCount + referenceReservedTokenCount;
|
|
736
|
+
|
|
737
|
+
// Fall back to the original token count when previewed counts were unavailable but the hook reported a floor.
|
|
738
|
+
if (referenceTotal == 0) referenceTotal = referenceTokenCount;
|
|
739
|
+
|
|
740
|
+
// If both reference totals are zero, treat the whole candidate as beneficiary tokens so the route stays
|
|
741
|
+
// comparable instead of disappearing from scoring.
|
|
742
|
+
if (referenceTotal == 0) return (tokenCount, 0);
|
|
743
|
+
|
|
744
|
+
// Scale the beneficiary share proportionally from the reference split to the candidate total being scored.
|
|
745
|
+
beneficiaryTokenCount = mulDiv({x: tokenCount, y: referenceBeneficiaryTokenCount, denominator: referenceTotal});
|
|
746
|
+
|
|
747
|
+
// Assign the residual to reserved tokens so rounding cannot lose supply during route comparison.
|
|
748
|
+
reservedTokenCount = tokenCount - beneficiaryTokenCount;
|
|
749
|
+
}
|
|
750
|
+
|
|
671
751
|
/// @notice Resolve whether the current route input should first be treated as a project-token cashout source.
|
|
672
752
|
/// @param router The router terminal whose project-token lookup should be used.
|
|
673
753
|
/// @param tokenIn The current route input token.
|
|
@@ -698,6 +778,39 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
698
778
|
}
|
|
699
779
|
}
|
|
700
780
|
|
|
781
|
+
/// @notice Choose the stronger preview outcome using beneficiary tokens first and reserved tokens as a tie-break.
|
|
782
|
+
/// @param currentBeneficiaryTokenCount The beneficiary token count from the strongest route so far.
|
|
783
|
+
/// @param currentReservedTokenCount The reserved token count from the strongest route so far.
|
|
784
|
+
/// @param candidateBeneficiaryTokenCount The beneficiary token count from the candidate route.
|
|
785
|
+
/// @param candidateReservedTokenCount The reserved token count from the candidate route.
|
|
786
|
+
/// @return beneficiaryTokenCount The beneficiary token count to keep.
|
|
787
|
+
/// @return reservedTokenCount The reserved token count to keep.
|
|
788
|
+
function _strongerPreviewPayTokenCounts(
|
|
789
|
+
uint256 currentBeneficiaryTokenCount,
|
|
790
|
+
uint256 currentReservedTokenCount,
|
|
791
|
+
uint256 candidateBeneficiaryTokenCount,
|
|
792
|
+
uint256 candidateReservedTokenCount
|
|
793
|
+
)
|
|
794
|
+
internal
|
|
795
|
+
pure
|
|
796
|
+
returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
|
|
797
|
+
{
|
|
798
|
+
// Prefer the route that gives the beneficiary more tokens, since that is the user's primary output.
|
|
799
|
+
if (
|
|
800
|
+
candidateBeneficiaryTokenCount > currentBeneficiaryTokenCount
|
|
801
|
+
|| (
|
|
802
|
+
// When beneficiary output ties, keep the route that also mints more reserved tokens.
|
|
803
|
+
candidateBeneficiaryTokenCount == currentBeneficiaryTokenCount
|
|
804
|
+
&& candidateReservedTokenCount > currentReservedTokenCount
|
|
805
|
+
)
|
|
806
|
+
) {
|
|
807
|
+
return (candidateBeneficiaryTokenCount, candidateReservedTokenCount);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Keep the current winner when the candidate does not improve beneficiary output or the reserved tie-break.
|
|
811
|
+
return (currentBeneficiaryTokenCount, currentReservedTokenCount);
|
|
812
|
+
}
|
|
813
|
+
|
|
701
814
|
/// @notice Resolve the usable primary terminal for a discovered candidate token.
|
|
702
815
|
/// @param router The router whose circular-terminal rule should be applied.
|
|
703
816
|
/// @param directory The directory used to resolve primary terminals.
|
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -50,9 +50,9 @@ import {JBPayRouteResolver} from "./JBPayRouteResolver.sol";
|
|
|
50
50
|
import {CashOutPathCandidates} from "./structs/CashOutPathCandidates.sol";
|
|
51
51
|
import {PoolInfo} from "./structs/PoolInfo.sol";
|
|
52
52
|
|
|
53
|
-
/// @notice
|
|
54
|
-
///
|
|
55
|
-
///
|
|
53
|
+
/// @notice A universal payment terminal that accepts any token and automatically converts it into whatever token the
|
|
54
|
+
/// destination project accepts. Routes payments via direct forwarding, Uniswap V3/V4 swaps, recursive JB token
|
|
55
|
+
/// cashouts, or a combination — always selecting the path that yields the most project tokens for the beneficiary.
|
|
56
56
|
/// @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM
|
|
57
57
|
contract JBRouterTerminal is
|
|
58
58
|
ERC2771Context,
|
|
@@ -75,6 +75,7 @@ contract JBRouterTerminal is
|
|
|
75
75
|
error JBRouterTerminal_AmountOverflow(uint256 amount);
|
|
76
76
|
error JBRouterTerminal_CallerNotPool(address caller);
|
|
77
77
|
error JBRouterTerminal_CallerNotPoolManager(address caller);
|
|
78
|
+
error JBRouterTerminal_CashOutDidNotDeliver(address sourceToken, address tokenToReclaim, uint256 cashOutCount);
|
|
78
79
|
error JBRouterTerminal_CashOutLoopLimit();
|
|
79
80
|
error JBRouterTerminal_InsufficientTwapHistory();
|
|
80
81
|
error JBRouterTerminal_NoCashOutPath(uint256 sourceProjectId, uint256 destProjectId);
|
|
@@ -449,7 +450,10 @@ contract JBRouterTerminal is
|
|
|
449
450
|
amountOut = uint256(uint128(delta0));
|
|
450
451
|
}
|
|
451
452
|
|
|
452
|
-
|
|
453
|
+
// Enforce the caller's V4 minimum against the realized delta before settling/taking pool balances.
|
|
454
|
+
if (amountOut < minAmountOut) {
|
|
455
|
+
revert JBRouterTerminal_SlippageExceeded({amountOut: amountOut, minAmountOut: minAmountOut});
|
|
456
|
+
}
|
|
453
457
|
|
|
454
458
|
// Settle input (pay what we owe to the PoolManager).
|
|
455
459
|
Currency inputCurrency = zeroForOne ? key.currency0 : key.currency1;
|
|
@@ -805,15 +809,13 @@ contract JBRouterTerminal is
|
|
|
805
809
|
continue;
|
|
806
810
|
}
|
|
807
811
|
|
|
808
|
-
// Decode only the
|
|
809
|
-
|
|
812
|
+
// Decode only the buyback field used for route scoring. `minimumSwapAmountOut` is executable because the
|
|
813
|
+
// hook will enforce it; the later raw quote word is diagnostic and can overstate what execution can prove.
|
|
814
|
+
(uint256 minimumSwapAmountOut,,,,,,) =
|
|
810
815
|
abi.decode(specification.metadata, (uint256, uint256, uint256, int24, uint128, PoolId, uint256));
|
|
811
816
|
|
|
812
|
-
//
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
// Keep whichever understood hook quote implies the strongest previewed cash-out output.
|
|
816
|
-
if (quotedAmount > effectiveAmount) effectiveAmount = quotedAmount;
|
|
817
|
+
// Keep whichever understood executable hook commitment implies the strongest cash-out output.
|
|
818
|
+
if (minimumSwapAmountOut > effectiveAmount) effectiveAmount = minimumSwapAmountOut;
|
|
817
819
|
|
|
818
820
|
unchecked {
|
|
819
821
|
++i;
|
|
@@ -1197,6 +1199,7 @@ contract JBRouterTerminal is
|
|
|
1197
1199
|
sourceProjectId: sourceProjectId, destProjectId: destProjectId, preferredToken: preferredToken
|
|
1198
1200
|
});
|
|
1199
1201
|
|
|
1202
|
+
uint256 cashOutCount = amount;
|
|
1200
1203
|
uint256 balanceBefore = _balanceOf({token: tokenToReclaim, account: address(this)});
|
|
1201
1204
|
|
|
1202
1205
|
// Cash out the source project's tokens.
|
|
@@ -1216,8 +1219,20 @@ contract JBRouterTerminal is
|
|
|
1216
1219
|
metadata: ""
|
|
1217
1220
|
});
|
|
1218
1221
|
|
|
1222
|
+
// Measure the reclaimed-token balance delta so fee-on-transfer behavior cannot fake delivery.
|
|
1219
1223
|
amount = _balanceOf({token: tokenToReclaim, account: address(this)}) - balanceBefore;
|
|
1220
|
-
|
|
1224
|
+
|
|
1225
|
+
// A non-zero cashout that delivers no reclaim tokens means this hop cannot safely continue.
|
|
1226
|
+
if (cashOutCount != 0 && amount == 0) {
|
|
1227
|
+
revert JBRouterTerminal_CashOutDidNotDeliver({
|
|
1228
|
+
sourceToken: token, tokenToReclaim: tokenToReclaim, cashOutCount: cashOutCount
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Enforce the caller's first-hop reclaim floor against the actual balance delta received.
|
|
1233
|
+
if (amount < minTokensReclaimed) {
|
|
1234
|
+
revert JBRouterTerminal_SlippageExceeded({amountOut: amount, minAmountOut: minTokensReclaimed});
|
|
1235
|
+
}
|
|
1221
1236
|
|
|
1222
1237
|
// Clear the reclaim minimum after the first hop.
|
|
1223
1238
|
// Multi-hop routes can change token units between hops, so there is no sound generic way to rescale a
|
|
@@ -1374,7 +1389,9 @@ contract JBRouterTerminal is
|
|
|
1374
1389
|
|
|
1375
1390
|
// Enforce slippage protection via realized output vs minimum acceptable output.
|
|
1376
1391
|
// This is strictly more correct than sqrtPriceLimitX96 (which conflates marginal and average price).
|
|
1377
|
-
if (amountOut < minAmountOut)
|
|
1392
|
+
if (amountOut < minAmountOut) {
|
|
1393
|
+
revert JBRouterTerminal_SlippageExceeded({amountOut: amountOut, minAmountOut: minAmountOut});
|
|
1394
|
+
}
|
|
1378
1395
|
}
|
|
1379
1396
|
|
|
1380
1397
|
/// @notice Execute a swap through a V4 pool via PoolManager.unlock().
|
|
@@ -1681,7 +1698,8 @@ contract JBRouterTerminal is
|
|
|
1681
1698
|
if (_unwrapCurrency(currency) == address(0)) _wethDeposit(amount);
|
|
1682
1699
|
}
|
|
1683
1700
|
|
|
1684
|
-
/// @notice
|
|
1701
|
+
/// @notice Transfer tokens from one address to another using direct approval, `safeTransfer`, or Permit2 as a
|
|
1702
|
+
/// fallback.
|
|
1685
1703
|
/// @param from The address to transfer tokens from.
|
|
1686
1704
|
/// @param to The address to transfer tokens to.
|
|
1687
1705
|
/// @param token The address of the token being transferred.
|
|
@@ -2333,9 +2351,12 @@ contract JBRouterTerminal is
|
|
|
2333
2351
|
// An OOB access in the try-success block panics and is NOT caught by catch{}.
|
|
2334
2352
|
if (tickCumulatives.length >= 2) {
|
|
2335
2353
|
// Derive the arithmetic mean tick: (cumulative_now - cumulative_start) / elapsed_seconds.
|
|
2336
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2337
2354
|
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
|
|
2355
|
+
// The TWAP window is a small protocol constant that fits in int32 and int56.
|
|
2356
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2338
2357
|
int56 period = int56(int32(_TWAP_WINDOW));
|
|
2358
|
+
// The cumulative tick values come from Uniswap observations, whose average tick is int24-bounded.
|
|
2359
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2339
2360
|
tick = int24(tickDelta / period);
|
|
2340
2361
|
// Round towards negative infinity for negative ticks (Uniswap convention).
|
|
2341
2362
|
if (tickDelta < 0 && (tickDelta % period != 0)) tick--;
|
|
@@ -2566,7 +2587,9 @@ contract JBRouterTerminal is
|
|
|
2566
2587
|
});
|
|
2567
2588
|
|
|
2568
2589
|
// Enforce the caller's minimum reclaim amount only on the first hop.
|
|
2569
|
-
if (amount < minTokensReclaimed)
|
|
2590
|
+
if (amount < minTokensReclaimed) {
|
|
2591
|
+
revert JBRouterTerminal_SlippageExceeded({amountOut: amount, minAmountOut: minTokensReclaimed});
|
|
2592
|
+
}
|
|
2570
2593
|
|
|
2571
2594
|
// Clear the reclaim minimum after the first hop because later hops may operate in different token units.
|
|
2572
2595
|
minTokensReclaimed = 0;
|
|
@@ -2622,6 +2645,8 @@ contract JBRouterTerminal is
|
|
|
2622
2645
|
metadata: ""
|
|
2623
2646
|
});
|
|
2624
2647
|
|
|
2648
|
+
// Deployment config makes this router a feeless cash-out beneficiary, so previews use the terminal's raw
|
|
2649
|
+
// reclaim amount and avoid carrying fee-discovery bytecode in the router.
|
|
2625
2650
|
reclaimAmount = _effectivePreviewCashOutAmount(reclaimAmount, hookSpecifications);
|
|
2626
2651
|
}
|
|
2627
2652
|
|
|
@@ -27,7 +27,8 @@ import {IJBForwardingTerminal} from "./interfaces/IJBForwardingTerminal.sol";
|
|
|
27
27
|
import {IJBRouterTerminalRegistry} from "./interfaces/IJBRouterTerminalRegistry.sol";
|
|
28
28
|
import {IJBPayerTracker} from "./interfaces/IJBPayerTracker.sol";
|
|
29
29
|
|
|
30
|
-
/// @notice
|
|
30
|
+
/// @notice A forwarding layer that lets each project choose which router terminal receives its payments, with an
|
|
31
|
+
/// owner-managed default for projects that have not opted in. Projects can lock their choice to guarantee permanence.
|
|
31
32
|
contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned, Ownable, ERC2771Context {
|
|
32
33
|
// A library that adds default safety checks to ERC20 functionality.
|
|
33
34
|
using SafeERC20 for IERC20;
|
|
@@ -154,9 +155,29 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
154
155
|
return terminal.accountingContextsOf(projectId);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
/// @notice
|
|
158
|
-
/// @
|
|
159
|
-
|
|
158
|
+
/// @notice Always returns 0 because the registry only forwards funds and does not hold project balances.
|
|
159
|
+
/// @param projectId Unused.
|
|
160
|
+
/// @param tokens Unused.
|
|
161
|
+
/// @param decimals Unused.
|
|
162
|
+
/// @param currency Unused.
|
|
163
|
+
/// @return currentSurplus Always 0.
|
|
164
|
+
function currentSurplusOf(
|
|
165
|
+
uint256 projectId,
|
|
166
|
+
address[] calldata tokens,
|
|
167
|
+
uint256 decimals,
|
|
168
|
+
uint256 currency
|
|
169
|
+
)
|
|
170
|
+
external
|
|
171
|
+
pure
|
|
172
|
+
override
|
|
173
|
+
returns (uint256)
|
|
174
|
+
{
|
|
175
|
+
projectId;
|
|
176
|
+
tokens;
|
|
177
|
+
decimals;
|
|
178
|
+
currency;
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
160
181
|
|
|
161
182
|
/// @notice Preview a payment by forwarding the call to the terminal currently resolved for the project.
|
|
162
183
|
/// @dev Uses the project-specific terminal when set, otherwise falls back to `defaultTerminal`.
|
|
@@ -226,6 +247,13 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
226
247
|
return super._contextSuffixLength();
|
|
227
248
|
}
|
|
228
249
|
|
|
250
|
+
/// @notice Prevent the registry from forwarding straight back into its immediate caller.
|
|
251
|
+
/// @param terminal The terminal the registry is about to forward into.
|
|
252
|
+
function _enforceNoCircularForward(IJBTerminal terminal) internal view {
|
|
253
|
+
// Reject immediate caller cycles so router -> registry -> same router cannot recurse indefinitely.
|
|
254
|
+
if (msg.sender == address(terminal)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
255
|
+
}
|
|
256
|
+
|
|
229
257
|
/// @notice The calldata. Preferred to use over `msg.data`.
|
|
230
258
|
/// @return calldata The `msg.data` of this call.
|
|
231
259
|
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
@@ -238,6 +266,29 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
238
266
|
return ERC2771Context._msgSender();
|
|
239
267
|
}
|
|
240
268
|
|
|
269
|
+
/// @notice Reject terminal choices that would forward the project back into this registry.
|
|
270
|
+
/// @param projectId The project whose forwarding target is being validated.
|
|
271
|
+
/// @param terminal The terminal being configured or locked.
|
|
272
|
+
function _requireNonCircularTerminalFor(uint256 projectId, IJBTerminal terminal) internal view {
|
|
273
|
+
// Reject direct self-selection so the registry cannot forward a project to itself.
|
|
274
|
+
if (address(terminal) == address(this)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
275
|
+
|
|
276
|
+
// Externally owned accounts cannot implement `terminalOf`, so there is no forwarding route to inspect.
|
|
277
|
+
if (address(terminal).code.length == 0) return;
|
|
278
|
+
|
|
279
|
+
// If the candidate is another forwarding terminal, ask where this project would end up.
|
|
280
|
+
try IJBForwardingTerminal(address(terminal)).terminalOf({projectId: projectId}) returns (
|
|
281
|
+
IJBTerminal downstreamTerminal
|
|
282
|
+
) {
|
|
283
|
+
// Reject one-hop forwarding cycles that bounce this project back into the registry.
|
|
284
|
+
if (address(downstreamTerminal) == address(this)) {
|
|
285
|
+
revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Non-forwarding terminals are valid choices; failed interface probes should not block them.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
241
292
|
/// @notice Resolve the effective terminal for a project, falling back to the default terminal when unset.
|
|
242
293
|
/// @param projectId The project whose terminal should be resolved.
|
|
243
294
|
/// @return terminal The project-specific terminal, or the default terminal if no override exists.
|
|
@@ -249,13 +300,6 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
249
300
|
if (terminal == IJBTerminal(address(0))) terminal = defaultTerminal;
|
|
250
301
|
}
|
|
251
302
|
|
|
252
|
-
/// @notice Prevent the registry from forwarding straight back into its immediate caller.
|
|
253
|
-
/// @param terminal The terminal the registry is about to forward into.
|
|
254
|
-
function _enforceNoCircularForward(IJBTerminal terminal) internal view {
|
|
255
|
-
// Reject immediate caller cycles so router -> registry -> same router cannot recurse indefinitely.
|
|
256
|
-
if (msg.sender == address(terminal)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
303
|
//*********************************************************************//
|
|
260
304
|
// ---------------------- external transactions ---------------------- //
|
|
261
305
|
//*********************************************************************//
|
|
@@ -271,13 +315,14 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
271
315
|
override
|
|
272
316
|
{}
|
|
273
317
|
|
|
274
|
-
/// @notice
|
|
275
|
-
/// @
|
|
318
|
+
/// @notice Add funds to a project's balance by forwarding them through the project's resolved router terminal.
|
|
319
|
+
/// @dev Uses the project-specific terminal when set, otherwise falls back to `defaultTerminal`.
|
|
320
|
+
/// @param projectId The ID of the project receiving the balance addition.
|
|
276
321
|
/// @param token The address of the token being paid in.
|
|
277
322
|
/// @param amount The amount of tokens being paid in.
|
|
278
|
-
/// @param shouldReturnHeldFees
|
|
323
|
+
/// @param shouldReturnHeldFees Whether held fees should be returned based on the amount added.
|
|
279
324
|
/// @param memo A memo to pass along to the emitted event.
|
|
280
|
-
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
325
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format (may include `permit2` allowance data).
|
|
281
326
|
// slither-disable-next-line reentrancy-benign,reentrancy-eth
|
|
282
327
|
function addToBalanceOf(
|
|
283
328
|
uint256 projectId,
|
|
@@ -327,8 +372,8 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
327
372
|
originalPayer = previousPayer;
|
|
328
373
|
}
|
|
329
374
|
|
|
330
|
-
/// @notice
|
|
331
|
-
/// @dev Only the owner can
|
|
375
|
+
/// @notice Add a terminal to the allowlist so projects can select it as their router.
|
|
376
|
+
/// @dev Only the registry owner can call this.
|
|
332
377
|
/// @param terminal The terminal to allow.
|
|
333
378
|
function allowTerminal(IJBTerminal terminal) external onlyOwner {
|
|
334
379
|
// Mark the terminal as selectable for future project-level configuration.
|
|
@@ -338,8 +383,8 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
338
383
|
emit JBRouterTerminalRegistry_AllowTerminal(terminal, _msgSender());
|
|
339
384
|
}
|
|
340
385
|
|
|
341
|
-
/// @notice
|
|
342
|
-
/// @dev Only the owner can
|
|
386
|
+
/// @notice Remove a terminal from the allowlist so no new projects can select it.
|
|
387
|
+
/// @dev Only the registry owner can call this. Cannot disallow the current default terminal — call
|
|
343
388
|
/// `setDefaultTerminal` to change the default first.
|
|
344
389
|
/// @param terminal The terminal to disallow.
|
|
345
390
|
function disallowTerminal(IJBTerminal terminal) external onlyOwner {
|
|
@@ -355,11 +400,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
355
400
|
emit JBRouterTerminalRegistry_DisallowTerminal(terminal, _msgSender());
|
|
356
401
|
}
|
|
357
402
|
|
|
358
|
-
/// @notice
|
|
403
|
+
/// @notice Permanently lock a project's router terminal choice so it can never be changed again.
|
|
359
404
|
/// @dev Only the project's owner or an address with the `JBPermissionIds.SET_ROUTER_TERMINAL` permission can lock.
|
|
360
|
-
/// @dev
|
|
361
|
-
/// permanently brick routing for the project through this registry. Because locks are irreversible, callers must
|
|
362
|
-
/// verify that the terminal is valid and non-circular before locking.
|
|
405
|
+
/// @dev Circular or self-referential terminals are rejected before the irreversible lock is written.
|
|
363
406
|
/// @param projectId The ID of the project to lock the terminal for.
|
|
364
407
|
/// @param expectedTerminal The terminal the caller expects to lock. Prevents race conditions where the default
|
|
365
408
|
/// changes between transaction submission and execution.
|
|
@@ -384,31 +427,46 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
384
427
|
revert JBRouterTerminalRegistry_TerminalMismatch(terminal, expectedTerminal);
|
|
385
428
|
}
|
|
386
429
|
|
|
430
|
+
// Reject a terminal that would make this irreversible lock forward back into the registry.
|
|
431
|
+
_requireNonCircularTerminalFor({projectId: projectId, terminal: terminal});
|
|
432
|
+
|
|
387
433
|
hasLockedTerminal[projectId] = true;
|
|
388
434
|
|
|
389
435
|
emit JBRouterTerminalRegistry_LockTerminal(projectId, _msgSender());
|
|
390
436
|
}
|
|
391
437
|
|
|
392
|
-
/// @notice
|
|
438
|
+
/// @notice Always returns 0 because the registry holds no project balances to migrate.
|
|
439
|
+
/// @param projectId Unused.
|
|
440
|
+
/// @param token Unused.
|
|
441
|
+
/// @param to Unused.
|
|
442
|
+
/// @return balance Always 0.
|
|
393
443
|
function migrateBalanceOf(
|
|
394
444
|
uint256 projectId,
|
|
395
445
|
address token,
|
|
396
446
|
IJBTerminal to
|
|
397
447
|
)
|
|
398
448
|
external
|
|
449
|
+
pure
|
|
399
450
|
override
|
|
400
|
-
returns (uint256
|
|
401
|
-
{
|
|
451
|
+
returns (uint256)
|
|
452
|
+
{
|
|
453
|
+
projectId;
|
|
454
|
+
token;
|
|
455
|
+
to;
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
402
458
|
|
|
403
|
-
/// @notice Pay a project by
|
|
459
|
+
/// @notice Pay a project by accepting the caller's tokens and forwarding them to the project's resolved router
|
|
460
|
+
/// terminal.
|
|
461
|
+
/// @dev Uses the project-specific terminal when set, otherwise falls back to `defaultTerminal`.
|
|
404
462
|
/// @param projectId The ID of the project being paid.
|
|
405
463
|
/// @param token The address of the token being paid in.
|
|
406
464
|
/// @param amount The amount of tokens being paid in.
|
|
407
|
-
/// @param beneficiary The
|
|
465
|
+
/// @param beneficiary The address that will receive any project tokens minted by the destination.
|
|
408
466
|
/// @param minReturnedTokens The minimum number of project tokens expected in return.
|
|
409
467
|
/// @param memo A memo to pass along to the emitted event.
|
|
410
|
-
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
411
|
-
/// @return result The number of tokens
|
|
468
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format (may include `permit2` allowance data).
|
|
469
|
+
/// @return result The number of project tokens minted for the beneficiary.
|
|
412
470
|
// slither-disable-next-line reentrancy-benign,reentrancy-eth
|
|
413
471
|
function pay(
|
|
414
472
|
uint256 projectId,
|
|
@@ -462,11 +520,12 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
462
520
|
originalPayer = previousPayer;
|
|
463
521
|
}
|
|
464
522
|
|
|
465
|
-
/// @notice
|
|
466
|
-
/// @dev Only the owner can
|
|
523
|
+
/// @notice Change the registry-wide default terminal that all projects without an explicit override will use.
|
|
524
|
+
/// @dev Only the registry owner can call this. Automatically allowlists the new default.
|
|
467
525
|
/// @param terminal The terminal to set as the default.
|
|
468
526
|
function setDefaultTerminal(IJBTerminal terminal) external onlyOwner {
|
|
469
527
|
if (address(terminal) == address(0)) revert JBRouterTerminalRegistry_ZeroAddress();
|
|
528
|
+
if (address(terminal) == address(this)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
470
529
|
|
|
471
530
|
defaultTerminal = terminal;
|
|
472
531
|
|
|
@@ -476,7 +535,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
476
535
|
emit JBRouterTerminalRegistry_SetDefaultTerminal(terminal, _msgSender());
|
|
477
536
|
}
|
|
478
537
|
|
|
479
|
-
/// @notice
|
|
538
|
+
/// @notice Choose which router terminal a project's payments are forwarded through.
|
|
480
539
|
/// @dev Only the project's owner or an address with the `JBPermissionIds.SET_ROUTER_TERMINAL` permission can set.
|
|
481
540
|
/// @param projectId The ID of the project to set the terminal for.
|
|
482
541
|
/// @param terminal The terminal to set for the project.
|
|
@@ -486,6 +545,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
486
545
|
|
|
487
546
|
if (!isTerminalAllowed[terminal]) revert JBRouterTerminalRegistry_TerminalNotAllowed(terminal);
|
|
488
547
|
|
|
548
|
+
// Reject a terminal that would forward this project back into the registry before saving it.
|
|
549
|
+
_requireNonCircularTerminalFor({projectId: projectId, terminal: terminal});
|
|
550
|
+
|
|
489
551
|
// Enforce permissions.
|
|
490
552
|
_requirePermissionFrom({
|
|
491
553
|
account: PROJECTS.ownerOf(projectId),
|
|
@@ -571,7 +633,8 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
571
633
|
return 0;
|
|
572
634
|
}
|
|
573
635
|
|
|
574
|
-
/// @notice
|
|
636
|
+
/// @notice Transfer tokens from one address to another using direct approval, `safeTransfer`, or Permit2 as a
|
|
637
|
+
/// fallback.
|
|
575
638
|
/// @param from The address to transfer tokens from.
|
|
576
639
|
/// @param to The address to transfer tokens to.
|
|
577
640
|
/// @param token The address of the token being transferred.
|