Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(universal-router-sdk): handles all ETH/WETH transitions #157

Merged
merged 27 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d6cdba7
universal-router-sdk handles ETH output fee
ewilz Oct 8, 2024
f2b0019
tests to wrap ETH output to WETH for recipient, fails need v4-sdk bug…
ewilz Oct 10, 2024
1de6365
fix encodeToPath
ewilz Oct 10, 2024
ae9d28d
add test cases (so I can see clean diff) and small var rename
ewilz Oct 10, 2024
f042591
Revert "fix encodeToPath"
ewilz Oct 10, 2024
043c608
tons of tests failing, have to find what changed
ewilz Oct 10, 2024
eb8c695
found bug all previous tests now back to passing, moving onto my new …
ewilz Oct 10, 2024
3ab3903
install new v4-sdk
ewilz Oct 10, 2024
dec6963
import new router-sdk
ewilz Oct 10, 2024
40804e0
Merge remote-tracking branch 'origin/main' into ur-weth-eth
ewilz Oct 10, 2024
8064579
all tests passing, still more tests to put in solidity
ewilz Oct 14, 2024
5f78eb8
lint
ewilz Oct 14, 2024
e54cc77
Merge remote-tracking branch 'origin/main' into ur-weth-eth
ewilz Oct 14, 2024
f7fc070
lint interop
ewilz Oct 14, 2024
9e97a40
support unwrapping WETH to trade ETH on v4
ewilz Oct 15, 2024
5529762
support wrapping ETH to trade on V4 (exact output)
ewilz Oct 15, 2024
0553c96
lint
ewilz Oct 15, 2024
1b49ea1
force types to appease build (better way to do this??)
ewilz Oct 15, 2024
f445984
PR comments
ewilz Oct 17, 2024
38ebceb
PR comments
ewilz Nov 5, 2024
73457b3
woops forgot one PR comment
ewilz Nov 5, 2024
e941e22
Merge remote-tracking branch 'origin/main' into ur-weth-eth
ewilz Nov 5, 2024
61c8505
revert submodule change
ewilz Nov 5, 2024
b89f494
Merge branch 'main' into ur-weth-eth
ewilz Nov 14, 2024
b38b124
refund ETH on exactOutput native trade
ewilz Nov 14, 2024
276aa21
typo
ewilz Nov 14, 2024
bf5cb50
lint
ewilz Nov 18, 2024
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
4 changes: 2 additions & 2 deletions sdks/universal-router-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@
"dependencies": {
"@openzeppelin/contracts": "4.7.0",
"@uniswap/permit2-sdk": "^1.3.0",
"@uniswap/router-sdk": "^1.14.2",
"@uniswap/router-sdk": "^1.14.3",
"@uniswap/sdk-core": "^5.8.2",
"@uniswap/universal-router": "2.0.0-beta.1",
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v2-sdk": "^4.6.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-sdk": "^3.18.1",
"@uniswap/v4-sdk": "^1.10.0",
"@uniswap/v4-sdk": "^1.10.3",
"bignumber.js": "^9.0.2",
"ethers": "^5.7.0"
},
Expand Down
119 changes: 79 additions & 40 deletions sdks/universal-router-sdk/src/entities/actions/uniswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
IRoute,
RouteV2,
RouteV3,
RouteV4,
MixedRouteSDK,
MixedRoute,
SwapOptions as RouterSwapOptions,
Expand All @@ -26,9 +25,10 @@ import {
} from '@uniswap/router-sdk'
import { Permit2Permit } from '../../utils/inputTokens'
import { getPathCurrency } from '../../utils/pathCurrency'
import { Currency, TradeType, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { Currency, TradeType, Token, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { Command, RouterActionType, TradeConfig } from '../Command'
import { SENDER_AS_RECIPIENT, ROUTER_AS_RECIPIENT, CONTRACT_BALANCE, ETH_ADDRESS } from '../../utils/constants'
import { getCurrencyAddress } from '../../utils/getCurrencyAddress'
import { encodeFeeBips } from '../../utils/numbers'
import { BigNumber, BigNumberish } from 'ethers'
import { TPool } from '@uniswap/router-sdk/dist/utils/TPool'
Expand Down Expand Up @@ -65,8 +65,7 @@ export class UniswapTrade implements Command {

constructor(public trade: RouterTrade<Currency, Currency, TradeType>, public options: SwapOptions) {
if (!!options.fee && !!options.flatFee) throw new Error('Only one fee option permitted')
if (this.inputRequiresWrap) this.payerIsUser = false
else if (this.options.useRouterBalance) this.payerIsUser = false
if (this.inputRequiresWrap || this.inputRequiresUnwrap || this.options.useRouterBalance) this.payerIsUser = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could just be done using a this.payerIsUser = () ? false : true might be clearer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linting on this is ugly IMO but could be done...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok fair

else this.payerIsUser = true
}

Expand All @@ -79,24 +78,51 @@ export class UniswapTrade implements Command {
}

get inputRequiresWrap(): boolean {
if (!this.isAllV4) {
return this.trade.inputAmount.currency.isNative
if (this.isAllV4) {
return (
this.trade.inputAmount.currency.isNative &&
!(this.trade.swaps[0].route as unknown as V4Route<Currency, Currency>).pathInput.isNative
)
} else {
// We only support wrapping all ETH or no ETH currently. We cannot support splitting where half needs to be wrapped
// If the input currency is ETH and the input of the first path is not ETH it must be WETH that needs wrapping
return this.trade.inputAmount.currency.isNative && !this.trade.swaps[0].route.input.isNative
return this.trade.inputAmount.currency.isNative
jsy1218 marked this conversation as resolved.
Show resolved Hide resolved
}
}

get inputRequiresUnwrap(): boolean {
if (this.isAllV4) {
return (
!this.trade.inputAmount.currency.isNative &&
(this.trade.swaps[0].route as unknown as V4Route<Currency, Currency>).pathInput.isNative
)
}
return false
}

get outputRequiresWrap(): boolean {
if (this.isAllV4) {
return (
!this.trade.outputAmount.currency.isNative &&
(this.trade.swaps[0].route as unknown as V4Route<Currency, Currency>).pathOutput.isNative
)
}
return false
}

get outputRequiresUnwrap(): boolean {
if (!this.isAllV4) {
return this.trade.outputAmount.currency.isNative
if (this.isAllV4) {
return (
this.trade.outputAmount.currency.isNative &&
!(this.trade.swaps[0].route as unknown as V4Route<Currency, Currency>).pathOutput.isNative
)
} else {
// If the output currency is ETH and the output of the swap is not ETH it must be WETH that needs unwrapping
return this.trade.outputAmount.currency.isNative && !this.trade.swaps[0].route.output.isNative
return this.trade.outputAmount.currency.isNative
}
}

get outputRequiresTransition(): boolean {
return this.outputRequiresWrap || this.outputRequiresUnwrap
}

encode(planner: RoutePlanner, _config: TradeConfig): void {
// If the input currency is the native currency, we need to wrap it with the router as the recipient
if (this.inputRequiresWrap) {
Expand All @@ -105,6 +131,14 @@ export class UniswapTrade implements Command {
ROUTER_AS_RECIPIENT,
this.trade.maximumAmountIn(this.options.slippageTolerance).quotient.toString(),
])
} else if (this.inputRequiresUnwrap) {
// send wrapped token to router to unwrap
planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [
(this.trade.inputAmount.currency as Token).address,
ROUTER_AS_RECIPIENT,
this.trade.maximumAmountIn(this.options.slippageTolerance).quotient.toString(),
ewilz marked this conversation as resolved.
Show resolved Hide resolved
])
planner.addCommand(CommandType.UNWRAP_WETH, [ROUTER_AS_RECIPIENT, 0])
}
// The overall recipient at the end of the trade, SENDER_AS_RECIPIENT uses the msg.sender
this.options.recipient = this.options.recipient ?? SENDER_AS_RECIPIENT
Expand All @@ -115,7 +149,8 @@ export class UniswapTrade implements Command {
// in that the reversion probability is lower
const performAggregatedSlippageCheck =
this.trade.tradeType === TradeType.EXACT_INPUT && this.trade.routes.length > 2
const routerMustCustody = performAggregatedSlippageCheck || this.outputRequiresUnwrap || hasFeeOption(this.options)
const routerMustCustody =
performAggregatedSlippageCheck || this.outputRequiresTransition || hasFeeOption(this.options)

for (const swap of this.trade.swaps) {
switch (swap.route.protocol) {
Expand All @@ -139,18 +174,17 @@ export class UniswapTrade implements Command {
let minimumAmountOut: BigNumber = BigNumber.from(
this.trade.minimumAmountOut(this.options.slippageTolerance).quotient.toString()
)

// The router custodies for 3 reasons: to unwrap, to take a fee, and/or to do a slippage check
if (routerMustCustody) {
const pools = this.trade.swaps[0].route.pools
const pathOutputCurrency = getPathCurrency(this.trade.outputAmount.currency, pools[pools.length - 1])
const pathOutputCurrencyAddress = pathOutputCurrency.isNative ? ETH_ADDRESS : pathOutputCurrency.address
jsy1218 marked this conversation as resolved.
Show resolved Hide resolved

// If there is a fee, that percentage is sent to the fee recipient
// In the case where ETH is the output currency, the fee is taken in WETH (for gas reasons)
if (!!this.options.fee) {
const feeBips = encodeFeeBips(this.options.fee.fee)
planner.addCommand(CommandType.PAY_PORTION, [
this.trade.outputAmount.currency.wrapped.address,
this.options.fee.recipient,
feeBips,
])
planner.addCommand(CommandType.PAY_PORTION, [pathOutputCurrencyAddress, this.options.fee.recipient, feeBips])
jsy1218 marked this conversation as resolved.
Show resolved Hide resolved

// If the trade is exact output, and a fee was taken, we must adjust the amount out to be the amount after the fee
// Otherwise we continue as expected with the trade's normal expected output
Expand All @@ -165,11 +199,7 @@ export class UniswapTrade implements Command {
const feeAmount = this.options.flatFee.amount
if (minimumAmountOut.lt(feeAmount)) throw new Error('Flat fee amount greater than minimumAmountOut')

planner.addCommand(CommandType.TRANSFER, [
this.trade.outputAmount.currency.wrapped.address,
this.options.flatFee.recipient,
feeAmount,
])
planner.addCommand(CommandType.TRANSFER, [pathOutputCurrencyAddress, this.options.flatFee.recipient, feeAmount])

// If the trade is exact output, and a fee was taken, we must adjust the amount out to be the amount after the fee
// Otherwise we continue as expected with the trade's normal expected output
Expand All @@ -182,19 +212,25 @@ export class UniswapTrade implements Command {
// by this if-else clause.
if (this.outputRequiresUnwrap) {
planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, minimumAmountOut])
} else if (this.outputRequiresWrap) {
planner.addCommand(CommandType.WRAP_ETH, [this.options.recipient, CONTRACT_BALANCE])
} else {
planner.addCommand(CommandType.SWEEP, [
this.trade.outputAmount.currency.wrapped.address,
getCurrencyAddress(this.trade.outputAmount.currency),
this.options.recipient,
minimumAmountOut,
])
}
}

if (this.inputRequiresWrap && (this.trade.tradeType === TradeType.EXACT_OUTPUT || riskOfPartialFill(this.trade))) {
// for exactOutput swaps that take native currency as input
// we need to send back the change to the user
planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, 0])
// for exactOutput swaps that take perform an inputToken transition (wrap or unwrap)
// we need to send back the change to the user
if (this.trade.tradeType === TradeType.EXACT_OUTPUT || riskOfPartialFill(this.trade)) {
if (this.inputRequiresWrap) {
planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, 0])
} else if (this.inputRequiresUnwrap) {
planner.addCommand(CommandType.WRAP_ETH, [this.options.recipient, CONTRACT_BALANCE])
}
}

if (this.options.safeMode) planner.addCommand(CommandType.SWEEP, [ETH_ADDRESS, this.options.recipient, 0])
Expand Down Expand Up @@ -275,31 +311,34 @@ function addV3Swap<TInput extends Currency, TOutput extends Currency>(

function addV4Swap<TInput extends Currency, TOutput extends Currency>(
planner: RoutePlanner,
{ route, inputAmount, outputAmount }: Swap<TInput, TOutput>,
{ inputAmount, outputAmount, route }: Swap<TInput, TOutput>,
tradeType: TradeType,
options: SwapOptions,
payerIsUser: boolean,
routerMustCustody: boolean
): void {
// create a deep copy of pools since v4Planner encoding tampers with array
const pools = route.pools.map((p) => p) as V4Pool[]
const v4Route = new V4Route(pools, inputAmount.currency, outputAmount.currency)
const trade = V4Trade.createUncheckedTrade({
route: route as RouteV4<TInput, TOutput>,
route: v4Route,
inputAmount,
outputAmount,
tradeType,
})

payerIsUser = payerIsUser && v4Route.input == v4Route.pathInput

const slippageToleranceOnSwap =
routerMustCustody && tradeType == TradeType.EXACT_INPUT ? undefined : options.slippageTolerance

const inputWethFromRouter = inputAmount.currency.isNative && !route.input.isNative
if (inputWethFromRouter && !payerIsUser) throw new Error('Inconsistent payer')

const v4Planner = new V4Planner()
v4Planner.addTrade(trade, slippageToleranceOnSwap)
v4Planner.addSettle(inputWethFromRouter ? inputAmount.currency.wrapped : inputAmount.currency, payerIsUser)

options.recipient = options.recipient ?? SENDER_AS_RECIPIENT
v4Planner.addTake(outputAmount.currency, routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient)

v4Planner.addSettle(trade.route.pathInput, payerIsUser)
v4Planner.addTake(
trade.route.pathOutput,
routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient ?? SENDER_AS_RECIPIENT
)
planner.addCommand(CommandType.V4_SWAP, [v4Planner.finalize()])
}

Expand Down
6 changes: 6 additions & 0 deletions sdks/universal-router-sdk/src/utils/getCurrencyAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Currency } from '@uniswap/sdk-core'
import { ETH_ADDRESS } from './constants'

export function getCurrencyAddress(currency: Currency): string {
return currency.isNative ? ETH_ADDRESS : currency.wrapped.address
}
12 changes: 4 additions & 8 deletions sdks/universal-router-sdk/src/utils/pathCurrency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,10 @@ export function getPathCurrency(currency: Currency, pool: TPool): Currency {
return currency.wrapped

// return native currency if pool involves native version of wrapped currency (only applies to V4)
} else if (pool instanceof V4Pool) {
if (pool.token0.wrapped.equals(currency)) {
return pool.token0
} else if (pool.token1.wrapped.equals(currency)) {
return pool.token1
}

// otherwise the token is invalid
} else if (pool instanceof V4Pool && pool.token0.wrapped.equals(currency)) {
return pool.token0
} else if (pool instanceof V4Pool && pool.token1.wrapped.equals(currency)) {
return pool.token1
} else {
throw new Error(`Expected currency ${currency.symbol} to be either ${pool.token0.symbol} or ${pool.token1.symbol}`)
}
Expand Down
Loading
Loading