Skip to content
Merged
33 changes: 28 additions & 5 deletions src/lib/swapper/swappers/CowSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import type { Result } from '@sniptt/monads/build'
import { getConfig } from 'config'
import { getDefaultSlippageDecimalPercentageForSwapper } from 'constants/constants'
import { v4 as uuid } from 'uuid'
import { bn } from 'lib/bignumber/bignumber'
import { bn, bnOrZero } from 'lib/bignumber/bignumber'
import { createDefaultStatusResponse } from 'lib/utils/evm'
import { convertBasisPointsToDecimalPercentage } from 'state/slices/tradeQuoteSlice/utils'

import { isNativeEvmAsset } from '../utils/helpers/helpers'
import { getCowSwapTradeQuote } from './getCowSwapTradeQuote/getCowSwapTradeQuote'
Expand All @@ -33,6 +34,7 @@ import {
} from './utils/constants'
import { cowService } from './utils/cowService'
import {
getAffiliateAppDataFragmentByChainId,
getCowswapNetwork,
getFullAppData,
getNowPlusThirtyMinutesTimestamp,
Expand Down Expand Up @@ -78,7 +80,14 @@ export const cowApi: SwapperApi = {
const network = maybeNetwork.unwrap()
const baseUrl = getConfig().REACT_APP_COWSWAP_BASE_URL

const { appData, appDataHash } = await getFullAppData(slippageTolerancePercentageDecimal)
const affiliateAppDataFragment = getAffiliateAppDataFragmentByChainId({
affiliateBps: tradeQuote.affiliateBps,
chainId: sellAsset.chainId,
})
const { appData, appDataHash } = await getFullAppData(
slippageTolerancePercentageDecimal,
affiliateAppDataFragment,
)
// https://api.cow.fi/docs/#/default/post_api_v1_quote
const maybeQuoteResponse = await cowService.post<CowSwapQuoteResponse>(
`${baseUrl}/${network}/api/v1/quote/`,
Expand All @@ -105,9 +114,23 @@ export const cowApi: SwapperApi = {
// For the slippage actually to be enforced, the final message to be signed needs to have slippage deducted.
// Failure to do so means orders may take forever to be filled, or never be filled at all.
const quoteBuyAmount = quote.buyAmount
const slippageDeductedBuyAmount = bn(quoteBuyAmount).minus(
bn(quoteBuyAmount).times(slippageTolerancePercentageDecimal),

const hasAffiliateFee = bnOrZero(tradeQuote.affiliateBps).gt(0)

// Remove affiliate fees off the buyAmount to get the amount after affiliate fees, but before slippage bips
const buyAmountAfterAffiliateFeesCryptoBaseUnit = hasAffiliateFee
? bn(quoteBuyAmount)
.times(bn(1).minus(convertBasisPointsToDecimalPercentage(tradeQuote.affiliateBps)))
.toFixed(0)
: quoteBuyAmount
const buyAmountAfterAffiliateFeesAndSlippageCryptoBaseUnit = bn(
buyAmountAfterAffiliateFeesCryptoBaseUnit,
)
.minus(
bn(buyAmountAfterAffiliateFeesCryptoBaseUnit).times(slippageTolerancePercentageDecimal),
)
.toFixed(0)

// CoW API and flow is weird - same idea as the mutation above, we need to incorporate protocol fees into the order
// This was previously working as-is with fees being deducted from the sell amount at protocol-level, but we now we need to add them into the order
// In other words, this means what was previously CoW being "feeless" as far as we're concerned
Expand All @@ -120,7 +143,7 @@ export const cowApi: SwapperApi = {
// Another mutation from the original quote to go around the fact that CoW API flow is weird
// they return us a quote with fees, but we have to zero them out when sending the order
feeAmount: '0',
buyAmount: slippageDeductedBuyAmount.toFixed(0),
buyAmount: buyAmountAfterAffiliateFeesAndSlippageCryptoBaseUnit,
sellAmount: sellAmountPlusProtocolFees.toFixed(0),
// from,
sellTokenBalance: ERC20_TOKEN_BALANCE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { cowService } from 'lib/swapper/swappers/CowSwapper/utils/cowService'
import {
assertValidTrade,
getAffiliateAppDataFragmentByChainId,
getCowswapNetwork,
getFullAppData,
getNowPlusThirtyMinutesTimestamp,
Expand All @@ -34,6 +35,8 @@ export async function getCowSwapTradeQuote(
chainId,
receiveAddress,
sellAmountIncludingProtocolFeesCryptoBaseUnit,
potentialAffiliateBps,
affiliateBps,
} = input

const slippageTolerancePercentageDecimal =
Expand All @@ -57,7 +60,15 @@ export async function getCowSwapTradeQuote(
const network = maybeNetwork.unwrap()
const baseUrl = getConfig().REACT_APP_COWSWAP_BASE_URL

const { appData, appDataHash } = await getFullAppData(slippageTolerancePercentageDecimal)
const affiliateAppDataFragment = getAffiliateAppDataFragmentByChainId({
affiliateBps,
chainId: sellAsset.chainId,
})

const { appData, appDataHash } = await getFullAppData(
slippageTolerancePercentageDecimal,
affiliateAppDataFragment,
)

// https://api.cow.fi/docs/#/default/post_api_v1_quote
const maybeQuoteResponse = await cowService.post<CowSwapQuoteResponse>(
Expand Down Expand Up @@ -95,24 +106,21 @@ export async function getCowSwapTradeQuote(

const { data } = maybeQuoteResponse.unwrap()

const {
feeAmount: feeAmountInSellTokenCryptoBaseUnit,
buyAmount: buyAmountAfterFeesCryptoBaseUnit,
} = data.quote
const { feeAmount: feeAmountInSellTokenCryptoBaseUnit } = data.quote
Comment thread
kaladinlight marked this conversation as resolved.

const { rate, buyAmountBeforeFeesCryptoBaseUnit } = getValuesFromQuoteResponse({
buyAsset,
sellAsset,
response: data,
})
const { rate, buyAmountAfterFeesCryptoBaseUnit, buyAmountBeforeFeesCryptoBaseUnit } =
getValuesFromQuoteResponse({
buyAsset,
sellAsset,
response: data,
affiliateBps,
})

const quote: TradeQuote = {
id: data.id.toString(),
receiveAddress,
// CowSwap does not support affiliate bps
// But we still need to return them as 0, so they are properly displayed as such at view-layer
affiliateBps: '0',
potentialAffiliateBps: '0',
affiliateBps,
potentialAffiliateBps,
rate,
slippageTolerancePercentageDecimal,
steps: [
Expand Down
7 changes: 7 additions & 0 deletions src/lib/swapper/swappers/CowSwapper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ export type CowSwapGetTradesResponse = {
export type CowSwapGetTransactionsResponse = {
status: 'presignaturePending' | 'open' | 'fulfilled' | 'cancelled' | 'expired'
}[]

export type AffiliateAppDataFragment = {
partnerFee?: {
bps: number
recipient: string
}
}
46 changes: 41 additions & 5 deletions src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import { Err, Ok } from '@sniptt/monads'
import type { TypedDataDomain, TypedDataField } from 'ethers'
import { TypedDataEncoder } from 'ethers'
import { keccak256, stringToBytes } from 'viem'
import { bnOrZero, convertPrecision } from 'lib/bignumber/bignumber'
import { bn, bnOrZero, convertPrecision } from 'lib/bignumber/bignumber'
import { fromBaseUnit } from 'lib/math'
import { convertDecimalPercentageToBasisPoints } from 'state/slices/tradeQuoteSlice/utils'
import { getTreasuryAddressFromChainId } from 'lib/swapper/swappers/utils/helpers/helpers'
import {
convertBasisPointsToDecimalPercentage,
convertDecimalPercentageToBasisPoints,
} from 'state/slices/tradeQuoteSlice/utils'

import type { CowSwapQuoteResponse } from '../../types'
import type { AffiliateAppDataFragment, CowSwapQuoteResponse } from '../../types'
import { CowNetwork } from '../../types'

export const ORDER_TYPE_FIELDS = [
Expand Down Expand Up @@ -158,19 +162,29 @@ type GetValuesFromQuoteResponseArgs = {
buyAsset: Asset
sellAsset: Asset
response: CowSwapQuoteResponse
affiliateBps: string
}

export const getValuesFromQuoteResponse = ({
buyAsset,
sellAsset,
response,
affiliateBps,
}: GetValuesFromQuoteResponseArgs) => {
const {
sellAmount: sellAmountAfterFeesCryptoBaseUnit,
feeAmount: feeAmountInSellTokenCryptoBaseUnit,
buyAmount: buyAmountAfterFeesCryptoBaseUnit,
buyAmount,
} = response.quote

const hasAffiliateFee = bnOrZero(affiliateBps).gt(0)
// Remove affiliate fees off the buyAmount to get the amount after affiliate fees, but before slippage bips
Comment thread
kaladinlight marked this conversation as resolved.
const buyAmountAfterFeesCryptoBaseUnit = hasAffiliateFee
? bn(buyAmount)
.times(bn(1).minus(convertBasisPointsToDecimalPercentage(affiliateBps)))
.toFixed(0)
: buyAmount

const buyAmountAfterFeesCryptoPrecision = fromBaseUnit(
buyAmountAfterFeesCryptoBaseUnit,
buyAsset.precision,
Expand Down Expand Up @@ -215,7 +229,10 @@ const generateAppDataFromDoc = async (

const metadataApi = new MetadataApi()
// See https://api.cow.fi/docs/#/default/post_api_v1_quote / https://github.com/cowprotocol/app-data
export const getFullAppData = async (slippageTolerancePercentage: string) => {
export const getFullAppData = async (
slippageTolerancePercentage: string,
affiliateAppDataFragment: AffiliateAppDataFragment,
) => {
const APP_CODE = 'shapeshift'
const orderClass: OrderClass = { orderClass: 'market' }
const quote = {
Expand All @@ -227,9 +244,28 @@ export const getFullAppData = async (slippageTolerancePercentage: string) => {
metadata: {
quote,
orderClass,
...affiliateAppDataFragment,
},
})

const { fullAppData, appDataKeccak256 } = await generateAppDataFromDoc(appDataDoc)
return { appDataHash: appDataKeccak256, appData: fullAppData }
}

export const getAffiliateAppDataFragmentByChainId = ({
affiliateBps,
chainId,
}: {
affiliateBps: string
chainId: ChainId
}): AffiliateAppDataFragment => {
const hasAffiliateFee = bnOrZero(affiliateBps).gt(0)
if (!hasAffiliateFee) return {}

return {
partnerFee: {
bps: Number(affiliateBps),
recipient: getTreasuryAddressFromChainId(chainId),
},
}
}