Skip to content

Commit d8ed2aa

Browse files
feat: 1inch integration (#587)
Co-authored-by: valia fetisov <contact@valiafetisov.com>
1 parent 42e3abd commit d8ed2aa

17 files changed

+451
-30
lines changed

core/package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@nomiclabs/hardhat-ethers": "^2.1.0",
3333
"@uniswap/sdk": "^3.0.3",
3434
"@uniswap/smart-order-router": "^2.10.0",
35+
"async-await-queue": "^2.1.3",
3536
"bignumber.js": "^9.0.1",
3637
"date-fns": "^2.28.0",
3738
"deep-equal-in-any-order": "^2.0.0",

core/src/auctions.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
TakeEvent,
77
MarketData,
88
ExchangeFees,
9+
GetCalleeDataParams,
910
} from './types';
1011
import BigNumber from './bignumber';
1112
import fetchAuctionsByCollateralType, {
@@ -337,6 +338,22 @@ export const bidWithDai = async function (
337338
return executeTransaction(network, contractName, 'take', contractParameters, { notifier });
338339
};
339340

341+
const buildGetCalleeDataParams = (marketData?: MarketData): GetCalleeDataParams | undefined => {
342+
const preloadedPools = marketData && 'pools' in marketData ? marketData.pools : undefined;
343+
const oneInchData = marketData && 'oneInch' in marketData ? marketData.oneInch : undefined;
344+
if (preloadedPools && oneInchData) {
345+
throw new Error('Cannot use both preloaded pools and oneInch data as params to get callee data');
346+
}
347+
if (preloadedPools) {
348+
return {
349+
pools: preloadedPools,
350+
};
351+
}
352+
if (oneInchData) {
353+
return { oneInchParams: { txData: oneInchData.calleeData, to: oneInchData.to } };
354+
}
355+
return undefined;
356+
};
340357
export const bidWithCallee = async function (
341358
network: string,
342359
auction: Auction,
@@ -346,8 +363,8 @@ export const bidWithCallee = async function (
346363
): Promise<string> {
347364
const calleeAddress = getCalleeAddressByCollateralType(network, auction.collateralType, marketId);
348365
const marketData = auction.marketDataRecords?.[marketId];
349-
const preloadedPools = marketData && 'pools' in marketData ? marketData.pools : undefined;
350-
const calleeData = await getCalleeData(network, auction.collateralType, marketId, profitAddress, preloadedPools);
366+
const params = buildGetCalleeDataParams(marketData);
367+
const calleeData = await getCalleeData(network, auction.collateralType, marketId, profitAddress, params);
351368
const contractName = getClipperNameByCollateralType(auction.collateralType);
352369
const contractParameters = [
353370
convertNumberTo32Bytes(auction.index),

core/src/calleeFunctions/CurveLpTokenUniv3Callee.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
1+
import type { CalleeFunctions, CollateralConfig, GetCalleeDataParams, Pool } from '../types';
22
import { ethers } from 'ethers';
33
import BigNumber from '../bignumber';
44
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
@@ -13,12 +13,13 @@ const getCalleeData = async function (
1313
collateral: CollateralConfig,
1414
marketId: string,
1515
profitAddress: string,
16-
preloadedPools?: Pool[]
16+
params?: GetCalleeDataParams
1717
): Promise<string> {
1818
const marketData = collateral.exchanges[marketId];
1919
if (marketData?.callee !== 'CurveLpTokenUniv3Callee') {
2020
throw new Error(`Can not encode route for the "${collateral.ilk}"`);
2121
}
22+
const preloadedPools = !!params && 'pools' in params ? params.pools : undefined;
2223
if (!preloadedPools) {
2324
throw new Error(`Can not encode route for the "${collateral.ilk}" without preloaded pools`);
2425
}
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { CalleeFunctions, CollateralConfig, GetCalleeDataParams } from '../types';
2+
import { ethers } from 'ethers';
3+
import BigNumber from '../bignumber';
4+
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
5+
import { getOneinchSwapParameters } from './helpers/oneInch';
6+
import { DAI_NUMBER_OF_DIGITS } from '../constants/UNITS';
7+
8+
const getCalleeData = async function (
9+
network: string,
10+
collateral: CollateralConfig,
11+
marketId: string,
12+
profitAddress: string,
13+
params?: GetCalleeDataParams
14+
): Promise<string> {
15+
const marketData = collateral.exchanges[marketId];
16+
if (marketData?.callee !== 'OneInchCallee') {
17+
throw new Error(`getCalleeData called with invalid collateral type "${collateral.ilk}"`);
18+
}
19+
const oneInchParams = !!params && 'oneInchParams' in params ? params.oneInchParams : undefined;
20+
if (!oneInchParams) {
21+
throw new Error(`getCalleeData called with invalid txData`);
22+
}
23+
const joinAdapterAddress = await getContractAddressByName(network, getJoinNameByCollateralType(collateral.ilk));
24+
const minProfit = 1;
25+
const typesArray = ['address', 'address', 'uint256', 'address', 'address', 'bytes'];
26+
return ethers.utils.defaultAbiCoder.encode(typesArray, [
27+
profitAddress,
28+
joinAdapterAddress,
29+
minProfit,
30+
ethers.constants.AddressZero,
31+
oneInchParams.to,
32+
ethers.utils.hexDataSlice(oneInchParams.txData, 4),
33+
]);
34+
};
35+
36+
const getMarketPrice = async function (
37+
network: string,
38+
collateral: CollateralConfig,
39+
marketId: string,
40+
collateralAmount: BigNumber
41+
): Promise<{ price: BigNumber; pools: undefined }> {
42+
// convert collateral into DAI
43+
const collateralIntegerAmount = collateralAmount.shiftedBy(collateral.decimals).toFixed(0);
44+
const { toTokenAmount } = await getOneinchSwapParameters(
45+
network,
46+
collateral.symbol,
47+
collateralIntegerAmount,
48+
marketId
49+
);
50+
51+
// return price per unit
52+
return {
53+
price: new BigNumber(toTokenAmount).shiftedBy(-DAI_NUMBER_OF_DIGITS).dividedBy(collateralAmount),
54+
pools: undefined,
55+
};
56+
};
57+
58+
const UniswapV2CalleeDai: CalleeFunctions = {
59+
getCalleeData,
60+
getMarketPrice,
61+
};
62+
63+
export default UniswapV2CalleeDai;

core/src/calleeFunctions/UniswapV3Callee.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
1+
import type { CalleeFunctions, CollateralConfig, GetCalleeDataParams, Pool } from '../types';
22
import { ethers } from 'ethers';
33
import BigNumber from '../bignumber';
44
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
@@ -12,12 +12,13 @@ const getCalleeData = async function (
1212
collateral: CollateralConfig,
1313
marketId: string,
1414
profitAddress: string,
15-
preloadedPools?: Pool[]
15+
params?: GetCalleeDataParams
1616
): Promise<string> {
1717
const marketData = collateral.exchanges[marketId];
1818
if (marketData?.callee !== 'UniswapV3Callee') {
1919
throw new Error(`getCalleeData called with invalid collateral type "${collateral.ilk}"`);
2020
}
21+
const preloadedPools = !!params && 'pools' in params ? params.pools : undefined;
2122
const pools = preloadedPools || (await getPools(network, collateral, marketId));
2223
if (!pools) {
2324
throw new Error(`getCalleeData called with invalid pools`);
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { ethers } from 'ethers';
2+
import { getCalleeAddressByCollateralType } from '../../constants/CALLEES';
3+
import { getCollateralConfigBySymbol } from '../../constants/COLLATERALS';
4+
import { getErc20SymbolByAddress } from '../../contracts';
5+
import { getDecimalChainIdByNetworkType, getNetworkConfigByType } from '../../network';
6+
import { CollateralConfig } from '../../types';
7+
import BigNumber from '../../bignumber';
8+
import { getTokenAddressByNetworkAndSymbol } from '../../tokens';
9+
import { Queue } from 'async-await-queue';
10+
import memoizee from 'memoizee';
11+
import { convertETHtoDAI } from '../../fees';
12+
13+
const MAX_DELAY_BETWEEN_REQUESTS_MS = 600;
14+
const REQUEST_QUEUE = new Queue(1, MAX_DELAY_BETWEEN_REQUESTS_MS);
15+
const EXPECTED_SIGNATURE = '0x12aa3caf'; // see https://www.4byte.directory/signatures/?bytes4_signature=0x12aa3caf
16+
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
17+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
18+
19+
export const getOneInchUrl = (chainId: number) => {
20+
return `https://api.1inch.io/v5.0/${chainId}`;
21+
};
22+
23+
interface Protocol {
24+
id: string;
25+
title: string;
26+
img: string;
27+
img_color: string;
28+
}
29+
30+
interface OneInchToken {
31+
symbol: string;
32+
name: string;
33+
address: string;
34+
decimals: 0;
35+
logoURI: string;
36+
}
37+
interface OneInchSwapRepsonse {
38+
fromToken: OneInchToken;
39+
toToken: OneInchToken;
40+
toTokenAmount: string;
41+
fromTokenAmount: string;
42+
protocols: OneInchSwapRoute[];
43+
tx: {
44+
from: string;
45+
to: string;
46+
data: string;
47+
value: string;
48+
gasPrice: string;
49+
gas: string;
50+
};
51+
}
52+
interface LiquiditySourcesResponse {
53+
protocols: Protocol[];
54+
}
55+
type OneInchSwapRoute = { name: string; part: number; fromTokenAddress: string; toTokenAddress: string }[][];
56+
57+
const executeRequestInQueue = async (url: string) => {
58+
const apiRequestSymbol = Symbol();
59+
await REQUEST_QUEUE.wait(apiRequestSymbol);
60+
const response = await fetch(url).then(res => res.json());
61+
REQUEST_QUEUE.end(apiRequestSymbol);
62+
return response;
63+
};
64+
65+
export const executeOneInchApiRequest = async (
66+
chainId: number,
67+
endpoint: '/swap' | '/liquidity-sources',
68+
params?: Record<string, any>
69+
) => {
70+
const oneInchUrl = getOneInchUrl(chainId);
71+
const url = `${oneInchUrl}${endpoint}?${new URLSearchParams(params)}`;
72+
const response = await executeRequestInQueue(url);
73+
if (response.error) {
74+
throw new Error(`failed to receive response from oneinch: ${response.error}`);
75+
}
76+
return response;
77+
};
78+
79+
async function _getOneinchValidProtocols(chainId: number) {
80+
// Fetch all supported protocols except for the limit orders
81+
const response: LiquiditySourcesResponse = await executeOneInchApiRequest(chainId, '/liquidity-sources');
82+
const protocolIds = response.protocols.map(protocol => protocol.id);
83+
return protocolIds.filter(protocolId => !protocolId.toLowerCase().includes('limit'));
84+
}
85+
86+
export const getOneinchValidProtocols = memoizee(_getOneinchValidProtocols, {
87+
promise: true,
88+
length: 1,
89+
maxAge: ONE_DAY_MS,
90+
});
91+
92+
export async function getOneinchSwapParameters(
93+
network: string,
94+
collateralSymbol: string,
95+
amount: string,
96+
marketId: string,
97+
slippage = '1'
98+
): Promise<OneInchSwapRepsonse> {
99+
const isFork = getNetworkConfigByType(network).isFork;
100+
const chainId = isFork ? 1 : getDecimalChainIdByNetworkType(network);
101+
if (!isFork && !SUPPORTED_1INCH_NETWORK_IDS.includes(chainId)) {
102+
throw new Error(`1inch does not support network ${network}`);
103+
}
104+
const toTokenAddress = await getTokenAddressByNetworkAndSymbol(network, 'DAI');
105+
const fromTokenAddress = await getTokenAddressByNetworkAndSymbol(network, collateralSymbol);
106+
const calleeAddress = getCalleeAddressByCollateralType(
107+
network,
108+
getCollateralConfigBySymbol(collateralSymbol).ilk,
109+
marketId
110+
);
111+
// Documentation https://docs.1inch.io/docs/aggregation-protocol/api/swap-params/
112+
const swapParams = {
113+
fromTokenAddress,
114+
toTokenAddress,
115+
fromAddress: calleeAddress,
116+
amount,
117+
slippage,
118+
allowPartialFill: false, // disable partial fill
119+
disableEstimate: true, // disable eth_estimateGas
120+
compatibilityMode: true, // always receive parameters for the `swap` call
121+
};
122+
const oneinchResponse = await executeOneInchApiRequest(chainId, '/swap', swapParams);
123+
const functionSignature = ethers.utils.hexDataSlice(oneinchResponse.tx.data, 0, 4); // see https://docs.soliditylang.org/en/develop/abi-spec.html#function-selector
124+
if (functionSignature !== EXPECTED_SIGNATURE) {
125+
throw new Error(`Unexpected 1inch function signature: ${functionSignature}, expected: ${EXPECTED_SIGNATURE}`);
126+
}
127+
return oneinchResponse;
128+
}
129+
130+
export async function extractPathFromSwapResponseProtocols(
131+
network: string,
132+
oneInchRoutes: OneInchSwapRoute[]
133+
): Promise<string[]> {
134+
const pathStepsResolves = await Promise.all(
135+
oneInchRoutes[0].map(async route => {
136+
return await Promise.all([
137+
await getErc20SymbolByAddress(network, route[0].fromTokenAddress),
138+
await getErc20SymbolByAddress(network, route[0].toTokenAddress),
139+
]);
140+
})
141+
);
142+
const path = [pathStepsResolves[0][0]];
143+
for (const step of pathStepsResolves) {
144+
path.push(step[1]);
145+
}
146+
return path;
147+
}
148+
149+
export async function getOneInchMarketData(
150+
network: string,
151+
collateral: CollateralConfig,
152+
amount: BigNumber,
153+
marketId: string
154+
) {
155+
const swapData = await getOneinchSwapParameters(
156+
network,
157+
collateral.symbol,
158+
amount.shiftedBy(collateral.decimals).toFixed(0),
159+
marketId
160+
);
161+
const path = await extractPathFromSwapResponseProtocols(network, swapData.protocols);
162+
const calleeData = swapData.tx.data;
163+
const estimatedGas = swapData.tx.gas;
164+
const exchangeFeeEth = new BigNumber(swapData.tx.gasPrice).multipliedBy(estimatedGas);
165+
const exchangeFeeDai = await convertETHtoDAI(network, exchangeFeeEth);
166+
const to = swapData.tx.to;
167+
return {
168+
path,
169+
exchangeFeeEth,
170+
exchangeFeeDai,
171+
calleeData,
172+
to,
173+
};
174+
}

core/src/calleeFunctions/helpers/uniswapV3.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { DAI_NUMBER_OF_DIGITS, MKR_NUMBER_OF_DIGITS } from '../../constants/UNIT
66
import { getCollateralConfigBySymbol } from '../../constants/COLLATERALS';
77
import { getTokenAddressByNetworkAndSymbol } from '../../tokens';
88
import { Pool } from '../../types';
9+
import memoizee from 'memoizee';
10+
import { MARKET_DATA_RECORDS_CACHE_MS } from '..';
911

1012
const UNISWAP_V3_QUOTER_ADDRESS = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6';
1113
export const UNISWAP_FEE = 3000; // denominated in hundredths of a bip
@@ -54,7 +56,7 @@ export const convertCollateralToDaiUsingPool = async function (
5456
return daiAmount;
5557
};
5658

57-
export const convertSymbolToDai = async function (
59+
const _convertSymbolToDai = async function (
5860
network: string,
5961
symbol: string,
6062
amount: BigNumber,
@@ -73,6 +75,15 @@ export const convertSymbolToDai = async function (
7375
return daiAmount;
7476
};
7577

78+
export const convertSymbolToDai = memoizee(_convertSymbolToDai, {
79+
promise: true,
80+
length: 4,
81+
maxAge: MARKET_DATA_RECORDS_CACHE_MS,
82+
normalizer: (args: any[]) => {
83+
return JSON.stringify(args); // use normalizer due to BigNumber object being an argument
84+
},
85+
});
86+
7687
export const convertDaiToSymbol = async function (
7788
network: string,
7889
symbol: string,

0 commit comments

Comments
 (0)