Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 76 additions & 3 deletions sdks/universal-router-sdk/src/entities/actions/uniswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface Swap<TInput extends Currency, TOutput extends Currency> {
export class UniswapTrade implements Command {
readonly tradeType: RouterActionType = RouterActionType.UniswapTrade
readonly payerIsUser: boolean
private _hasSplitRoutesWithV4AndNonV4: boolean | undefined

constructor(public trade: RouterTrade<Currency, Currency, TradeType>, public options: SwapOptions) {
if (!!options.fee && !!options.flatFee) throw new Error('Only one fee option permitted')
Expand All @@ -82,6 +83,39 @@ export class UniswapTrade implements Command {
return result
}

// Check if the trade contains split routes with both V4 and non-V4 (V2 or V3) swaps
// For mixed routes, only counts as non-V4 if the last pool is not V4
// This is cached after first calculation to avoid re-iterating
get hasSplitRoutesWithV4AndNonV4(): boolean {
if (this._hasSplitRoutesWithV4AndNonV4 !== undefined) {
return this._hasSplitRoutesWithV4AndNonV4
}

let hasV4 = false
let hasNonV4 = false

for (const swap of this.trade.swaps) {
if (swap.route.protocol === Protocol.V4) {
hasV4 = true
} else if (swap.route.protocol === Protocol.V2 || swap.route.protocol === Protocol.V3) {
hasNonV4 = true
} else if (swap.route.protocol === Protocol.MIXED) {
// For mixed routes, check if the last pool is not V4
// If the last pool is V4, it can handle ETH natively, so we don't count it as non-V4
const mixedRoute = swap.route as MixedRoute<Currency, Currency>
const lastPool = mixedRoute.pools[mixedRoute.pools.length - 1]
if (!(lastPool instanceof V4Pool)) {
hasNonV4 = true
}
}
// Early exit if we've found both
if (hasV4 && hasNonV4) break
}

this._hasSplitRoutesWithV4AndNonV4 = hasV4 && hasNonV4
return this._hasSplitRoutesWithV4AndNonV4
}

// this.trade.swaps is an array of swaps / trades.
// we are iterating over one swap (trade) at a time so length is 1
// route is either v2, v3, v4, or mixed
Expand Down Expand Up @@ -140,6 +174,21 @@ export class UniswapTrade implements Command {
}

get outputRequiresUnwrap(): boolean {
// For split routes with both V4 and non-V4, if any V4 route outputs ETH,
// we force V4 to take WETH, so we need to unwrap at the end

// need to check that mixed last pool is not v4
if (this.hasSplitRoutesWithV4AndNonV4) {
// Check if any swap is V4 and outputs native currency
for (const swap of this.trade.swaps) {
if (swap.route.protocol === Protocol.V4) {
const v4Route = swap.route as unknown as V4Route<Currency, Currency>
if (v4Route.pathOutput.isNative) {
return true
}
}
}
}
const swap = this.trade.swaps[0]
const lastRoute = swap.route
const lastPool = lastRoute.pools[lastRoute.pools.length - 1]
Expand Down Expand Up @@ -198,7 +247,15 @@ export class UniswapTrade implements Command {
addV3Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody)
break
case Protocol.V4:
addV4Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody)
addV4Swap(
planner,
swap,
this.trade.tradeType,
this.options,
this.payerIsUser,
routerMustCustody,
this.hasSplitRoutesWithV4AndNonV4
)
break
case Protocol.MIXED:
addMixedSwap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody)
Expand Down Expand Up @@ -356,7 +413,8 @@ function addV4Swap<TInput extends Currency, TOutput extends Currency>(
tradeType: TradeType,
options: SwapOptions,
payerIsUser: boolean,
routerMustCustody: boolean
routerMustCustody: boolean,
hasSplitRoutesWithV4AndNonV4: boolean = false
): void {
// create a deep copy of pools since v4Planner encoding tampers with array
const pools = route.pools.map((p) => p) as V4Pool[]
Expand All @@ -375,8 +433,23 @@ function addV4Swap<TInput extends Currency, TOutput extends Currency>(
v4Planner.addTrade(trade, slippageToleranceOnSwap, options.maxHopSlippage)

v4Planner.addSettle(trade.route.pathInput, payerIsUser)

// If this trade is part of split routes with both V4 and non-V4 trades,
// and the pathOutput is ETH, we need to use WETH instead for V4 to take
// need to also check that last pool is eth-weth pool
let pathOutputForTake = trade.route.pathOutput
if (hasSplitRoutesWithV4AndNonV4 && pathOutputForTake.isNative) {
const lastPool = trade.route.pools[trade.route.pools.length - 1]
const isEthWethPool =
(lastPool.currency0.isNative && lastPool.currency1.wrapped.equals(lastPool.currency0.wrapped)) ||
(lastPool.currency1.isNative && lastPool.currency0.wrapped.equals(lastPool.currency1.wrapped))
if (isEthWethPool) {
pathOutputForTake = pathOutputForTake.wrapped
}
}

v4Planner.addTake(
trade.route.pathOutput,
pathOutputForTake,
routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient ?? SENDER_AS_RECIPIENT
)
planner.addCommand(CommandType.V4_SWAP, [v4Planner.finalize()])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,75 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter {
assertGt(DAI.balanceOf(RECIPIENT), 9 * ONE_DAI / 10);
}

function testV4ExactInMultiHop2Routes1() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_SPLIT_TWO_ROUTES_V4");

deal(address(USDC), from, BALANCE);
USDC.approve(address(permit2), BALANCE);
permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000));
assertEq(USDC.balanceOf(from), BALANCE);
uint256 startingRecipientBalance = RECIPIENT.balance;
uint256 wethStartingBalance = WETH.balanceOf(RECIPIENT);

(bool success,) = address(router).call{value: params.value}(params.data);
require(success, "call failed");
assertEq(WETH.balanceOf(RECIPIENT), wethStartingBalance);
assertGt(RECIPIENT.balance, startingRecipientBalance);
assertEq(address(router).balance, 0);
assertEq(USDC.balanceOf(from), BALANCE - 2000 * ONE_USDC);
}

function testV4ExactInMultiHop2RoutesMixed1() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_SPLIT_TWO_ROUTES_V4_MIXED");

uint256 daiAmount = 2000 ether;
deal(address(DAI), from, daiAmount);
DAI.approve(address(permit2), daiAmount);
permit2.approve(address(DAI), address(router), uint160(daiAmount), uint48(block.timestamp + 1000));
assertEq(DAI.balanceOf(from), daiAmount);
uint256 startingRecipientBalance = RECIPIENT.balance;

(bool success,) = address(router).call{value: params.value}(params.data);
require(success, "call failed");
assertEq(WETH.balanceOf(RECIPIENT), 0);
assertGt(RECIPIENT.balance, startingRecipientBalance);
assertEq(address(router).balance, 0);
}

function testV4ExactInMultiHop2RoutesMixedEth() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_SPLIT_TWO_ROUTES_V4_MIXED_ENDING_WITH_ETH");

uint256 daiAmount = 2000 ether;
deal(address(DAI), from, daiAmount);
DAI.approve(address(permit2), daiAmount);
permit2.approve(address(DAI), address(router), uint160(daiAmount), uint48(block.timestamp + 1000));
assertEq(DAI.balanceOf(from), daiAmount);
uint256 startingRecipientBalance = RECIPIENT.balance;

(bool success,) = address(router).call{value: params.value}(params.data);
require(success, "call failed");
assertEq(WETH.balanceOf(RECIPIENT), 0);
assertGt(RECIPIENT.balance, startingRecipientBalance);
assertEq(address(router).balance, 0);
}

function testV4ExactInMultiHop2RoutesMixedWETH() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_SPLIT_TWO_ROUTES_V4_MIXED_ENDING_WITH_WETH");

uint256 daiAmount = 2000 ether;
deal(address(DAI), from, daiAmount);
DAI.approve(address(permit2), daiAmount);
permit2.approve(address(DAI), address(router), uint160(daiAmount), uint48(block.timestamp + 1000));
assertEq(DAI.balanceOf(from), daiAmount);
uint256 startingRecipientBalance = RECIPIENT.balance;

(bool success,) = address(router).call{value: params.value}(params.data);
require(success, "call failed");
assertEq(WETH.balanceOf(RECIPIENT), 0);
assertGt(RECIPIENT.balance, startingRecipientBalance);
assertEq(address(router).balance, 0);
}

function testMixedExactInputNative() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_MIXED_1_ETH_FOR_DAI");

Expand Down
Loading
Loading