@bananapus/router-terminal-v6 0.0.33 → 0.0.34
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 +122 -10
- package/src/JBRouterTerminal.sol +36 -12
- package/src/JBRouterTerminalRegistry.sol +38 -10
- 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.34",
|
|
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,6 +9,7 @@ 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";
|
|
@@ -273,8 +274,17 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
273
274
|
continue;
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
// Decode
|
|
277
|
-
|
|
277
|
+
// Decode the buyback hook's routing metadata. When the hook mints in `afterPayRecordedWith`, the terminal
|
|
278
|
+
// preview returns zero token counts and the router must score the route from the hook's commitments.
|
|
279
|
+
(
|
|
280
|
+
,
|
|
281
|
+
uint256 amountToMintWith,
|
|
282
|
+
uint256 minimumSwapAmountOut,,,
|
|
283
|
+
uint256 tokenCountWithoutHook,,,,,
|
|
284
|
+
uint256 minimumBeneficiaryTokenCount,
|
|
285
|
+
uint256 minimumReservedTokenCount,
|
|
286
|
+
uint256 rawSwapQuote
|
|
287
|
+
) = abi.decode(
|
|
278
288
|
specification.metadata,
|
|
279
289
|
(
|
|
280
290
|
bool,
|
|
@@ -293,14 +303,44 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
293
303
|
)
|
|
294
304
|
);
|
|
295
305
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
// The hook's beneficiary/reserved commitments are only for the AMM leg. If the hook leaves part of the
|
|
307
|
+
// payment to mint directly, estimate that direct-mint leg at the same issuance rate used for the swapped
|
|
308
|
+
// amount so the router compares a whole-route token count against ordinary terminal previews.
|
|
309
|
+
uint256 directMintTokenCount;
|
|
310
|
+
if (amountToMintWith != 0 && specification.amount != 0 && tokenCountWithoutHook != 0) {
|
|
311
|
+
directMintTokenCount =
|
|
312
|
+
mulDiv({x: amountToMintWith, y: tokenCountWithoutHook, denominator: specification.amount});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Score the executable floor first. This supports callers that only provide a minimum and no live quote.
|
|
316
|
+
(uint256 candidateBeneficiaryTokenCount, uint256 candidateReservedTokenCount) = _scaledPreviewPayTokenCounts({
|
|
317
|
+
tokenCount: minimumSwapAmountOut + directMintTokenCount,
|
|
318
|
+
referenceTokenCount: minimumSwapAmountOut,
|
|
319
|
+
referenceBeneficiaryTokenCount: minimumBeneficiaryTokenCount,
|
|
320
|
+
referenceReservedTokenCount: minimumReservedTokenCount
|
|
321
|
+
});
|
|
322
|
+
(effectiveBeneficiaryTokenCount, effectiveReservedTokenCount) = _strongerPreviewPayTokenCounts({
|
|
323
|
+
currentBeneficiaryTokenCount: effectiveBeneficiaryTokenCount,
|
|
324
|
+
currentReservedTokenCount: effectiveReservedTokenCount,
|
|
325
|
+
candidateBeneficiaryTokenCount: candidateBeneficiaryTokenCount,
|
|
326
|
+
candidateReservedTokenCount: candidateReservedTokenCount
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// If the hook also surfaced a stronger live quote, score it too. This lets programmatic buyback routes win
|
|
330
|
+
// when the expected executable output is better than the conservative minimum.
|
|
331
|
+
if (rawSwapQuote > minimumSwapAmountOut) {
|
|
332
|
+
(candidateBeneficiaryTokenCount, candidateReservedTokenCount) = _scaledPreviewPayTokenCounts({
|
|
333
|
+
tokenCount: rawSwapQuote + directMintTokenCount,
|
|
334
|
+
referenceTokenCount: minimumSwapAmountOut,
|
|
335
|
+
referenceBeneficiaryTokenCount: minimumBeneficiaryTokenCount,
|
|
336
|
+
referenceReservedTokenCount: minimumReservedTokenCount
|
|
337
|
+
});
|
|
338
|
+
(effectiveBeneficiaryTokenCount, effectiveReservedTokenCount) = _strongerPreviewPayTokenCounts({
|
|
339
|
+
currentBeneficiaryTokenCount: effectiveBeneficiaryTokenCount,
|
|
340
|
+
currentReservedTokenCount: effectiveReservedTokenCount,
|
|
341
|
+
candidateBeneficiaryTokenCount: candidateBeneficiaryTokenCount,
|
|
342
|
+
candidateReservedTokenCount: candidateReservedTokenCount
|
|
343
|
+
});
|
|
304
344
|
}
|
|
305
345
|
unchecked {
|
|
306
346
|
++i;
|
|
@@ -668,6 +708,45 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
668
708
|
return abi.decode(data, (IJBTerminal[]));
|
|
669
709
|
}
|
|
670
710
|
|
|
711
|
+
/// @notice Scale a known beneficiary/reserved token split to a different total token count.
|
|
712
|
+
/// @param tokenCount The total token count being scored.
|
|
713
|
+
/// @param referenceTokenCount The total token count the reference split was computed from.
|
|
714
|
+
/// @param referenceBeneficiaryTokenCount The beneficiary share of the reference split.
|
|
715
|
+
/// @param referenceReservedTokenCount The reserved share of the reference split.
|
|
716
|
+
/// @return beneficiaryTokenCount The scaled beneficiary token count.
|
|
717
|
+
/// @return reservedTokenCount The scaled reserved token count.
|
|
718
|
+
function _scaledPreviewPayTokenCounts(
|
|
719
|
+
uint256 tokenCount,
|
|
720
|
+
uint256 referenceTokenCount,
|
|
721
|
+
uint256 referenceBeneficiaryTokenCount,
|
|
722
|
+
uint256 referenceReservedTokenCount
|
|
723
|
+
)
|
|
724
|
+
internal
|
|
725
|
+
pure
|
|
726
|
+
returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
|
|
727
|
+
{
|
|
728
|
+
// A zero candidate means there is no stronger route output to scale, so preserve the known reference split.
|
|
729
|
+
if (tokenCount == 0) {
|
|
730
|
+
return (referenceBeneficiaryTokenCount, referenceReservedTokenCount);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Prefer the already-previewed beneficiary/reserved total because it includes the destination's reserve logic.
|
|
734
|
+
uint256 referenceTotal = referenceBeneficiaryTokenCount + referenceReservedTokenCount;
|
|
735
|
+
|
|
736
|
+
// Fall back to the original token count when previewed counts were unavailable but the hook reported a floor.
|
|
737
|
+
if (referenceTotal == 0) referenceTotal = referenceTokenCount;
|
|
738
|
+
|
|
739
|
+
// If both reference totals are zero, treat the whole candidate as beneficiary tokens so the route stays
|
|
740
|
+
// comparable instead of disappearing from scoring.
|
|
741
|
+
if (referenceTotal == 0) return (tokenCount, 0);
|
|
742
|
+
|
|
743
|
+
// Scale the beneficiary share proportionally from the reference split to the candidate total being scored.
|
|
744
|
+
beneficiaryTokenCount = mulDiv({x: tokenCount, y: referenceBeneficiaryTokenCount, denominator: referenceTotal});
|
|
745
|
+
|
|
746
|
+
// Assign the residual to reserved tokens so rounding cannot lose supply during route comparison.
|
|
747
|
+
reservedTokenCount = tokenCount - beneficiaryTokenCount;
|
|
748
|
+
}
|
|
749
|
+
|
|
671
750
|
/// @notice Resolve whether the current route input should first be treated as a project-token cashout source.
|
|
672
751
|
/// @param router The router terminal whose project-token lookup should be used.
|
|
673
752
|
/// @param tokenIn The current route input token.
|
|
@@ -698,6 +777,39 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
698
777
|
}
|
|
699
778
|
}
|
|
700
779
|
|
|
780
|
+
/// @notice Choose the stronger preview outcome using beneficiary tokens first and reserved tokens as a tie-break.
|
|
781
|
+
/// @param currentBeneficiaryTokenCount The beneficiary token count from the strongest route so far.
|
|
782
|
+
/// @param currentReservedTokenCount The reserved token count from the strongest route so far.
|
|
783
|
+
/// @param candidateBeneficiaryTokenCount The beneficiary token count from the candidate route.
|
|
784
|
+
/// @param candidateReservedTokenCount The reserved token count from the candidate route.
|
|
785
|
+
/// @return beneficiaryTokenCount The beneficiary token count to keep.
|
|
786
|
+
/// @return reservedTokenCount The reserved token count to keep.
|
|
787
|
+
function _strongerPreviewPayTokenCounts(
|
|
788
|
+
uint256 currentBeneficiaryTokenCount,
|
|
789
|
+
uint256 currentReservedTokenCount,
|
|
790
|
+
uint256 candidateBeneficiaryTokenCount,
|
|
791
|
+
uint256 candidateReservedTokenCount
|
|
792
|
+
)
|
|
793
|
+
internal
|
|
794
|
+
pure
|
|
795
|
+
returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
|
|
796
|
+
{
|
|
797
|
+
// Prefer the route that gives the beneficiary more tokens, since that is the user's primary output.
|
|
798
|
+
if (
|
|
799
|
+
candidateBeneficiaryTokenCount > currentBeneficiaryTokenCount
|
|
800
|
+
|| (
|
|
801
|
+
// When beneficiary output ties, keep the route that also mints more reserved tokens.
|
|
802
|
+
candidateBeneficiaryTokenCount == currentBeneficiaryTokenCount
|
|
803
|
+
&& candidateReservedTokenCount > currentReservedTokenCount
|
|
804
|
+
)
|
|
805
|
+
) {
|
|
806
|
+
return (candidateBeneficiaryTokenCount, candidateReservedTokenCount);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Keep the current winner when the candidate does not improve beneficiary output or the reserved tie-break.
|
|
810
|
+
return (currentBeneficiaryTokenCount, currentReservedTokenCount);
|
|
811
|
+
}
|
|
812
|
+
|
|
701
813
|
/// @notice Resolve the usable primary terminal for a discovered candidate token.
|
|
702
814
|
/// @param router The router whose circular-terminal rule should be applied.
|
|
703
815
|
/// @param directory The directory used to resolve primary terminals.
|
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -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().
|
|
@@ -2333,9 +2350,12 @@ contract JBRouterTerminal is
|
|
|
2333
2350
|
// An OOB access in the try-success block panics and is NOT caught by catch{}.
|
|
2334
2351
|
if (tickCumulatives.length >= 2) {
|
|
2335
2352
|
// Derive the arithmetic mean tick: (cumulative_now - cumulative_start) / elapsed_seconds.
|
|
2336
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2337
2353
|
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
|
|
2354
|
+
// The TWAP window is a small protocol constant that fits in int32 and int56.
|
|
2355
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2338
2356
|
int56 period = int56(int32(_TWAP_WINDOW));
|
|
2357
|
+
// The cumulative tick values come from Uniswap observations, whose average tick is int24-bounded.
|
|
2358
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
2339
2359
|
tick = int24(tickDelta / period);
|
|
2340
2360
|
// Round towards negative infinity for negative ticks (Uniswap convention).
|
|
2341
2361
|
if (tickDelta < 0 && (tickDelta % period != 0)) tick--;
|
|
@@ -2566,7 +2586,9 @@ contract JBRouterTerminal is
|
|
|
2566
2586
|
});
|
|
2567
2587
|
|
|
2568
2588
|
// Enforce the caller's minimum reclaim amount only on the first hop.
|
|
2569
|
-
if (amount < minTokensReclaimed)
|
|
2589
|
+
if (amount < minTokensReclaimed) {
|
|
2590
|
+
revert JBRouterTerminal_SlippageExceeded({amountOut: amount, minAmountOut: minTokensReclaimed});
|
|
2591
|
+
}
|
|
2570
2592
|
|
|
2571
2593
|
// Clear the reclaim minimum after the first hop because later hops may operate in different token units.
|
|
2572
2594
|
minTokensReclaimed = 0;
|
|
@@ -2622,6 +2644,8 @@ contract JBRouterTerminal is
|
|
|
2622
2644
|
metadata: ""
|
|
2623
2645
|
});
|
|
2624
2646
|
|
|
2647
|
+
// Deployment config makes this router a feeless cash-out beneficiary, so previews use the terminal's raw
|
|
2648
|
+
// reclaim amount and avoid carrying fee-discovery bytecode in the router.
|
|
2625
2649
|
reclaimAmount = _effectivePreviewCashOutAmount(reclaimAmount, hookSpecifications);
|
|
2626
2650
|
}
|
|
2627
2651
|
|
|
@@ -226,6 +226,13 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
226
226
|
return super._contextSuffixLength();
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/// @notice Prevent the registry from forwarding straight back into its immediate caller.
|
|
230
|
+
/// @param terminal The terminal the registry is about to forward into.
|
|
231
|
+
function _enforceNoCircularForward(IJBTerminal terminal) internal view {
|
|
232
|
+
// Reject immediate caller cycles so router -> registry -> same router cannot recurse indefinitely.
|
|
233
|
+
if (msg.sender == address(terminal)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
234
|
+
}
|
|
235
|
+
|
|
229
236
|
/// @notice The calldata. Preferred to use over `msg.data`.
|
|
230
237
|
/// @return calldata The `msg.data` of this call.
|
|
231
238
|
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
@@ -238,6 +245,29 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
238
245
|
return ERC2771Context._msgSender();
|
|
239
246
|
}
|
|
240
247
|
|
|
248
|
+
/// @notice Reject terminal choices that would forward the project back into this registry.
|
|
249
|
+
/// @param projectId The project whose forwarding target is being validated.
|
|
250
|
+
/// @param terminal The terminal being configured or locked.
|
|
251
|
+
function _requireNonCircularTerminalFor(uint256 projectId, IJBTerminal terminal) internal view {
|
|
252
|
+
// Reject direct self-selection so the registry cannot forward a project to itself.
|
|
253
|
+
if (address(terminal) == address(this)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
254
|
+
|
|
255
|
+
// Externally owned accounts cannot implement `terminalOf`, so there is no forwarding route to inspect.
|
|
256
|
+
if (address(terminal).code.length == 0) return;
|
|
257
|
+
|
|
258
|
+
// If the candidate is another forwarding terminal, ask where this project would end up.
|
|
259
|
+
try IJBForwardingTerminal(address(terminal)).terminalOf({projectId: projectId}) returns (
|
|
260
|
+
IJBTerminal downstreamTerminal
|
|
261
|
+
) {
|
|
262
|
+
// Reject one-hop forwarding cycles that bounce this project back into the registry.
|
|
263
|
+
if (address(downstreamTerminal) == address(this)) {
|
|
264
|
+
revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Non-forwarding terminals are valid choices; failed interface probes should not block them.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
241
271
|
/// @notice Resolve the effective terminal for a project, falling back to the default terminal when unset.
|
|
242
272
|
/// @param projectId The project whose terminal should be resolved.
|
|
243
273
|
/// @return terminal The project-specific terminal, or the default terminal if no override exists.
|
|
@@ -249,13 +279,6 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
249
279
|
if (terminal == IJBTerminal(address(0))) terminal = defaultTerminal;
|
|
250
280
|
}
|
|
251
281
|
|
|
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
282
|
//*********************************************************************//
|
|
260
283
|
// ---------------------- external transactions ---------------------- //
|
|
261
284
|
//*********************************************************************//
|
|
@@ -357,9 +380,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
357
380
|
|
|
358
381
|
/// @notice Lock a terminal for a project.
|
|
359
382
|
/// @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.
|
|
383
|
+
/// @dev Circular or self-referential terminals are rejected before the irreversible lock is written.
|
|
363
384
|
/// @param projectId The ID of the project to lock the terminal for.
|
|
364
385
|
/// @param expectedTerminal The terminal the caller expects to lock. Prevents race conditions where the default
|
|
365
386
|
/// changes between transaction submission and execution.
|
|
@@ -384,6 +405,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
384
405
|
revert JBRouterTerminalRegistry_TerminalMismatch(terminal, expectedTerminal);
|
|
385
406
|
}
|
|
386
407
|
|
|
408
|
+
// Reject a terminal that would make this irreversible lock forward back into the registry.
|
|
409
|
+
_requireNonCircularTerminalFor({projectId: projectId, terminal: terminal});
|
|
410
|
+
|
|
387
411
|
hasLockedTerminal[projectId] = true;
|
|
388
412
|
|
|
389
413
|
emit JBRouterTerminalRegistry_LockTerminal(projectId, _msgSender());
|
|
@@ -467,6 +491,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
467
491
|
/// @param terminal The terminal to set as the default.
|
|
468
492
|
function setDefaultTerminal(IJBTerminal terminal) external onlyOwner {
|
|
469
493
|
if (address(terminal) == address(0)) revert JBRouterTerminalRegistry_ZeroAddress();
|
|
494
|
+
if (address(terminal) == address(this)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
470
495
|
|
|
471
496
|
defaultTerminal = terminal;
|
|
472
497
|
|
|
@@ -486,6 +511,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
486
511
|
|
|
487
512
|
if (!isTerminalAllowed[terminal]) revert JBRouterTerminalRegistry_TerminalNotAllowed(terminal);
|
|
488
513
|
|
|
514
|
+
// Reject a terminal that would forward this project back into the registry before saving it.
|
|
515
|
+
_requireNonCircularTerminalFor({projectId: projectId, terminal: terminal});
|
|
516
|
+
|
|
489
517
|
// Enforce permissions.
|
|
490
518
|
_requirePermissionFrom({
|
|
491
519
|
account: PROJECTS.ownerOf(projectId),
|
package/ADMINISTRATION.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Administration
|
|
2
|
-
|
|
3
|
-
## At A Glance
|
|
4
|
-
|
|
5
|
-
| Item | Details |
|
|
6
|
-
| --- | --- |
|
|
7
|
-
| Scope | Global router terminal allowlisting and project-local terminal selection |
|
|
8
|
-
| Control posture | Mixed registry-owner and project-local delegated control |
|
|
9
|
-
| Highest-risk actions | Changing the default terminal, locking a project to the wrong terminal, and relying on misconfigured credit-cashout routing |
|
|
10
|
-
| Recovery posture | Unlocked projects can move; locked projects and immutable router wiring limit recovery |
|
|
11
|
-
|
|
12
|
-
## Purpose
|
|
13
|
-
|
|
14
|
-
`nana-router-terminal-v6` splits administration between a global registry and project-local terminal selection. The router logic itself is mostly immutable. The mutable control plane lives in `JBRouterTerminalRegistry`.
|
|
15
|
-
|
|
16
|
-
## Control Model
|
|
17
|
-
|
|
18
|
-
- `JBRouterTerminalRegistry` is globally `Ownable`
|
|
19
|
-
- project owners or delegates choose and can lock their router terminal
|
|
20
|
-
- `JBRouterTerminal` has immutable routing dependencies and no owner-controlled strategy knobs
|
|
21
|
-
- some transaction paths depend on project-local `JBPermissions`, such as `TRANSFER_CREDITS`
|
|
22
|
-
|
|
23
|
-
## Roles
|
|
24
|
-
|
|
25
|
-
| Role | How Assigned | Scope | Notes |
|
|
26
|
-
| --- | --- | --- | --- |
|
|
27
|
-
| Registry owner | `Ownable(owner)` | Global | Controls allowlist and default terminal |
|
|
28
|
-
| Project owner | `JBProjects.ownerOf(projectId)` | Per project | May delegate `SET_ROUTER_TERMINAL` |
|
|
29
|
-
| Terminal delegate | `JBPermissions` grant | Per project | Usually `SET_ROUTER_TERMINAL` |
|
|
30
|
-
| Payer | Per transaction | Per payment | May need `TRANSFER_CREDITS` for credit-cashout routing |
|
|
31
|
-
|
|
32
|
-
## Privileged Surfaces
|
|
33
|
-
|
|
34
|
-
| Contract | Function | Who Can Call | Effect |
|
|
35
|
-
| --- | --- | --- | --- |
|
|
36
|
-
| `JBRouterTerminalRegistry` | `allowTerminal(...)`, `disallowTerminal(...)`, `setDefaultTerminal(...)` | Registry owner | Controls global terminal availability and fallback |
|
|
37
|
-
| `JBRouterTerminalRegistry` | `setTerminalFor(...)` | Project owner or `SET_ROUTER_TERMINAL` delegate | Sets a project's explicit router terminal |
|
|
38
|
-
| `JBRouterTerminalRegistry` | `lockTerminalFor(...)` | Project owner or `SET_ROUTER_TERMINAL` delegate | Irreversibly locks the resolved terminal for a project |
|
|
39
|
-
|
|
40
|
-
## Immutable And One-Way
|
|
41
|
-
|
|
42
|
-
- `lockTerminalFor(...)` is irreversible
|
|
43
|
-
- constructor dependencies on the router are immutable
|
|
44
|
-
- the current default terminal must move before the old default can be disallowed
|
|
45
|
-
|
|
46
|
-
## Operational Notes
|
|
47
|
-
|
|
48
|
-
- keep the terminal allowlist small and explicit
|
|
49
|
-
- change the default terminal carefully because unconfigured projects inherit it
|
|
50
|
-
- encourage projects to lock only after validating the resolved terminal and routing behavior
|
|
51
|
-
- review credit-cashout routing permissions before relying on that path
|
|
52
|
-
- distinguish configuration risk from quote-quality risk
|
|
53
|
-
|
|
54
|
-
## Machine Notes
|
|
55
|
-
|
|
56
|
-
- do not treat registry ownership as authority to override a locked project choice
|
|
57
|
-
- inspect `src/JBRouterTerminalRegistry.sol` and `src/JBRouterTerminal.sol` separately; they govern different control boundaries
|
|
58
|
-
- if effective terminal resolution and the documented default differ, resolve registry state before further actions
|
|
59
|
-
- if route previews are falling back to weaker discovery or quote paths, do not describe the router as offering uniform oracle-quality guarantees
|
|
60
|
-
|
|
61
|
-
## Recovery
|
|
62
|
-
|
|
63
|
-
- unlocked projects can switch to another allowlisted terminal
|
|
64
|
-
- locked projects cannot be unlocked by the registry
|
|
65
|
-
- bad immutable router behavior means replacement infrastructure, not in-place edits
|
|
66
|
-
- quote-path weakness is usually mitigated operationally with better pool choice, external quoting, or replacement routing infrastructure
|
|
67
|
-
|
|
68
|
-
## Admin Boundaries
|
|
69
|
-
|
|
70
|
-
- the registry owner cannot unlock or override a locked project terminal
|
|
71
|
-
- project operators cannot set a terminal that the registry does not allow
|
|
72
|
-
- router maintainers cannot tune routing heuristics or constructor immutables post-deploy
|
|
73
|
-
- there is no pause surface in the registry or router
|
|
74
|
-
|
|
75
|
-
## Source Map
|
|
76
|
-
|
|
77
|
-
- `src/JBRouterTerminalRegistry.sol`
|
|
78
|
-
- `src/JBRouterTerminal.sol`
|
|
79
|
-
- `src/JBPayRouteResolver.sol`
|
|
80
|
-
- `test/`
|
package/ARCHITECTURE.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
`nana-router-terminal-v6` lets a payer fund a Juicebox project with a token the project does not directly accept. It discovers the destination token, wraps or unwraps native assets when needed, can recursively cash out upstream JB project tokens, and swaps through bounded Uniswap V3 or V4 routes before forwarding value to the destination terminal.
|
|
6
|
-
|
|
7
|
-
The router is intentionally heuristic. It does not search every possible route for a globally optimal price.
|
|
8
|
-
|
|
9
|
-
## System Overview
|
|
10
|
-
|
|
11
|
-
`JBRouterTerminal` is a terminal-shaped adapter, not an accounting source of truth. `JBRouterTerminalRegistry` is both a registry and a stable project-facing proxy surface: projects can point at the registry while the registry resolves, and can later lock, the actual router terminal implementation to use. `JBPayRouteResolver` expands preview candidates without forcing the main router contract to carry all preview complexity inline.
|
|
12
|
-
|
|
13
|
-
Final accounting still happens in the downstream terminal selected through `nana-core-v6`.
|
|
14
|
-
|
|
15
|
-
## Core Invariants
|
|
16
|
-
|
|
17
|
-
- the router's own accounting context is synthetic and must not be treated as the project ledger
|
|
18
|
-
- preview route discovery and live execution must stay aligned
|
|
19
|
-
- refund behavior is part of correctness, not just UX
|
|
20
|
-
- registry locking prevents silent migration to untrusted router implementations
|
|
21
|
-
- final terminal-facing ERC-20 hops only support standard, non-lossy transfers
|
|
22
|
-
- recursive project-token cashout routing is intentionally bounded
|
|
23
|
-
- caller reclaim minima only apply to the first cashout hop, because later hops may change token units
|
|
24
|
-
- circular `router -> registry -> same router` forwarding remains blocked in the registry
|
|
25
|
-
|
|
26
|
-
## Modules
|
|
27
|
-
|
|
28
|
-
| Module | Responsibility | Notes |
|
|
29
|
-
| --- | --- | --- |
|
|
30
|
-
| `JBRouterTerminal` | Intake, route discovery, swap execution, forwarding, and refunds | Main runtime surface |
|
|
31
|
-
| `JBRouterTerminalRegistry` | Project-level router selection, locking, and proxy forwarding to the resolved router terminal | Governance, safety, and proxy surface |
|
|
32
|
-
| `JBPayRouteResolver` | Preview candidate evaluation | Helper to keep runtime size bounded |
|
|
33
|
-
| `JBSwapLib` and routing structs | Pool discovery, quoting, and route metadata | Shared routing logic |
|
|
34
|
-
|
|
35
|
-
## Trust Boundaries
|
|
36
|
-
|
|
37
|
-
- final accounting remains in the downstream terminal selected through `JBDirectory`
|
|
38
|
-
- the router trusts Uniswap V3, Uniswap V4, Permit2, and optional payer trackers for routing-side behavior
|
|
39
|
-
- fee-on-transfer tokens are only tolerated on ingress where received-balance deltas can be reconciled
|
|
40
|
-
- the registry is trusted to resolve and forward into the intended router implementation for a project
|
|
41
|
-
|
|
42
|
-
## Critical Flows
|
|
43
|
-
|
|
44
|
-
### Route And Pay
|
|
45
|
-
|
|
46
|
-
```text
|
|
47
|
-
router pay call
|
|
48
|
-
-> accept native, ERC-20, or JB-token-like input
|
|
49
|
-
-> if input is a project token, recursively cash it out first
|
|
50
|
-
-> resolve the destination token the project terminal actually accepts
|
|
51
|
-
-> choose the best direct, wrap/unwrap, or swap path under the router's bounded candidate-discovery heuristic
|
|
52
|
-
-> execute the route and forward the result to the downstream terminal
|
|
53
|
-
-> refund leftover input when possible
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Accounting Model
|
|
57
|
-
|
|
58
|
-
The router does not own project balances. It owns transient route accounting: input reconciliation, swap execution, forwarded amount, and refund resolution.
|
|
59
|
-
|
|
60
|
-
Preview and execution share the same conceptual route shape: optional recursive cashout first, then destination-token resolution, then final conversion and forwarding.
|
|
61
|
-
|
|
62
|
-
## Security Model
|
|
63
|
-
|
|
64
|
-
- native-asset handling and refunds are the most failure-prone paths
|
|
65
|
-
- V3 and V4 discovery must stay synchronized between preview and live execution
|
|
66
|
-
- V4 discovery intentionally considers both vanilla pools and pools using the canonical `UNIV4_HOOK`
|
|
67
|
-
- the router's "best route" claim is only as strong as its bounded discovery set and external-terminal safety checks
|
|
68
|
-
- recursive cashout behavior, preferred-token handling, and one-shot source overrides are tightly coupled
|
|
69
|
-
|
|
70
|
-
## Safe Change Guide
|
|
71
|
-
|
|
72
|
-
- keep route discovery and route execution semantics paired
|
|
73
|
-
- be conservative with native wrapping, unwrapping, and refund behavior
|
|
74
|
-
- if recursive cash-out logic changes, review hop limits and failure handling together
|
|
75
|
-
- if metadata semantics change, re-check first-hop reclaim minima, one-shot source overrides, and preferred-token routing together
|
|
76
|
-
- do not turn the router into a persistent treasury layer
|
|
77
|
-
|
|
78
|
-
## Canonical Checks
|
|
79
|
-
|
|
80
|
-
- bounded recursive cash-out behavior:
|
|
81
|
-
`test/regression/CashOutLoopLimit.t.sol`
|
|
82
|
-
- preview versus execution terminal alignment:
|
|
83
|
-
`test/audit/PreviewPrimaryTerminalMismatch.t.sol`
|
|
84
|
-
- router-wide route and refund invariants:
|
|
85
|
-
`test/invariant/RouterTerminalInvariant.t.sol`
|
|
86
|
-
|
|
87
|
-
## Source Map
|
|
88
|
-
|
|
89
|
-
- `src/JBRouterTerminal.sol`
|
|
90
|
-
- `src/JBRouterTerminalRegistry.sol`
|
|
91
|
-
- `src/JBPayRouteResolver.sol`
|
|
92
|
-
- `test/regression/CashOutLoopLimit.t.sol`
|
|
93
|
-
- `test/audit/PreviewPrimaryTerminalMismatch.t.sol`
|
|
94
|
-
- `test/invariant/RouterTerminalInvariant.t.sol`
|