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

feat: permit2 allowance flow for zrx swaps #7758

Merged
merged 26 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
88e662d
chore: break up approval components into reusable ones
woodenfurniture Sep 17, 2024
31f1fe8
fix: bring back gome safe changes to approvals post rebase
woodenfurniture Sep 17, 2024
f944d18
fix: multiple bugs
woodenfurniture Sep 18, 2024
fb205f7
fix: use proper loading translation for approval gas fee
woodenfurniture Sep 19, 2024
34f722c
wip: initial prgression of permit2 ui
woodenfurniture Sep 19, 2024
d87fd77
feat: complete rework of approval ux flow
woodenfurniture Sep 20, 2024
8e7f1b3
chore: split up approval requirements hooks
woodenfurniture Sep 22, 2024
3cded98
feat: upgrade zrx swapper quotes to support v2 api endpoints with per…
woodenfurniture Sep 23, 2024
fd2ff91
feat: wire up initial requirement setting for permit2
woodenfurniture Sep 23, 2024
9bcf8dc
fix: swapper tests
woodenfurniture Sep 23, 2024
d006f9e
wip: best effort initial blocking out of permit2
woodenfurniture Sep 23, 2024
fae6132
chore: finish feature flag for zrx permit2 swaps
woodenfurniture Sep 23, 2024
72963b2
wip: more progress wiring up permit2 with mock 0x flow
woodenfurniture Sep 24, 2024
e9d7df5
wip: progression of permit2 flow up to signature
woodenfurniture Sep 24, 2024
d2524b6
feat: use shapeshift api proxy for 0x swapper
woodenfurniture Sep 24, 2024
3b3851b
feat: plumb in permit2 signature to trade execution
woodenfurniture Sep 24, 2024
6133504
feat: wiring up permit2 signature
woodenfurniture Sep 25, 2024
42b3bb0
fix: gas estimate in 0x quotes
woodenfurniture Sep 25, 2024
7d4ef4d
fix: dont bring back the allowance step content when the trade completes
woodenfurniture Sep 25, 2024
c0be46e
chore: cleanup
woodenfurniture Sep 26, 2024
19a4c62
fix: 0x trades from native assets
woodenfurniture Oct 1, 2024
dece4fd
chore: actioned low hanging gome feedback
woodenfurniture Oct 17, 2024
73d5cbc
chore: actioned beard ui feedback
woodenfurniture Oct 18, 2024
9264c05
chore: actioned apo review feedback
woodenfurniture Oct 20, 2024
5012092
Merge branch 'develop' into permit2
gomesalexandre Oct 21, 2024
131f9dd
Merge branch 'develop' into permit2
gomesalexandre Oct 21, 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: 4 additions & 0 deletions .env.base
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ REACT_APP_FEATURE_FOX_PAGE_FOX_SECTION=true
REACT_APP_FEATURE_FOX_PAGE_FOX_FARMING_SECTION=false
REACT_APP_FEATURE_FOX_PAGE_GOVERNANCE=false
REACT_APP_FEATURE_PHANTOM_WALLET=true
REACT_APP_FEATURE_ZRX_PERMIT2=false

# absolute URL prefix
REACT_APP_ABSOLUTE_URL_PREFIX=https://app.shapeshift.com
Expand Down Expand Up @@ -172,3 +173,6 @@ REACT_APP_SENTRY_DSN_URL=https://c612e7f4ef0637e4add433a2f4683aa8@o4507174990905

# Zerion
REACT_APP_ZERION_BASE_URL=https://api.proxy.shapeshift.com/api/v1/zerion

# 0x
REACT_APP_ZRX_BASE_URL=https://api.proxy.shapeshift.com/api/v1/zrx/
4 changes: 4 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ REACT_APP_FEATURE_FOX_PAGE_FOX_SECTION=true
REACT_APP_FEATURE_FOX_PAGE_FOX_FARMING_SECTION=true
REACT_APP_FEATURE_FOX_PAGE_GOVERNANCE=true
REACT_APP_FEATURE_PHANTOM_WALLET=true
REACT_APP_FEATURE_ZRX_PERMIT2=true

# logging
REACT_APP_REDUX_WINDOW=false
Expand Down Expand Up @@ -63,3 +64,6 @@ REACT_APP_SOLANA_NODE_URL=https://dev-api.solana.shapeshift.com/api/v1/jsonrpc

# thorchain
REACT_APP_MIDGARD_URL=https://dev-indexer.thorchain.shapeshift.com/v2

# 0x
REACT_APP_ZRX_BASE_URL=https://dev-api.proxy.shapeshift.com/api/v1/zrx/
4 changes: 4 additions & 0 deletions packages/contracts/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,7 @@ export const UNI_V2_FOX_STAKING_REWARDS_CONTRACTS = [
UNI_V2_FOX_STAKING_REWARDS_V4_CONTRACT,
UNI_V2_FOX_STAKING_REWARDS_V5_CONTRACT,
] as const

// Permit2 is deployed here across all chains.
// https://0x.org/docs/introduction/0x-cheat-sheet#permit2-contract
export const PERMIT2_CONTRACT = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
12 changes: 12 additions & 0 deletions packages/contracts/src/viemClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ export const viemClientByChainId: Record<ChainId, PublicClient> = {
[KnownChainIds.BaseMainnet]: viemBaseClient,
}

export const viemNetworkIdByChainId: Record<ChainId, number> = {
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
[KnownChainIds.EthereumMainnet]: mainnet.id,
[KnownChainIds.BnbSmartChainMainnet]: bsc.id,
[KnownChainIds.AvalancheMainnet]: avalanche.id,
[KnownChainIds.ArbitrumMainnet]: arbitrum.id,
[KnownChainIds.ArbitrumNovaMainnet]: arbitrumNova.id,
[KnownChainIds.GnosisMainnet]: gnosis.id,
[KnownChainIds.PolygonMainnet]: polygon.id,
[KnownChainIds.OptimismMainnet]: optimism.id,
[KnownChainIds.BaseMainnet]: base.id,
}

export const viemClientByNetworkId: Record<number, PublicClient> = {
[mainnet.id]: viemEthMainnetClient,
[bsc.id]: viemBscClient,
Expand Down
99 changes: 72 additions & 27 deletions packages/swapper/src/swappers/ZrxSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import { fromChainId } from '@shapeshiftoss/caip'
import { evm } from '@shapeshiftoss/chain-adapters'
import type { Result } from '@sniptt/monads/build'
import BigNumber from 'bignumber.js'
import type { Hex } from 'viem'
import { concat, numberToHex, size } from 'viem'

import type {
EvmTransactionRequest,
GetEvmTradeQuoteInput,
GetTradeQuoteInput,
GetUnsignedEvmTransactionArgs,
SwapErrorRight,
SwapperApi,
SwapperDeps,
TradeQuote,
import { getDefaultSlippageDecimalPercentageForSwapper } from '../../constants'
import {
type EvmTransactionRequest,
type GetEvmTradeQuoteInput,
type GetTradeQuoteInput,
type GetUnsignedEvmTransactionArgs,
type SwapErrorRight,
type SwapperApi,
type SwapperDeps,
SwapperName,
type TradeQuote,
} from '../../types'
import { checkEvmSwapStatus } from '../../utils'
import { getZrxTradeQuote } from './getZrxTradeQuote/getZrxTradeQuote'
Expand All @@ -20,11 +24,14 @@ import { fetchFromZrx } from './utils/fetchFromZrx'
export const zrxApi: SwapperApi = {
getTradeQuote: async (
input: GetTradeQuoteInput,
{ assertGetEvmChainAdapter }: SwapperDeps,
{ assertGetEvmChainAdapter, assetsById, config }: SwapperDeps,
): Promise<Result<TradeQuote[], SwapErrorRight>> => {
const tradeQuoteResult = await getZrxTradeQuote(
input as GetEvmTradeQuoteInput,
assertGetEvmChainAdapter,
config.REACT_APP_FEATURE_ZRX_PERMIT2,
assetsById,
config.REACT_APP_ZRX_BASE_URL,
)

return tradeQuoteResult.map(tradeQuote => {
Expand All @@ -36,33 +43,71 @@ export const zrxApi: SwapperApi = {
chainId,
from,
tradeQuote,
permit2Signature,
supportsEIP1559,
assertGetEvmChainAdapter,
config,
}: GetUnsignedEvmTransactionArgs): Promise<EvmTransactionRequest> => {
const { affiliateBps, receiveAddress, slippageTolerancePercentageDecimal, steps } = tradeQuote
const { buyAsset, sellAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = steps[0]

// We need to re-fetch the quote from 0x here because actual quote fetches include validation of
// approvals, which prevent quotes during trade input from succeeding if the user hasn't already
// approved the token they are getting a quote for.
// TODO: we'll want to let users know if the quoted amounts change much after re-fetching
const zrxQuoteResponse = await fetchFromZrx({
priceOrQuote: 'quote',
const {
buyAsset,
sellAsset,
sellAmountIncludingProtocolFeesCryptoBaseUnit,
receiveAddress,
affiliateBps: affiliateBps ?? '0',
slippageTolerancePercentageDecimal,
})
transactionMetadata,
} = steps[0]

const { value, to, data, estimatedGas } = await (async () => {
// If this is a quote from the 0x V2 API, i.e. has `transactionMetadata`, the comment below RE
// re-fetching does not apply. We must use the original transaction returned in the quote
// because the Permit2 signature is coupled to it.
if (transactionMetadata) {
return {
value: transactionMetadata.value?.toString() ?? '0',
to: transactionMetadata.to ?? '0x',
data: transactionMetadata.data ?? '0x',
estimatedGas: transactionMetadata.gas?.toString() ?? '0',
}
}

// We need to re-fetch the quote from 0x here because actual quote fetches include validation of
// approvals, which prevent quotes during trade input from succeeding if the user hasn't already
// approved the token they are getting a quote for.
// TODO: we'll want to let users know if the quoted amounts change much after re-fetching
const zrxQuoteResponse = await fetchFromZrx({
priceOrQuote: 'quote',
buyAsset,
sellAsset,
sellAmountIncludingProtocolFeesCryptoBaseUnit,
receiveAddress,
affiliateBps: affiliateBps ?? '0',
slippageTolerancePercentageDecimal:
slippageTolerancePercentageDecimal ??
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Zrx),
zrxBaseUrl: config.REACT_APP_ZRX_BASE_URL,
})

if (zrxQuoteResponse.isErr()) throw zrxQuoteResponse.unwrapErr()

return zrxQuoteResponse.unwrap()
})()

if (zrxQuoteResponse.isErr()) throw zrxQuoteResponse.unwrapErr()
const calldataWithSignature = (() => {
if (!permit2Signature) return data

const { value, to, data, estimatedGas } = zrxQuoteResponse.unwrap()
// Append the signature to the calldata
// For details, see
// https://0x.org/docs/0x-swap-api/guides/swap-tokens-with-0x-swap-api#5-append-signature-length-and-signature-data-to-transactiondata
const signatureLengthInHex = numberToHex(size(permit2Signature as Hex), {
signed: false,
size: 32,
})
return concat([data, signatureLengthInHex, permit2Signature] as Hex[])
})()

// Gas estimation
const { gasLimit, ...feeData } = await evm.getFees({
adapter: assertGetEvmChainAdapter(chainId),
data,
data: calldataWithSignature,
to,
value,
from,
Expand All @@ -73,9 +118,9 @@ export const zrxApi: SwapperApi = {
to,
from,
value,
data,
data: calldataWithSignature,
chainId: Number(fromChainId(chainId).chainReference),
// Use the higher amount of the node or the API, as the node doesn't always provide enought gas padding for
// Use the higher amount of the node or the API, as the node doesn't always provide enough gas padding for
// total gas used.
gasLimit: BigNumber.max(gasLimit, estimatedGas).toFixed(),
...feeData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@ vi.mock('@shapeshiftoss/chain-adapters', async () => {
const mockOk = Ok
const mockErr = Err
describe('getZrxTradeQuote', () => {
const zrxBaseUrl = 'https://0x.shapeshift.com/ethereum/'

const assertGetChainAdapter = (_chainId: ChainId) =>
({
getChainId: () => KnownChainIds.EthereumMainnet,
getGasFeeData: () => Promise.resolve(gasFeeData),
}) as unknown as EvmChainAdapter
const zrxService = zrxServiceFactory({ baseUrl: 'https://0x.shapeshift.com/ethereum/' })
const zrxService = zrxServiceFactory({ baseUrl: zrxBaseUrl })

it('returns quote with fee data', async () => {
const { quoteInput } = setupQuote()
Expand All @@ -75,7 +77,13 @@ describe('getZrxTradeQuote', () => {
} as AxiosResponse<unknown, any>),
),
)
const maybeQuote = await getZrxTradeQuote(quoteInput, assertGetChainAdapter)
const maybeQuote = await getZrxTradeQuote(
quoteInput,
assertGetChainAdapter,
false,
{},
zrxBaseUrl,
)

expect(maybeQuote.isErr()).toBe(false)
const quote = maybeQuote.unwrap()
Expand All @@ -96,7 +104,13 @@ describe('getZrxTradeQuote', () => {
>,
),
)
const maybeTradeQuote = await getZrxTradeQuote(quoteInput, assertGetChainAdapter)
const maybeTradeQuote = await getZrxTradeQuote(
quoteInput,
assertGetChainAdapter,
false,
{},
zrxBaseUrl,
)

expect(maybeTradeQuote.isErr()).toBe(true)
expect(maybeTradeQuote.unwrapErr()).toMatchObject({
Expand All @@ -112,7 +126,13 @@ describe('getZrxTradeQuote', () => {
}) as unknown as never,
)

const maybeTradeQuote = await getZrxTradeQuote(quoteInput, assertGetChainAdapter)
const maybeTradeQuote = await getZrxTradeQuote(
quoteInput,
assertGetChainAdapter,
false,
{},
zrxBaseUrl,
)

expect(maybeTradeQuote.isErr()).toBe(true)
expect(maybeTradeQuote.unwrapErr()).toMatchObject({
Expand All @@ -129,7 +149,13 @@ describe('getZrxTradeQuote', () => {
} as AxiosResponse<unknown>),
),
)
const maybeQuote = await getZrxTradeQuote(quoteInput, assertGetChainAdapter)
const maybeQuote = await getZrxTradeQuote(
quoteInput,
assertGetChainAdapter,
false,
{},
zrxBaseUrl,
)
expect(maybeQuote.isErr()).toBe(false)
const quote = maybeQuote.unwrap()

Expand All @@ -149,6 +175,9 @@ describe('getZrxTradeQuote', () => {
buyAsset: BTC,
},
assertGetChainAdapter,
false,
{},
zrxBaseUrl,
)

expect(maybeTradeQuote.isErr()).toBe(true)
Expand All @@ -172,6 +201,9 @@ describe('getZrxTradeQuote', () => {
sellAsset: { ...sellAsset, chainId: btcChainId },
},
assertGetChainAdapter,
false,
{},
zrxBaseUrl,
)

expect(maybeTradeQuote.isErr()).toBe(true)
Expand Down
Loading
Loading