Skip to content
This repository has been archived by the owner on Apr 25, 2024. It is now read-only.

Support STETH trades that auto wrap to WSTETH #148

Merged
merged 18 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,63 @@ const unwrapWETH = new UnwrapWETH(amountWETH, chainId, optionalPermit2Params)
const { calldata, value } = SwapRouter.swapCallParameters([unwrapWETH, seaportTrades, looksRareTrades])
```

### Trading stETH
To trade stETH as an input token, you can make sure the router automatically wraps stETH to wstETH before trading across a wstETH route.

If this is an exactOut trade, we'll need to wrap the maximumAmountIn of steth, and therefore should add an unwrap command at the end of the transaction to account for any leftover steth that didn't get traded. `amountMinimum` can be set to 0 in this scenario for the unwrapSteth commmand.
```typescript
import { TradeType } from '@uniswap/sdk-core'
import { Trade as V2TradeSDK } from '@uniswap/v2-sdk'
import { Trade as V3TradeSDK } from '@uniswap/v3-sdk'
import { MixedRouteTrade, MixedRouteSDK, Trade as RouterTrade } from '@uniswap/router-sdk'
import {
UniswapTrade,
WrapSTETH
} from "@uniswap/universal-router-sdk";

// EXACT INPUT
// including optional permit2 parameter will transfer STETH amount using permit2
const wrapSTETH = new WrapSTETH(inputSTETH, 1, WrapSTETHPermitData?, wrapAmountOtherThanContractBalance?)
const uniswapWstethTrade = new UniswapTrade(
new RouterTrade({ v2Routes, v3Routes, mixedRoutes, tradeType: TradeType.EXACT_INPUT }),
{ slippageTolerance}
)
const { calldata, value } = SwapRouter.swapCallParameters([wrapSTETH, uniswapWstethTrade])

// EXACT OUTPUT
const wrapSTETH = new WrapSTETH(maximumInputSTETH, 1, WrapSTETHPermitData?, wrapAmountOtherThanContractBalance?)
const uniswapWstethTrade = new UniswapTrade(
new RouterTrade({ v2Routes, v3Routes, mixedRoutes, tradeType: TradeType.EXACT_OUTPUT }),
{ slippageTolerance}
)
const unwrapSTETH = new UnwrapSTETH(recipient, amountMinimum = 0, chainId)

const { calldata, value } = SwapRouter.swapCallParameters([wrapSTETH, uniswapWstethTrade, unwrapSTETH])

```

To recieve stETH as an output token, you can make sure the router automatically unwraps wstETH to stETH before returning to the swapper
```typescript
import { TradeType } from '@uniswap/sdk-core'
import { Trade as V2TradeSDK } from '@uniswap/v2-sdk'
import { Trade as V3TradeSDK } from '@uniswap/v3-sdk'
import { MixedRouteTrade, MixedRouteSDK, Trade as RouterTrade } from '@uniswap/router-sdk'
import {
ROUTER_AS_RECIPIENT,
UniswapTrade,
UnwrapSTETH
} from "@uniswap/universal-router-sdk";

// return trade to the router instead of the recipient using the ROUTER_AS_RECIPIENT constant so that the router may custody tokens to unwrap
const uniswapWstethTrade = new UniswapTrade(
new RouterTrade({ v2Routes, v3Routes, mixedRoutes, tradeType: TradeType.EXACT_INPUT }),
{ slippageTolerance, ROUTER_AS_RECIPIENT}
)
const unwrapSTETH = new UnwrapSTETH(recipient, amountMinimum, chainId)

const { calldata, value } = SwapRouter.swapCallParameters([uniswapWstethTrade, unwrapSTETH])
```


## Running this package
Make sure you are running `node v16`
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
solc = "0.8.17"
fs_permissions = [{ access = "read", path = "./permit2/out/Permit2.sol/Permit2.json"}, { access = "read", path = "./test/forge/interop.json"}]
src = "./test/forge"
via_ir = true
via_ir = true
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@uniswap/permit2-sdk": "^1.2.0",
"@uniswap/router-sdk": "^1.6.0",
"@uniswap/sdk-core": "^4.0.0",
"@uniswap/universal-router": "1.4.3",
"@uniswap/universal-router": "1.5.1",
"@uniswap/v2-sdk": "^3.2.0",
"@uniswap/v3-sdk": "^3.10.0",
"bignumber.js": "^9.0.2",
Expand Down
2 changes: 2 additions & 0 deletions src/entities/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export enum RouterTradeType {
UniswapTrade = 'UniswapTrade',
NFTTrade = 'NFTTrade',
UnwrapWETH = 'UnwrapWETH',
WrapSTETH = 'WrapSTETH',
UnwrapSTETH = 'UnwrapSTETH',
}

// interface for entities that can be encoded as a Universal Router command
Expand Down
2 changes: 2 additions & 0 deletions src/entities/protocols/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './uniswap'
export * from './sudoswap'
export * from './x2y2'
export * from './unwrapWETH'
export * from './wrapSTETH'
export * from './unwrapSTETH'
3 changes: 2 additions & 1 deletion src/entities/protocols/uniswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type FlatFeeOptions = {
// so we extend swap options with the permit2 permit
export type SwapOptions = Omit<RouterSwapOptions, 'inputTokenPermit'> & {
inputTokenPermit?: Permit2Permit
payerIsRouter?: boolean
flatFee?: FlatFeeOptions
}

Expand All @@ -51,7 +52,7 @@ export class UniswapTrade implements Command {
}

encode(planner: RoutePlanner, _config: TradeConfig): void {
let payerIsUser = true
let payerIsUser = !this.options.payerIsRouter
Copy link
Contributor

Choose a reason for hiding this comment

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

oh interesting, did we not expose the option to set payerIsUser/Router before via the sdk?

Copy link
Member Author

Choose a reason for hiding this comment

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

correct. I don't think it made any sense for any of the features we had.... wasn't sure if asking for the opposite (payerIsROUTER) as an option was awkward...


// If the input currency is the native currency, we need to wrap it with the router as the recipient
if (this.trade.inputAmount.currency.isNative) {
Expand Down
21 changes: 21 additions & 0 deletions src/entities/protocols/unwrapSTETH.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import invariant from 'tiny-invariant'
import { BigNumberish } from 'ethers'
import { RoutePlanner, CommandType } from '../../utils/routerCommands'
import { Command, RouterTradeType, TradeConfig } from '../Command'
import { STETH_ADDRESS, NOT_SUPPORTED_ON_CHAIN } from '../../utils/constants'

export class UnwrapSTETH implements Command {
readonly tradeType: RouterTradeType = RouterTradeType.UnwrapSTETH
readonly recipient: string
readonly amountMinimum: BigNumberish

constructor(recipient: string, amountMinimum: BigNumberish, chainId: number) {
this.recipient = recipient
this.amountMinimum = amountMinimum
invariant(STETH_ADDRESS(chainId) != NOT_SUPPORTED_ON_CHAIN, `STETH not supported on chain ${chainId}`)
}

encode(planner: RoutePlanner, _: TradeConfig): void {
planner.addCommand(CommandType.UNWRAP_STETH, [this.recipient, this.amountMinimum])
}
}
40 changes: 40 additions & 0 deletions src/entities/protocols/wrapSTETH.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import invariant from 'tiny-invariant'
import { BigNumberish } from 'ethers'
import { RoutePlanner, CommandType } from '../../utils/routerCommands'
import { encodeInputTokenOptions, Permit2Permit } from '../../utils/inputTokens'
import { Command, RouterTradeType, TradeConfig } from '../Command'
import { CONTRACT_BALANCE, ROUTER_AS_RECIPIENT, STETH_ADDRESS } from '../../utils/constants'

export class WrapSTETH implements Command {
readonly tradeType: RouterTradeType = RouterTradeType.WrapSTETH
readonly permit2Data: Permit2Permit
readonly stethAddress: string
readonly amount: BigNumberish
readonly wrapAmount: BigNumberish

constructor(amount: BigNumberish, chainId: number, permit2?: Permit2Permit, wrapAmount?: BigNumberish) {
this.stethAddress = STETH_ADDRESS(chainId)
this.amount = amount
this.wrapAmount = wrapAmount ?? CONTRACT_BALANCE

if (!!permit2) {
invariant(
permit2.details.token.toLowerCase() === this.stethAddress.toLowerCase(),
`must be permitting STETH address: ${this.stethAddress}`
)
invariant(permit2.details.amount >= amount, `Did not permit enough STETH for unwrapSTETH transaction`)
this.permit2Data = permit2
}
}

encode(planner: RoutePlanner, _: TradeConfig): void {
encodeInputTokenOptions(planner, {
permit2Permit: this.permit2Data,
permit2TransferFrom: {
token: this.stethAddress,
amount: this.amount.toString(),
},
})
planner.addCommand(CommandType.WRAP_STETH, [ROUTER_AS_RECIPIENT, this.wrapAmount])
}
}
7 changes: 6 additions & 1 deletion src/swapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,16 @@ export abstract class SwapRouter {
const UnwrapWETH = trade as UnwrapWETH
trade.encode(planner, { allowRevert: false })
currentNativeValueInRouter = currentNativeValueInRouter.add(UnwrapWETH.amount)
/**
* is (Un)WrapSTETH
*/
} else if (trade.tradeType == RouterTradeType.WrapSTETH || trade.tradeType == RouterTradeType.UnwrapSTETH) {
trade.encode(planner, { allowRevert: false })
/**
* else
*/
} else {
throw 'trade must be of instance: UniswapTrade or NFTTrade'
throw 'trade must be of instance: UniswapTrade, NFTTrade, UnwrapWETH, WrapSTETH'
}
}

Expand Down
62 changes: 56 additions & 6 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,131 @@ type ChainConfig = {
router: string
creationBlock: number
weth: string
steth: string
wsteth: string
}

const WETH_NOT_SUPPORTED_ON_CHAIN = '0x0000000000000000000000000000000000000000'
export const NOT_SUPPORTED_ON_CHAIN = '0x0000000000000000000000000000000000000000'

const CHAIN_CONFIGS: { [key: number]: ChainConfig } = {
// mainnet
[1]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
router: '0x3F6328669a86bef431Dc6F9201A5B90F7975a023',
weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
steth: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
wsteth: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
creationBlock: 17143817,
},
// goerli
[5]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
router: '0x3F6328669a86bef431Dc6F9201A5B90F7975a023',
weth: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6',
steth: '0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F',
wsteth: '0x6320cD32aA674d2898A68ec82e869385Fc5f7E2f',
creationBlock: 8940568,
},
// sepolia
[11155111]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
weth: '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 3543575,
},
// polygon
[137]: {
router: '0x643770E279d5D0733F21d6DC03A8efbABf3255B4',
weth: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 46866777,
},
//polygon mumbai
[80001]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
weth: '0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 35176052,
},
//optimism
[10]: {
router: '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4',
weth: '0x4200000000000000000000000000000000000006',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 108825869,
},
// optimism goerli
[420]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
weth: '0x4200000000000000000000000000000000000006',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 8887728,
},
// arbitrum
[42161]: {
router: '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4',
weth: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 125861718,
},
// arbitrum goerli
[421613]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
weth: '0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 18815277,
},
// celo
[42220]: {
router: '0x88a3ED7F21A3fCF6adb86b6F878C5B7a02D20e9b',
weth: WETH_NOT_SUPPORTED_ON_CHAIN,
weth: NOT_SUPPORTED_ON_CHAIN,
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 21116361,
},
// celo alfajores
[44787]: {
router: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
weth: WETH_NOT_SUPPORTED_ON_CHAIN,
weth: NOT_SUPPORTED_ON_CHAIN,
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 17566658,
},
// binance smart chain
[56]: {
router: '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4',
weth: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 31254967,
},
// avalanche
[43114]: {
router: '0x82635AF6146972cD6601161c4472ffe97237D292',
weth: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 34491144,
},
// base goerli
[84531]: {
router: '0xd0872d928672ae2ff74bdb2f5130ac12229cafaf',
weth: '0x4200000000000000000000000000000000000006',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 6915289,
},
// base mainnet
[8453]: {
router: '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4',
weth: '0x4200000000000000000000000000000000000006',
steth: NOT_SUPPORTED_ON_CHAIN,
wsteth: NOT_SUPPORTED_ON_CHAIN,
creationBlock: 3229053,
},
}
Expand All @@ -114,11 +146,29 @@ export const UNIVERSAL_ROUTER_CREATION_BLOCK = (chainId: number): number => {
export const WETH_ADDRESS = (chainId: number): string => {
if (!(chainId in CHAIN_CONFIGS)) throw new Error(`Universal Router not deployed on chain ${chainId}`)

if (CHAIN_CONFIGS[chainId].weth == WETH_NOT_SUPPORTED_ON_CHAIN) throw new Error(`Chain ${chainId} does not have WETH`)
if (CHAIN_CONFIGS[chainId].weth == NOT_SUPPORTED_ON_CHAIN) throw new Error(`Chain ${chainId} does not have WETH`)

return CHAIN_CONFIGS[chainId].weth
}

export const STETH_ADDRESS = (chainId: number): string => {
if (!(chainId in CHAIN_CONFIGS)) throw new Error(`Universal Router not deployed on chain ${chainId}`)

if (CHAIN_CONFIGS[chainId].steth == NOT_SUPPORTED_ON_CHAIN)
throw new Error(`Chain ${chainId} does not have STETH support`)

return CHAIN_CONFIGS[chainId].steth
}

export const WSTETH_ADDRESS = (chainId: number): string => {
if (!(chainId in CHAIN_CONFIGS)) throw new Error(`Universal Router not deployed on chain ${chainId}`)

if (CHAIN_CONFIGS[chainId].wsteth == NOT_SUPPORTED_ON_CHAIN)
throw new Error(`Chain ${chainId} does not have WSTETH support`)

return CHAIN_CONFIGS[chainId].wsteth
}

export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'

export const CONTRACT_BALANCE = BigNumber.from(2).pow(255)
Expand Down
4 changes: 4 additions & 0 deletions src/utils/routerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export enum CommandType {
SEAPORT_V1_4 = 0x20,
EXECUTE_SUB_PLAN = 0x21,
APPROVE_ERC20 = 0x22,
WRAP_STETH = 0x23,
UNWRAP_STETH = 0x24,
}

const ALLOW_REVERT_FLAG = 0x80
Expand Down Expand Up @@ -99,6 +101,8 @@ const ABI_DEFINITION: { [key in CommandType]: string[] } = {
[CommandType.OWNER_CHECK_721]: ['address', 'address', 'uint256'],
[CommandType.OWNER_CHECK_1155]: ['address', 'address', 'uint256', 'uint256'],
[CommandType.APPROVE_ERC20]: ['address', 'uint256'],
[CommandType.WRAP_STETH]: ['address', 'uint256'],
[CommandType.UNWRAP_STETH]: ['address', 'uint256'],

// NFT Markets
[CommandType.SEAPORT_V1_5]: ['uint256', 'bytes'],
Expand Down
Loading