Skip to content

Commit 545a5d2

Browse files
ccashwellhensha256claudedianakocsis
authored
fix: revert on exact-output partial fills in V4Router (#564)
* fix: revert on exact-output partial fills in V4Router A v4 exact-output swap stops at the hardcoded global price limit, so a pool that runs out of liquidity before then delivers LESS than the requested amountOut. V4Router only bounded the input (amountInMaximum) and never checked the realized output, so an under-filled exact-output swap silently succeeded with a short delivery (and the per-hop price guard, computed from the requested amount, could pass at a worse-than- bounded realized price). Make exact output all-or-nothing: _swap returns the BalanceDelta, and _swapExactOutputSingle/_swapExactOutput assert the realized output covers the requested amount, reverting V4ExactOutputUnfilled on a shortfall (per hop for multi-hop). Over-delivery (possible only via hook pools) is allowed. Exact-input and full fills are unchanged. The two PermissionedV4Router amountInMaximum tests requested 1e18 from a pool that can deliver at most 19992 out; they relied on the old silent underfill tripping amountInMaximum incidentally. Point them at a fillable amount so they still exercise the V4TooMuchRequested guard. Gas snapshots regenerated (isolate mode) for the router bytecode change. * fix: only assert exact-output fill on the total, not per intermediate hop The exact-output multihop loop runs backwards, so the first iteration (i == pathLength) is the last hop in the path — the one delivering the caller's final currencyOut, where amountOut equals the total requested output. That is the only delivery a partial fill can silently shrink, so it is the only one that needs an explicit V4ExactOutputUnfilled guard. An intermediate hop that underfills needs no check here: it leaves a non-zero intermediate-currency delta that fails to settle, reverting the whole swap. Gating the assert on the first iteration drops the redundant per-hop checks at a negligible (~40 gas) cost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G51nAjHMDEJyYWW54fTtNd * fix: assert exact-output fill on every hop, not just the last Reverts the narrowing from 7b2ea37. That commit gated the underfill assert on `i == pathLength`, reasoning that an intermediate hop needs no explicit check because a shortfall "leaves a non-zero intermediate-currency delta that fails to settle, reverting the whole swap." That only holds if the intermediate currency's delta is otherwise zero at the end of the unlock. PoolManager keeps one transient delta per (target, currency) for the entire unlock, so any same-currency credit absorbs the shortfall and it settles silently. Two ways to hit it: - A path that repeats a currency (A -> B -> A -> C): the missing A merges into the route's own A input debt. amountInMaximum is checked after the loop against `amountIn`, which only ever holds the LAST executed hop's input, so the merged shortfall is never compared against the cap. A route quoted at 0.507 with amountInMaximum 0.8 spent 0.902 while delivering exactly 1.0 out, no revert. - Batched actions where an earlier action's output currency is a later route's intermediate currency: the shortfall nets against that credit and the later route silently spends it as unbounded extra input. Both contradict the all-or-nothing guarantee documented on V4ExactOutputUnfilled (IV4Router.sol:26-28), which currently only holds for the final hop. The gas rationale was also inverted: `i == pathLength &&` adds a comparison on every iteration to skip one cheap comparison, so it cost gas rather than saving it. Removing it is 31/39/47 gas cheaper at 1/2/3 hops and 11 bytes smaller. Snapshots regenerated (isolate mode); exact-input is unaffected. Also corrects the stale "output delta will always be negative" comment: the refactor reads the output side via _swapOutput, which is positive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Alice Henshaw <henshawalice@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: dianakocsis <diana.kocsis@uniswap.org>
1 parent 3245c3c commit 545a5d2

7 files changed

Lines changed: 200 additions & 61 deletions

File tree

snapshots/PaymentsTests.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"Payments_swap_settleFromCaller_takeAllToMsgSender": "133163",
3-
"Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134632",
4-
"Payments_swap_settleWithBalance_takeAllToMsgSender": "127114",
5-
"Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127252"
2+
"Payments_swap_settleFromCaller_takeAllToMsgSender": "133387",
3+
"Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134856",
4+
"Payments_swap_settleWithBalance_takeAllToMsgSender": "127338",
5+
"Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127476"
66
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"PermissionedV4Router_ExactInputSingle_PermissionedTokens": "242611"
2+
"PermissionedV4Router_ExactInputSingle_PermissionedTokens": "242864"
33
}

snapshots/V4RouterTest.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
{
2-
"V4Router_Bytecode": "9950",
3-
"V4Router_ExactIn1Hop_nativeIn": "122222",
4-
"V4Router_ExactIn1Hop_nativeOut": "120688",
5-
"V4Router_ExactIn1Hop_oneForZero": "129560",
6-
"V4Router_ExactIn1Hop_zeroForOne": "136086",
7-
"V4Router_ExactIn2Hops": "193444",
8-
"V4Router_ExactIn2Hops_nativeIn": "179580",
9-
"V4Router_ExactIn3Hops": "250829",
10-
"V4Router_ExactIn3Hops_nativeIn": "236965",
11-
"V4Router_ExactInputSingle": "134158",
12-
"V4Router_ExactInputSingle_nativeIn": "120294",
13-
"V4Router_ExactInputSingle_nativeOut": "118738",
14-
"V4Router_ExactOut1Hop_nativeIn_sweepETH": "128509",
15-
"V4Router_ExactOut1Hop_nativeOut": "121898",
16-
"V4Router_ExactOut1Hop_oneForZero": "130770",
17-
"V4Router_ExactOut1Hop_zeroForOne": "135427",
18-
"V4Router_ExactOut2Hops": "191892",
19-
"V4Router_ExactOut2Hops_nativeIn": "184974",
20-
"V4Router_ExactOut3Hops": "248399",
21-
"V4Router_ExactOut3Hops_nativeIn": "241481",
22-
"V4Router_ExactOut3Hops_nativeOut": "225554",
23-
"V4Router_ExactOutputSingle": "133494",
24-
"V4Router_ExactOutputSingle_nativeIn_sweepETH": "126576",
25-
"V4Router_ExactOutputSingle_nativeOut": "119978",
26-
"router initcode hash (without constructor params, as uint256)": "19267777467363212309328806064985146558623592810537866695556902731584515865783"
2+
"V4Router_Bytecode": "10415",
3+
"V4Router_ExactIn1Hop_nativeIn": "122326",
4+
"V4Router_ExactIn1Hop_nativeOut": "120793",
5+
"V4Router_ExactIn1Hop_oneForZero": "129665",
6+
"V4Router_ExactIn1Hop_zeroForOne": "136190",
7+
"V4Router_ExactIn2Hops": "193652",
8+
"V4Router_ExactIn2Hops_nativeIn": "179788",
9+
"V4Router_ExactIn3Hops": "251141",
10+
"V4Router_ExactIn3Hops_nativeIn": "237277",
11+
"V4Router_ExactInputSingle": "134382",
12+
"V4Router_ExactInputSingle_nativeIn": "120518",
13+
"V4Router_ExactInputSingle_nativeOut": "118963",
14+
"V4Router_ExactOut1Hop_nativeIn_sweepETH": "128900",
15+
"V4Router_ExactOut1Hop_nativeOut": "122321",
16+
"V4Router_ExactOut1Hop_oneForZero": "131193",
17+
"V4Router_ExactOut1Hop_zeroForOne": "135818",
18+
"V4Router_ExactOut2Hops": "192674",
19+
"V4Router_ExactOut2Hops_nativeIn": "185756",
20+
"V4Router_ExactOut3Hops": "249572",
21+
"V4Router_ExactOut3Hops_nativeIn": "242654",
22+
"V4Router_ExactOut3Hops_nativeOut": "226823",
23+
"V4Router_ExactOutputSingle": "134050",
24+
"V4Router_ExactOutputSingle_nativeIn_sweepETH": "127132",
25+
"V4Router_ExactOutputSingle_nativeOut": "120566",
26+
"router initcode hash (without constructor params, as uint256)": "1906760093307030626709390179699396653508205060645061537790918706363253111413"
2727
}

src/V4Router.sol

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
8787
amountIn =
8888
_getFullCredit(params.zeroForOne ? params.poolKey.currency0 : params.poolKey.currency1).toUint128();
8989
}
90-
uint128 amountOut =
91-
_swap(params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.hookData).toUint128();
90+
uint128 amountOut = _swapOutput(
91+
_swap(params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.hookData), params.zeroForOne
92+
);
9293
if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut);
9394
if (params.minHopPriceX36 != 0) {
9495
uint256 priceX36 = uint256(amountOut) * PRECISION / amountIn;
@@ -115,7 +116,8 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
115116
pathKey = params.path[i];
116117
(PoolKey memory poolKey, bool zeroForOne) = pathKey.getPoolAndSwapDirection(currencyIn);
117118
// The output delta will always be positive, except for when interacting with certain hook pools
118-
amountOut = _swap(poolKey, zeroForOne, -int256(uint256(amountIn)), pathKey.hookData).toUint128();
119+
amountOut =
120+
_swapOutput(_swap(poolKey, zeroForOne, -int256(uint256(amountIn)), pathKey.hookData), zeroForOne);
119121

120122
if (perHopPriceLength != 0) {
121123
uint256 priceX36 = amountOut * PRECISION / amountIn;
@@ -137,13 +139,16 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
137139
amountOut =
138140
_getFullDebt(params.zeroForOne ? params.poolKey.currency1 : params.poolKey.currency0).toUint128();
139141
}
140-
uint128 amountIn = (uint256(
141-
-int256(_swap(params.poolKey, params.zeroForOne, int256(uint256(amountOut)), params.hookData))
142-
))
143-
.toUint128();
142+
BalanceDelta delta = _swap(params.poolKey, params.zeroForOne, int256(uint256(amountOut)), params.hookData);
143+
// exact output is all-or-nothing: a pool can deliver less than requested if it runs out of
144+
// liquidity before the price limit. Reverting on a shortfall keeps "exact output" exact;
145+
// over-delivery (possible only via hook pools) is allowed.
146+
uint128 amountOutActual = _swapOutput(delta, params.zeroForOne);
147+
if (amountOutActual < amountOut) revert V4ExactOutputUnfilled(amountOut, amountOutActual);
148+
uint128 amountIn = _swapInput(delta, params.zeroForOne);
144149
if (amountIn > params.amountInMaximum) revert V4TooMuchRequested(params.amountInMaximum, amountIn);
145150
if (params.minHopPriceX36 != 0) {
146-
uint256 priceX36 = uint256(amountOut) * PRECISION / amountIn;
151+
uint256 priceX36 = uint256(amountOutActual) * PRECISION / amountIn;
147152
if (priceX36 < params.minHopPriceX36) {
148153
revert V4TooMuchRequestedPerHopSingle(params.minHopPriceX36, priceX36);
149154
}
@@ -169,12 +174,19 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
169174
for (uint256 i = pathLength; i > 0; i--) {
170175
pathKey = params.path[i - 1];
171176
(PoolKey memory poolKey, bool oneForZero) = pathKey.getPoolAndSwapDirection(currencyOut);
172-
// The output delta will always be negative, except for when interacting with certain hook pools
173-
amountIn = (uint256(-int256(_swap(poolKey, !oneForZero, int256(uint256(amountOut)), pathKey.hookData))))
174-
.toUint128();
177+
// The output delta will always be positive, except for when interacting with certain hook pools
178+
BalanceDelta delta = _swap(poolKey, !oneForZero, int256(uint256(amountOut)), pathKey.hookData);
179+
uint128 amountOutActual = _swapOutput(delta, !oneForZero);
180+
// Every hop must fill. PoolManager nets one delta per currency across the whole unlock,
181+
// so an intermediate shortfall can be absorbed by same-currency credit and settle
182+
// silently rather than reverting.
183+
if (amountOutActual < amountOut) {
184+
revert V4ExactOutputUnfilled(amountOut, amountOutActual);
185+
}
186+
amountIn = _swapInput(delta, !oneForZero);
175187

176188
if (perHopPriceLength != 0) {
177-
uint256 priceX36 = amountOut * PRECISION / amountIn;
189+
uint256 priceX36 = uint256(amountOutActual) * PRECISION / amountIn;
178190
uint256 minPrice = params.minHopPriceX36[i - 1];
179191
if (priceX36 < minPrice) revert V4TooMuchRequestedPerHop(i - 1, minPrice, priceX36);
180192
}
@@ -187,19 +199,28 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
187199

188200
function _swap(PoolKey memory poolKey, bool zeroForOne, int256 amountSpecified, bytes calldata hookData)
189201
private
190-
returns (int128 reciprocalAmount)
202+
returns (BalanceDelta delta)
191203
{
192204
// for protection of exactOut swaps, sqrtPriceLimit is not exposed as a feature in this contract
193-
unchecked {
194-
BalanceDelta delta = poolManager.swap(
195-
poolKey,
196-
SwapParams(
197-
zeroForOne, amountSpecified, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
198-
),
199-
hookData
200-
);
201-
202-
reciprocalAmount = (zeroForOne == amountSpecified < 0) ? delta.amount1() : delta.amount0();
203-
}
205+
delta = poolManager.swap(
206+
poolKey,
207+
SwapParams(
208+
zeroForOne, amountSpecified, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
209+
),
210+
hookData
211+
);
212+
}
213+
214+
/// @notice The positive input amount a swap consumed, derived from its balance delta.
215+
/// @dev The spent currency's delta is negative (owed to the pool), so negate it to a positive amount.
216+
function _swapInput(BalanceDelta delta, bool zeroForOne) private pure returns (uint128) {
217+
return (uint256(-int256(zeroForOne ? delta.amount0() : delta.amount1()))).toUint128();
218+
}
219+
220+
/// @notice The positive output amount a swap produced, derived from its balance delta. For an
221+
/// exact-output swap this is the REALIZED output, which can be less than the requested
222+
/// amount when the pool lacks the liquidity to fill it before the price limit.
223+
function _swapOutput(BalanceDelta delta, bool zeroForOne) private pure returns (uint128) {
224+
return (zeroForOne ? delta.amount1() : delta.amount0()).toUint128();
204225
}
205226
}

src/interfaces/IV4Router.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ interface IV4Router is IImmutableState {
2323
error V4TooMuchRequestedPerHopSingle(uint256 minPrice, uint256 price);
2424
/// @notice Emitted when the length of the per-hop minimum price array is not zero and not equal to the path length
2525
error InvalidHopPriceLength();
26+
/// @notice Emitted when an exactOutput swap (or hop) delivers less than the requested amount, e.g. a
27+
/// pool runs out of liquidity before the price limit. Exact output is all-or-nothing.
28+
error V4ExactOutputUnfilled(uint256 amountOutRequested, uint256 amountOutReceived);
2629

2730
/// @notice Parameters for a single-hop exact-input swap
2831
struct ExactInputSingleParams {

test/hooks/permissionedPools/PermissionedV4Router.t.sol

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,11 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers {
13251325
//////////////////////////////////////////////////////////////*/
13261326

13271327
function test_swapExactOutputSingle_revertsForAmountIn() public {
1328-
uint256 amountOut = 1 ether;
1329-
uint256 expectedAmountIn = 369070324193623892281288;
1328+
// key0 (permissioned adapters) can deliver at most 19992 of output before the price hits the
1329+
// global limit; request a fillable amount so this exercises the amountInMaximum guard rather
1330+
// than the all-or-nothing underfill guard (V4ExactOutputUnfilled)
1331+
uint256 amountOut = 19992;
1332+
uint256 expectedAmountIn = 434604409;
13301333

13311334
IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams(
13321335
key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), 0, bytes("")
@@ -1336,9 +1339,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers {
13361339
bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER);
13371340

13381341
vm.expectRevert(
1339-
abi.encodeWithSelector(
1340-
IV4Router.V4TooMuchRequested.selector, expectedAmountIn - 1, 369070324193623892281288
1341-
)
1342+
abi.encodeWithSelector(IV4Router.V4TooMuchRequested.selector, expectedAmountIn - 1, expectedAmountIn)
13421343
);
13431344
permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max);
13441345
}
@@ -1406,8 +1407,11 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers {
14061407
}
14071408

14081409
function test_swapExactOut_revertsForAmountIn() public {
1409-
uint256 amountOut = 1 ether;
1410-
uint256 expectedAmountIn = 369070324262815812743748;
1410+
// key0 (permissioned adapters) can deliver at most 19992 of output before the price hits the
1411+
// global limit; request a fillable amount so this exercises the amountInMaximum guard rather
1412+
// than the all-or-nothing underfill guard (V4ExactOutputUnfilled)
1413+
uint256 amountOut = 19992;
1414+
uint256 expectedAmountIn = 433302557;
14111415

14121416
tokenPath.push(permissionsAdapter0Currency);
14131417
tokenPath.push(permissionsAdapter1Currency);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.26;
3+
4+
import {RoutingTestHelpers} from "../shared/RoutingTestHelpers.sol";
5+
import {Planner} from "../shared/Planner.sol";
6+
import {Actions} from "../../src/libraries/Actions.sol";
7+
import {ActionConstants} from "../../src/libraries/ActionConstants.sol";
8+
import {IV4Router} from "../../src/interfaces/IV4Router.sol";
9+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
10+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
11+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
12+
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
13+
import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
14+
15+
/// @notice Exact output is all-or-nothing. A v4 swap can partial-fill when a pool runs out of
16+
/// liquidity before the price limit; the router must revert `V4ExactOutputUnfilled` rather
17+
/// than silently deliver less than the requested `amountOut`. These drive real exact-output
18+
/// swaps through the router against a deliberately thin pool.
19+
contract V4RouterExactOutputUnfilledTest is RoutingTestHelpers {
20+
PoolKey internal thinKey;
21+
22+
function setUp() public {
23+
setupRouterCurrenciesAndPoolsWithLiquidity();
24+
plan = Planner.init();
25+
// liquidity sits in a single tick-spacing band, so a 1e18 exact-output request cannot fully
26+
// fill before the price leaves the band
27+
thinKey = _createThinPool(currency0, currency1, 500, 0, 60);
28+
}
29+
30+
function test_exactOutputSingle_revertsOnUnderfill() public {
31+
// buy currency0 by selling currency1 (oneForZero); the thin band cannot deliver 1e18 out
32+
IV4Router.ExactOutputSingleParams memory params =
33+
IV4Router.ExactOutputSingleParams(thinKey, false, 1 ether, type(uint128).max, 0, bytes(""));
34+
plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params));
35+
bytes memory data = plan.finalizeSwap(currency1, currency0, ActionConstants.MSG_SENDER);
36+
37+
vm.expectPartialRevert(IV4Router.V4ExactOutputUnfilled.selector);
38+
router.executeActions(data);
39+
}
40+
41+
function test_exactOutput_multiHop_revertsOnUnderfill() public {
42+
// a 2-hop path currency2 -> currency1 -> currency0 whose final hop (currency1 -> currency0)
43+
// is the thin pool; the final hop cannot deliver 1e18 of currency0
44+
tokenPath.push(currency2);
45+
tokenPath.push(currency1);
46+
tokenPath.push(currency0);
47+
IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, 1 ether);
48+
// route the final hop through the thin pool by overriding its fee/tickSpacing in the path
49+
params.path[1].fee = 500;
50+
params.path[1].tickSpacing = 60;
51+
52+
plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params));
53+
bytes memory data = plan.finalizeSwap(currency2, currency0, ActionConstants.MSG_SENDER);
54+
55+
vm.expectPartialRevert(IV4Router.V4ExactOutputUnfilled.selector);
56+
router.executeActions(data);
57+
}
58+
59+
/// @notice An intermediate underfill cannot be left for settlement to catch when the path repeats
60+
/// a currency. Here the middle currency1 -> currency0 hop underfills, and because currency0
61+
/// is also the route's input currency the shortfall merges into the input debt that
62+
/// SETTLE_ALL pays -- so no non-zero delta survives to fail settlement, and the
63+
/// post-loop amountInMaximum check only ever sees the last hop's own input.
64+
function test_exactOutput_revertsOnIntermediateUnderfill_repeatedCurrency() public {
65+
// deep pool for the final delivered hop, so only the middle hop is thin
66+
createPoolWithLiquidity(currency0, currency2, address(0));
67+
68+
// economic route currency0 -> currency1 -> currency0 -> currency2
69+
tokenPath.push(currency0);
70+
tokenPath.push(currency1);
71+
tokenPath.push(currency0);
72+
tokenPath.push(currency2);
73+
IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, 1 ether);
74+
// route the middle currency1 -> currency0 hop through the thin pool
75+
params.path[1].fee = 500;
76+
params.amountInMaximum = 0.8 ether;
77+
78+
plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params));
79+
bytes memory data = plan.finalizeSwap(currency0, currency2, ActionConstants.MSG_SENDER);
80+
81+
vm.expectPartialRevert(IV4Router.V4ExactOutputUnfilled.selector);
82+
router.executeActions(data);
83+
}
84+
85+
function test_exactOutputSingle_fullFillSucceeds() public {
86+
// the deep pool (key0) fully fills, unchanged by the new guard
87+
uint256 amountOut = 1 ether;
88+
IV4Router.ExactOutputSingleParams memory params =
89+
IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), type(uint128).max, 0, bytes(""));
90+
plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params));
91+
92+
(, uint256 outputBalanceBefore,, uint256 outputBalanceAfter) =
93+
_finalizeAndExecuteSwap(currency0, currency1, 2 ether);
94+
95+
assertEq(outputBalanceAfter - outputBalanceBefore, amountOut, "exact output delivered in full");
96+
assertEq(currency0.balanceOf(address(router)), 0, "router holds no input residual");
97+
assertEq(currency1.balanceOf(address(router)), 0, "router holds no output residual");
98+
}
99+
100+
function _createThinPool(Currency a, Currency b, uint24 fee, int24 lower, int24 upper)
101+
internal
102+
returns (PoolKey memory key)
103+
{
104+
if (Currency.unwrap(a) > Currency.unwrap(b)) (a, b) = (b, a);
105+
key = PoolKey(a, b, fee, 60, IHooks(address(0)));
106+
manager.initialize(key, SQRT_PRICE_1_1);
107+
MockERC20(Currency.unwrap(a)).approve(address(positionManager), type(uint256).max);
108+
MockERC20(Currency.unwrap(b)).approve(address(positionManager), type(uint256).max);
109+
positionManager.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, 200 ether, 0), "0x");
110+
}
111+
}

0 commit comments

Comments
 (0)