Skip to content

Commit

Permalink
feat: 1inch integration (#587)
Browse files Browse the repository at this point in the history
Co-authored-by: valia fetisov <[email protected]>
  • Loading branch information
KirillDogadin-std and valiafetisov authored Mar 21, 2023
1 parent 42e3abd commit d8ed2aa
Show file tree
Hide file tree
Showing 17 changed files with 451 additions and 30 deletions.
11 changes: 11 additions & 0 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@uniswap/sdk": "^3.0.3",
"@uniswap/smart-order-router": "^2.10.0",
"async-await-queue": "^2.1.3",
"bignumber.js": "^9.0.1",
"date-fns": "^2.28.0",
"deep-equal-in-any-order": "^2.0.0",
Expand Down
21 changes: 19 additions & 2 deletions core/src/auctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
TakeEvent,
MarketData,
ExchangeFees,
GetCalleeDataParams,
} from './types';
import BigNumber from './bignumber';
import fetchAuctionsByCollateralType, {
Expand Down Expand Up @@ -337,6 +338,22 @@ export const bidWithDai = async function (
return executeTransaction(network, contractName, 'take', contractParameters, { notifier });
};

const buildGetCalleeDataParams = (marketData?: MarketData): GetCalleeDataParams | undefined => {
const preloadedPools = marketData && 'pools' in marketData ? marketData.pools : undefined;
const oneInchData = marketData && 'oneInch' in marketData ? marketData.oneInch : undefined;
if (preloadedPools && oneInchData) {
throw new Error('Cannot use both preloaded pools and oneInch data as params to get callee data');
}
if (preloadedPools) {
return {
pools: preloadedPools,
};
}
if (oneInchData) {
return { oneInchParams: { txData: oneInchData.calleeData, to: oneInchData.to } };
}
return undefined;
};
export const bidWithCallee = async function (
network: string,
auction: Auction,
Expand All @@ -346,8 +363,8 @@ export const bidWithCallee = async function (
): Promise<string> {
const calleeAddress = getCalleeAddressByCollateralType(network, auction.collateralType, marketId);
const marketData = auction.marketDataRecords?.[marketId];
const preloadedPools = marketData && 'pools' in marketData ? marketData.pools : undefined;
const calleeData = await getCalleeData(network, auction.collateralType, marketId, profitAddress, preloadedPools);
const params = buildGetCalleeDataParams(marketData);
const calleeData = await getCalleeData(network, auction.collateralType, marketId, profitAddress, params);
const contractName = getClipperNameByCollateralType(auction.collateralType);
const contractParameters = [
convertNumberTo32Bytes(auction.index),
Expand Down
5 changes: 3 additions & 2 deletions core/src/calleeFunctions/CurveLpTokenUniv3Callee.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
import type { CalleeFunctions, CollateralConfig, GetCalleeDataParams, Pool } from '../types';
import { ethers } from 'ethers';
import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
Expand All @@ -13,12 +13,13 @@ const getCalleeData = async function (
collateral: CollateralConfig,
marketId: string,
profitAddress: string,
preloadedPools?: Pool[]
params?: GetCalleeDataParams
): Promise<string> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'CurveLpTokenUniv3Callee') {
throw new Error(`Can not encode route for the "${collateral.ilk}"`);
}
const preloadedPools = !!params && 'pools' in params ? params.pools : undefined;
if (!preloadedPools) {
throw new Error(`Can not encode route for the "${collateral.ilk}" without preloaded pools`);
}
Expand Down
63 changes: 63 additions & 0 deletions core/src/calleeFunctions/OneInchCallee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { CalleeFunctions, CollateralConfig, GetCalleeDataParams } from '../types';
import { ethers } from 'ethers';
import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
import { getOneinchSwapParameters } from './helpers/oneInch';
import { DAI_NUMBER_OF_DIGITS } from '../constants/UNITS';

const getCalleeData = async function (
network: string,
collateral: CollateralConfig,
marketId: string,
profitAddress: string,
params?: GetCalleeDataParams
): Promise<string> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'OneInchCallee') {
throw new Error(`getCalleeData called with invalid collateral type "${collateral.ilk}"`);
}
const oneInchParams = !!params && 'oneInchParams' in params ? params.oneInchParams : undefined;
if (!oneInchParams) {
throw new Error(`getCalleeData called with invalid txData`);
}
const joinAdapterAddress = await getContractAddressByName(network, getJoinNameByCollateralType(collateral.ilk));
const minProfit = 1;
const typesArray = ['address', 'address', 'uint256', 'address', 'address', 'bytes'];
return ethers.utils.defaultAbiCoder.encode(typesArray, [
profitAddress,
joinAdapterAddress,
minProfit,
ethers.constants.AddressZero,
oneInchParams.to,
ethers.utils.hexDataSlice(oneInchParams.txData, 4),
]);
};

const getMarketPrice = async function (
network: string,
collateral: CollateralConfig,
marketId: string,
collateralAmount: BigNumber
): Promise<{ price: BigNumber; pools: undefined }> {
// convert collateral into DAI
const collateralIntegerAmount = collateralAmount.shiftedBy(collateral.decimals).toFixed(0);
const { toTokenAmount } = await getOneinchSwapParameters(
network,
collateral.symbol,
collateralIntegerAmount,
marketId
);

// return price per unit
return {
price: new BigNumber(toTokenAmount).shiftedBy(-DAI_NUMBER_OF_DIGITS).dividedBy(collateralAmount),
pools: undefined,
};
};

const UniswapV2CalleeDai: CalleeFunctions = {
getCalleeData,
getMarketPrice,
};

export default UniswapV2CalleeDai;
5 changes: 3 additions & 2 deletions core/src/calleeFunctions/UniswapV3Callee.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
import type { CalleeFunctions, CollateralConfig, GetCalleeDataParams, Pool } from '../types';
import { ethers } from 'ethers';
import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
Expand All @@ -12,12 +12,13 @@ const getCalleeData = async function (
collateral: CollateralConfig,
marketId: string,
profitAddress: string,
preloadedPools?: Pool[]
params?: GetCalleeDataParams
): Promise<string> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'UniswapV3Callee') {
throw new Error(`getCalleeData called with invalid collateral type "${collateral.ilk}"`);
}
const preloadedPools = !!params && 'pools' in params ? params.pools : undefined;
const pools = preloadedPools || (await getPools(network, collateral, marketId));
if (!pools) {
throw new Error(`getCalleeData called with invalid pools`);
Expand Down
174 changes: 174 additions & 0 deletions core/src/calleeFunctions/helpers/oneInch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { ethers } from 'ethers';
import { getCalleeAddressByCollateralType } from '../../constants/CALLEES';
import { getCollateralConfigBySymbol } from '../../constants/COLLATERALS';
import { getErc20SymbolByAddress } from '../../contracts';
import { getDecimalChainIdByNetworkType, getNetworkConfigByType } from '../../network';
import { CollateralConfig } from '../../types';
import BigNumber from '../../bignumber';
import { getTokenAddressByNetworkAndSymbol } from '../../tokens';
import { Queue } from 'async-await-queue';
import memoizee from 'memoizee';
import { convertETHtoDAI } from '../../fees';

const MAX_DELAY_BETWEEN_REQUESTS_MS = 600;
const REQUEST_QUEUE = new Queue(1, MAX_DELAY_BETWEEN_REQUESTS_MS);
const EXPECTED_SIGNATURE = '0x12aa3caf'; // see https://www.4byte.directory/signatures/?bytes4_signature=0x12aa3caf
const SUPPORTED_1INCH_NETWORK_IDS = [1, 56, 137, 10, 42161, 100, 43114]; // see https://help.1inch.io/en/articles/5528619-how-to-use-different-networks-on-1inch
const ONE_DAY_MS = 24 * 60 * 60 * 1000;

export const getOneInchUrl = (chainId: number) => {
return `https://api.1inch.io/v5.0/${chainId}`;
};

interface Protocol {
id: string;
title: string;
img: string;
img_color: string;
}

interface OneInchToken {
symbol: string;
name: string;
address: string;
decimals: 0;
logoURI: string;
}
interface OneInchSwapRepsonse {
fromToken: OneInchToken;
toToken: OneInchToken;
toTokenAmount: string;
fromTokenAmount: string;
protocols: OneInchSwapRoute[];
tx: {
from: string;
to: string;
data: string;
value: string;
gasPrice: string;
gas: string;
};
}
interface LiquiditySourcesResponse {
protocols: Protocol[];
}
type OneInchSwapRoute = { name: string; part: number; fromTokenAddress: string; toTokenAddress: string }[][];

const executeRequestInQueue = async (url: string) => {
const apiRequestSymbol = Symbol();
await REQUEST_QUEUE.wait(apiRequestSymbol);
const response = await fetch(url).then(res => res.json());
REQUEST_QUEUE.end(apiRequestSymbol);
return response;
};

export const executeOneInchApiRequest = async (
chainId: number,
endpoint: '/swap' | '/liquidity-sources',
params?: Record<string, any>
) => {
const oneInchUrl = getOneInchUrl(chainId);
const url = `${oneInchUrl}${endpoint}?${new URLSearchParams(params)}`;
const response = await executeRequestInQueue(url);
if (response.error) {
throw new Error(`failed to receive response from oneinch: ${response.error}`);
}
return response;
};

async function _getOneinchValidProtocols(chainId: number) {
// Fetch all supported protocols except for the limit orders
const response: LiquiditySourcesResponse = await executeOneInchApiRequest(chainId, '/liquidity-sources');
const protocolIds = response.protocols.map(protocol => protocol.id);
return protocolIds.filter(protocolId => !protocolId.toLowerCase().includes('limit'));
}

export const getOneinchValidProtocols = memoizee(_getOneinchValidProtocols, {
promise: true,
length: 1,
maxAge: ONE_DAY_MS,
});

export async function getOneinchSwapParameters(
network: string,
collateralSymbol: string,
amount: string,
marketId: string,
slippage = '1'
): Promise<OneInchSwapRepsonse> {
const isFork = getNetworkConfigByType(network).isFork;
const chainId = isFork ? 1 : getDecimalChainIdByNetworkType(network);
if (!isFork && !SUPPORTED_1INCH_NETWORK_IDS.includes(chainId)) {
throw new Error(`1inch does not support network ${network}`);
}
const toTokenAddress = await getTokenAddressByNetworkAndSymbol(network, 'DAI');
const fromTokenAddress = await getTokenAddressByNetworkAndSymbol(network, collateralSymbol);
const calleeAddress = getCalleeAddressByCollateralType(
network,
getCollateralConfigBySymbol(collateralSymbol).ilk,
marketId
);
// Documentation https://docs.1inch.io/docs/aggregation-protocol/api/swap-params/
const swapParams = {
fromTokenAddress,
toTokenAddress,
fromAddress: calleeAddress,
amount,
slippage,
allowPartialFill: false, // disable partial fill
disableEstimate: true, // disable eth_estimateGas
compatibilityMode: true, // always receive parameters for the `swap` call
};
const oneinchResponse = await executeOneInchApiRequest(chainId, '/swap', swapParams);
const functionSignature = ethers.utils.hexDataSlice(oneinchResponse.tx.data, 0, 4); // see https://docs.soliditylang.org/en/develop/abi-spec.html#function-selector
if (functionSignature !== EXPECTED_SIGNATURE) {
throw new Error(`Unexpected 1inch function signature: ${functionSignature}, expected: ${EXPECTED_SIGNATURE}`);
}
return oneinchResponse;
}

export async function extractPathFromSwapResponseProtocols(
network: string,
oneInchRoutes: OneInchSwapRoute[]
): Promise<string[]> {
const pathStepsResolves = await Promise.all(
oneInchRoutes[0].map(async route => {
return await Promise.all([
await getErc20SymbolByAddress(network, route[0].fromTokenAddress),
await getErc20SymbolByAddress(network, route[0].toTokenAddress),
]);
})
);
const path = [pathStepsResolves[0][0]];
for (const step of pathStepsResolves) {
path.push(step[1]);
}
return path;
}

export async function getOneInchMarketData(
network: string,
collateral: CollateralConfig,
amount: BigNumber,
marketId: string
) {
const swapData = await getOneinchSwapParameters(
network,
collateral.symbol,
amount.shiftedBy(collateral.decimals).toFixed(0),
marketId
);
const path = await extractPathFromSwapResponseProtocols(network, swapData.protocols);
const calleeData = swapData.tx.data;
const estimatedGas = swapData.tx.gas;
const exchangeFeeEth = new BigNumber(swapData.tx.gasPrice).multipliedBy(estimatedGas);
const exchangeFeeDai = await convertETHtoDAI(network, exchangeFeeEth);
const to = swapData.tx.to;
return {
path,
exchangeFeeEth,
exchangeFeeDai,
calleeData,
to,
};
}
13 changes: 12 additions & 1 deletion core/src/calleeFunctions/helpers/uniswapV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { DAI_NUMBER_OF_DIGITS, MKR_NUMBER_OF_DIGITS } from '../../constants/UNIT
import { getCollateralConfigBySymbol } from '../../constants/COLLATERALS';
import { getTokenAddressByNetworkAndSymbol } from '../../tokens';
import { Pool } from '../../types';
import memoizee from 'memoizee';
import { MARKET_DATA_RECORDS_CACHE_MS } from '..';

const UNISWAP_V3_QUOTER_ADDRESS = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6';
export const UNISWAP_FEE = 3000; // denominated in hundredths of a bip
Expand Down Expand Up @@ -54,7 +56,7 @@ export const convertCollateralToDaiUsingPool = async function (
return daiAmount;
};

export const convertSymbolToDai = async function (
const _convertSymbolToDai = async function (
network: string,
symbol: string,
amount: BigNumber,
Expand All @@ -73,6 +75,15 @@ export const convertSymbolToDai = async function (
return daiAmount;
};

export const convertSymbolToDai = memoizee(_convertSymbolToDai, {
promise: true,
length: 4,
maxAge: MARKET_DATA_RECORDS_CACHE_MS,
normalizer: (args: any[]) => {
return JSON.stringify(args); // use normalizer due to BigNumber object being an argument
},
});

export const convertDaiToSymbol = async function (
network: string,
symbol: string,
Expand Down
Loading

0 comments on commit d8ed2aa

Please sign in to comment.