Skip to content

Commit

Permalink
Merge pull request #209 from levelkdev/velodrome
Browse files Browse the repository at this point in the history
Velodrome
  • Loading branch information
Mi-Lan authored Dec 15, 2022
2 parents a307fa6 + 14c588e commit 0b2545f
Show file tree
Hide file tree
Showing 10 changed files with 4,717 additions and 154 deletions.
3,960 changes: 3,808 additions & 152 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/entities/trades/curve/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ export async function getRoutablePools(

// For mainnet, account for ETH/WETH
if (chainId === ChainId.MAINNET) {
console.log('mainnet', tokenInAddress)
const isTokenInEther = tokenIn.address.toLowerCase() === TOKENS_MAINNET.eth.address.toLowerCase()
const isTokenOutEther = tokenOut.address.toLowerCase() === TOKENS_MAINNET.eth.address.toLowerCase()

Expand Down
1 change: 1 addition & 0 deletions src/entities/trades/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './interfaces/trade-options'
export { BaseRoutablePlatform, RoutablePlatform, UniswapV2RoutablePlatform } from './routable-platform'
export * from './uniswap'
export * from './uniswap-v2'
export * from './velodrome'
1 change: 1 addition & 0 deletions src/entities/trades/routable-platform/RoutablePlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BaseRoutablePlatform } from './BaseRoutablePlatform'
export class RoutablePlatform extends BaseRoutablePlatform {
public static readonly ZEROX = new RoutablePlatform([ChainId.MAINNET, ChainId.POLYGON], '0x')
public static readonly CURVE = new RoutablePlatform([ChainId.MAINNET, ChainId.ARBITRUM_ONE, ChainId.XDAI], 'Curve')
public static readonly VELODROME = new RoutablePlatform([ChainId.OPTIMISM_MAINNET], 'Velodrome')
/**
* @deprecated Use {@link RoutablePlatform.COW} instead.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/entities/trades/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const RPC_PROVIDER_LIST: Record<ChainId, string> = {
[ChainId.ARBITRUM_GOERLI]: 'https://goerli-rollup.arbitrum.io/rpc',
[ChainId.POLYGON]: 'https://polygon-rpc.com',
[ChainId.GOERLI]: 'https://goerli.infura.io/v3/e1a3bfc40093494ca4f36b286ab36f2d',
[ChainId.OPTIMISM_MAINNET]: 'https://mainnet.optimism.io',
[ChainId.OPTIMISM_MAINNET]: 'https://opt-mainnet.g.alchemy.com/v2/6cRVjVO2uOTC9gWFCsBnquUwOM9zuWQZ',
[ChainId.OPTIMISM_GOERLI]: 'https://goerli.optimism.io',
[ChainId.BSC_MAINNET]: 'https://bsc-dataseed1.binance.org/',
[ChainId.BSC_TESTNET]: 'https://data-seed-prebsc-1-s1.binance.org:8545/',
Expand Down
291 changes: 291 additions & 0 deletions src/entities/trades/velodrome/Velodrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { AddressZero } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { BaseProvider } from '@ethersproject/providers'
import type { UnsignedTransaction } from '@ethersproject/transactions'
import { formatUnits } from '@ethersproject/units'
import debug from 'debug'
import invariant from 'tiny-invariant'

import { ChainId, ONE, TradeType } from '../../../constants'
import { validateAndParseAddress } from '../../../utils'
import { Currency } from '../../currency'
import { CurrencyAmount, Fraction, Percent, Price, TokenAmount } from '../../fractions'
import { maximumSlippage as defaultMaximumSlippage } from '../constants'
import { Trade } from '../interfaces/trade'
import { TradeOptions } from '../interfaces/trade-options'
import { RoutablePlatform } from '../routable-platform'
import { toHex, ZERO_HEX } from '../uniswap-v2/utilts'
import { getProvider, tryGetChainId, wrappedCurrency } from '../utils'
import { LIBRARY_ABI, ROUTER_ABI } from './abi'
import { LIBRARY_ADDRESS, ROUTER_ADDRESS } from './contants'
import { getBestRoute } from './utils'

export interface VelodromeQuoteTypes {
amount: CurrencyAmount
quoteCurrency: Currency
tradeType: TradeType
maximumSlippage?: Percent
recipient?: string
}

interface VelodromConstructorParams {
maximumSlippage: Percent
currencyAmountIn: CurrencyAmount
currencyAmountOut: CurrencyAmount
tradeType: TradeType
chainId: ChainId
routes: { from: string; to: string; stable: boolean }[]
priceImpact: Percent
}

// Debuging logger. See documentation to enable logging.
const debugVelodromeGetQuote = debug('ecoRouter:velodrome:getQuote')

/**
* UniswapTrade uses the AutoRouter to find best trade across V2 and V3 pools
*/
export class VelodromeTrade extends Trade {
/**
* @property Route for trade to go through
*/
public readonly routes?: { from: string; to: string; stable: boolean }[]

public constructor({
maximumSlippage,
currencyAmountIn,
currencyAmountOut,
tradeType,
chainId,
routes,
priceImpact,
}: VelodromConstructorParams) {
super({
details: undefined,
type: tradeType,
inputAmount: currencyAmountIn,
outputAmount: currencyAmountOut,
maximumSlippage,
platform: RoutablePlatform.VELODROME,
chainId,
executionPrice: new Price({
baseCurrency: currencyAmountIn.currency,
quoteCurrency: currencyAmountOut.currency,
denominator: currencyAmountIn.raw,
numerator: currencyAmountOut.raw,
}),
priceImpact,
fee: new Percent('2', '10000'),
approveAddress: ROUTER_ADDRESS,
})
this.routes = routes
}

static async getQuote(
{ amount, quoteCurrency, tradeType, maximumSlippage, recipient }: VelodromeQuoteTypes,
provider?: BaseProvider
): Promise<VelodromeTrade | null> {
const chainId = tryGetChainId(amount, quoteCurrency)
invariant(chainId, 'VelodromeQuote.getQuote: chainId is required')

// Defaults
recipient = recipient || AddressZero
maximumSlippage = maximumSlippage || defaultMaximumSlippage

provider = provider || getProvider(chainId)

// Must match the currencies provided
invariant(
(await provider.getNetwork()).chainId == chainId,
`VelodromTrade.getQuote: currencies chainId does not match provider's chainId`
)

const currencyIn = amount.currency
const currencyOut = quoteCurrency

const wrappedCurrencyIn = wrappedCurrency(currencyIn, chainId)
const wrappedCurrencyOut = wrappedCurrency(currencyOut, chainId)

debugVelodromeGetQuote({
amount,
quoteCurrency,
currencyIn,
currencyOut,
tradeType,
recipient,
maximumSlippage,
})

let bestAmountOut
let finalValue
try {
const bestAmount = await getBestRoute({
currencyIn: wrappedCurrencyIn,
currencyOut: wrappedCurrencyOut,
amount,
provider,
chainId,
})
bestAmountOut = bestAmount
finalValue = bestAmount?.finalValue.toString()
if (!bestAmount) {
return null
}
if (tradeType === TradeType.EXACT_OUTPUT) {
const bestAmountForOutput = await getBestRoute({
currencyIn: wrappedCurrencyOut,
currencyOut: wrappedCurrencyIn,
amount: new TokenAmount(wrappedCurrencyOut, bestAmount.finalValue.toString()),
provider,
chainId,
})
bestAmountOut = bestAmountForOutput
}
if (!finalValue || !bestAmountOut) {
return null
}

const libraryContract = new Contract(LIBRARY_ADDRESS, LIBRARY_ABI, provider)
let totalRatio = 1

for (let i = 0; i < bestAmountOut.routes.length; i++) {
const amountIn = bestAmountOut.receiveAmounts[i]

const res = await libraryContract['getTradeDiff(uint256,address,address,bool)'](
amountIn,
bestAmountOut.routes[i].from,
bestAmountOut.routes[i].to,
bestAmountOut.routes[i].stable
)

const decimals = tradeType === TradeType.EXACT_INPUT ? quoteCurrency.decimals : amount.currency.decimals
const numberA = formatUnits(res.a, decimals)
const numberB = formatUnits(res.b, decimals)

const ratio = parseFloat(numberB) / parseFloat(numberA)

totalRatio = totalRatio * ratio
}

const calculation = Math.round((1 - totalRatio) * 1000)

const priceImpact = new Percent(calculation.toString(), '1000')

const convertToNative = (amount: string, currency: Currency, chainId: number) => {
if (Currency.isNative(currency)) return CurrencyAmount.nativeCurrency(amount, chainId)
return new TokenAmount(wrappedCurrency(currency, chainId), amount)
}

const currencyAmountIn =
TradeType.EXACT_INPUT === tradeType ? amount : convertToNative(finalValue.toString(), currencyOut, chainId)
const currencyAmountOut =
TradeType.EXACT_INPUT === tradeType
? convertToNative(bestAmountOut.finalValue.toString(), currencyOut, chainId)
: convertToNative(bestAmountOut.finalValue.toString(), currencyIn, chainId)

return new VelodromeTrade({
maximumSlippage,
currencyAmountIn,
currencyAmountOut,
tradeType,
chainId,
routes: bestAmountOut.routes,
priceImpact,
})
} catch (ex) {
console.error(ex)
return null
}
}

public minimumAmountOut(): CurrencyAmount {
if (this.tradeType === TradeType.EXACT_OUTPUT) {
return this.outputAmount
} else {
const slippageAdjustedAmountOut = new Fraction(ONE)
.add(this.maximumSlippage)
.invert()
.multiply(this.outputAmount.raw).quotient
return this.outputAmount instanceof TokenAmount
? new TokenAmount(this.outputAmount.token, slippageAdjustedAmountOut)
: CurrencyAmount.nativeCurrency(slippageAdjustedAmountOut, this.chainId)
}
}

public maximumAmountIn(): CurrencyAmount {
if (this.tradeType === TradeType.EXACT_INPUT) {
return this.inputAmount
} else {
const slippageAdjustedAmountIn = new Fraction(ONE)
.add(this.maximumSlippage)
.multiply(this.inputAmount.raw).quotient
return this.inputAmount instanceof TokenAmount
? new TokenAmount(this.inputAmount.token, slippageAdjustedAmountIn)
: CurrencyAmount.nativeCurrency(slippageAdjustedAmountIn, this.chainId)
}
}

/**
* Returns unsigned transaction for the trade
* @returns the unsigned transaction
*/
public async swapTransaction(options: TradeOptions): Promise<UnsignedTransaction> {
const nativeCurrency = Currency.getNative(this.chainId)
const etherIn = this.inputAmount.currency === nativeCurrency
const etherOut = this.outputAmount.currency === nativeCurrency
// the router does not support both ether in and out
invariant(this.routes, 'No Avaliable routes')
invariant(!(etherIn && etherOut), 'ETHER_IN_OUT')
invariant(options.ttl && options.ttl > 0, 'TTL')
invariant(this.inputAmount.currency.address && this.outputAmount.currency.address, 'No currency')

const to: string = validateAndParseAddress(options.recipient)
const amountIn: string = toHex(this.maximumAmountIn())
const amountOut: string = toHex(this.minimumAmountOut())

const deadline = `0x${(Math.floor(new Date().getTime() / 1000) + options.ttl).toString(16)}`

let methodName: string
let args: any
let value: string = ZERO_HEX
switch (this.tradeType) {
case TradeType.EXACT_INPUT:
if (etherIn) {
methodName = 'swapExactETHForTokens'
// (uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountOut, this.routes, to, deadline]
value = amountIn
} else if (etherOut) {
methodName = 'swapExactTokensForETH'
// (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountIn, amountOut, this.routes, to, deadline]
value = ZERO_HEX
} else {
methodName = 'swapExactTokensForTokens'
// (uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
args = [amountIn, amountOut, this.routes, to, deadline]
value = ZERO_HEX
}
break
case TradeType.EXACT_OUTPUT:
if (etherIn) {
methodName = 'swapETHForExactTokens'
// (uint amountOut, address[] calldata path, address to, uint deadline)
args = [amountOut, this.routes, to, deadline]
value = amountIn
} else if (etherOut) {
methodName = 'swapTokensForExactETH'
// (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
args = [amountOut, amountIn, this.routes, to, deadline]
value = ZERO_HEX
} else {
methodName = 'swapTokensForExactTokens'
// (uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
args = [amountOut, amountIn, this.routes, to, deadline]
value = ZERO_HEX
}
break
}

return new Contract(ROUTER_ADDRESS, ROUTER_ABI).populateTransaction[methodName](...args, { value })
}
}
Loading

0 comments on commit 0b2545f

Please sign in to comment.