Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

feat(uniswap-v2): Derive unknown tokens prices #466

Merged
merged 3 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 17 additions & 7 deletions src/apps/uniswap-v2/ethereum/uniswap-v2.pool.token-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppTokenPosition } from '~position/position.interface';
import { Network } from '~types/network.interface';

import { UniswapFactory, UniswapPair, UniswapV2ContractFactory } from '../contracts';
import { UniswapV2OnChainTokenDerivationStrategy } from '../helpers/uniswap-v2.on-chain.token-derivation-strategy';
import { UniswapV2PoolTokenHelper } from '../helpers/uniswap-v2.pool.token-helper';
import { UniswapV2TheGraphPoolTokenAddressStrategy } from '../helpers/uniswap-v2.the-graph.pool-token-address-strategy';
import { UniswapV2TheGraphPoolVolumeStrategy } from '../helpers/uniswap-v2.the-graph.pool-volume-strategy';
Expand All @@ -22,6 +23,8 @@ export class EthereumUniswapV2PoolTokenFetcher implements PositionFetcher<AppTok
private readonly uniswapV2ContractFactory: UniswapV2ContractFactory,
@Inject(UniswapV2PoolTokenHelper)
private readonly uniswapV2PoolTokenHelper: UniswapV2PoolTokenHelper,
@Inject(UniswapV2OnChainTokenDerivationStrategy)
private readonly uniswapV2OnChainTokenDerivationStrategy: UniswapV2OnChainTokenDerivationStrategy,
@Inject(UniswapV2TheGraphPoolTokenAddressStrategy)
private readonly uniswapV2TheGraphPoolTokenAddressStrategy: UniswapV2TheGraphPoolTokenAddressStrategy,
@Inject(UniswapV2TheGraphPoolVolumeStrategy)
Expand All @@ -36,19 +39,14 @@ export class EthereumUniswapV2PoolTokenFetcher implements PositionFetcher<AppTok
factoryAddress: '0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f',
hiddenTokens: ['0x62359ed7505efc61ff1d56fef82158ccaffa23d7', '0x35bd01fc9d6d5d81ca9e055db88dc49aa2c699a8'],
blockedPools: ['0x9cbfb60a09a9a33a10312da0f39977cbdb7fde23'], // Uniswap V2: SAITAMA - has a transfer fee (not supported by our zap)
appTokenDependencies: [
{
appId: 'alpha-v1',
groupIds: ['lending'],
network,
},
],
appTokenDependencies: [{ appId: 'alpha-v1', groupIds: ['lending'], network }],
resolveFactoryContract: ({ address, network }) =>
this.uniswapV2ContractFactory.uniswapFactory({ address, network }),
resolvePoolContract: ({ address, network }) => this.uniswapV2ContractFactory.uniswapPair({ address, network }),
resolvePoolTokenAddresses: this.uniswapV2TheGraphPoolTokenAddressStrategy.build({
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2',
first: 1000,
orderBy: 'trackedReserveETH',
requiredPools: [
'0xaad22f5543fcdaa694b68f94be177b561836ae57', // sUSD-$BASED
'0xe98f89a2b3aecdbe2118202826478eb02434459a', // DAI-DEBASE
Expand All @@ -75,6 +73,18 @@ export class EthereumUniswapV2PoolTokenFetcher implements PositionFetcher<AppTok
'0x735659c8576d88a2eb5c810415ea51cb06931696', // mAAPL/UST
],
}),
resolveDerivedUnderlyingToken: this.uniswapV2OnChainTokenDerivationStrategy.build({
priceDerivationWhitelist: [
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'0x853d955acef822db058eb8505911ed77f175b99e', // FRAX
'0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', // WBTC
],
resolvePoolAddress: ({ factoryContract, multicall, token0, token1 }) =>
multicall.wrap(factoryContract).getPair(token0, token1),
}),
resolvePoolTokenSymbol: ({ multicall, poolContract }) => multicall.wrap(poolContract).symbol(),
resolvePoolTokenSupply: ({ multicall, poolContract }) => multicall.wrap(poolContract).totalSupply(),
resolvePoolReserves: async ({ multicall, poolContract }) =>
Expand Down
15 changes: 9 additions & 6 deletions src/apps/uniswap-v2/ethereum/uniswap-v2.tvl-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { Inject } from '@nestjs/common';
import { sumBy } from 'lodash';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { Register } from '~app-toolkit/decorators';
import { TvlFetcher } from '~stats/tvl/tvl-fetcher.interface';
import { Network } from '~types/network.interface';

import { UniswapV2TheGraphTvlHelper } from '../helpers/uniswap-v2.the-graph.tvl-helper';
import UNISWAP_V2_DEFINITION from '../uniswap-v2.definition';

const appId = UNISWAP_V2_DEFINITION.id;
const network = Network.ETHEREUM_MAINNET;

@Register.TvlFetcher({ appId, network })
export class EthereumUniswapV2TvlFetcher implements TvlFetcher {
constructor(
@Inject(UniswapV2TheGraphTvlHelper) private readonly uniswapV2TheGraphTvlHelper: UniswapV2TheGraphTvlHelper,
) {}
constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {}

async getTvl() {
return this.uniswapV2TheGraphTvlHelper.getTvl({
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswapv2',
const positions = await this.appToolkit.getAppTokenPositions({
appId,
groupIds: ['pool'],
network,
});

return sumBy(positions, v => v.dataProps.liquidity as number);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Inject, Injectable } from '@nestjs/common';
import { isNull } from 'lodash';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { ZERO_ADDRESS } from '~app-toolkit/constants/address';
import { EthersMulticall as Multicall } from '~multicall/multicall.ethers';
import { ContractType } from '~position/contract.interface';
import { BaseToken } from '~position/token.interface';

import { UniswapFactory, UniswapPair } from '../contracts';

import { UniswapV2PoolTokenHelperParams } from './uniswap-v2.pool.token-helper';

type GetDerivedPriceParams<T = UniswapFactory> = {
priceDerivationWhitelist: string[];
resolvePoolAddress: (opts: {
multicall: Multicall;
factoryContract: T;
token0: string;
token1: string;
}) => Promise<string>;
};

@Injectable()
export class UniswapV2OnChainTokenDerivationStrategy {
constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {}

build<T = UniswapFactory, V = UniswapPair>({
priceDerivationWhitelist,
resolvePoolAddress,
}: GetDerivedPriceParams<T>): UniswapV2PoolTokenHelperParams<T, V>['resolveDerivedUnderlyingToken'] {
return async ({
factoryAddress,
network,
tokenAddress,
baseTokensByAddress,
resolvePoolUnderlyingTokenAddresses,
resolveFactoryContract,
resolvePoolContract,
resolvePoolReserves,
}) => {
const multicall = this.appToolkit.getMulticall(network);
const factoryContract = resolveFactoryContract({ address: factoryAddress, network });

const contract = this.appToolkit.globalContracts.erc20({ address: tokenAddress, network });
const [symbol, decimals] = await Promise.all([
multicall
.wrap(contract)
.symbol()
.catch(() => null),
multicall
.wrap(contract)
.decimals()
.catch(() => null),
]);

// For non-ERC20 tokens
if (isNull(symbol) || isNull(decimals)) {
return null;
}

const baseToken: BaseToken = {
type: ContractType.BASE_TOKEN,
address: tokenAddress,
network: network,
price: 0,
symbol: symbol,
decimals: decimals,
};

for (let i = 0; i < priceDerivationWhitelist.length; i++) {
const knownTokenAddress = priceDerivationWhitelist[i];
const poolAddress = await resolvePoolAddress({
factoryContract,
multicall,
token0: tokenAddress,
token1: knownTokenAddress,
});

if (poolAddress === ZERO_ADDRESS) continue;
const knownToken = baseTokensByAddress[knownTokenAddress];
const poolContract = resolvePoolContract({ address: poolAddress, network });
const tokensRaw = await resolvePoolUnderlyingTokenAddresses({ multicall, poolContract });
const reserves = await resolvePoolReserves({ multicall, poolContract });
const tokens = tokensRaw.map(t => t.toLowerCase());

const unknownIndex = tokens.findIndex(t => t === tokenAddress);
const knownIndex = 1 - unknownIndex;

const knownReserve = Number(reserves[knownIndex]) / 10 ** knownToken.decimals;
const unknownReserve = Number(reserves[unknownIndex]) / 10 ** decimals;
const knownLiquidity = knownToken.price * knownReserve;
if (knownLiquidity < 1) continue; // Minimum liquidity check

const price = knownLiquidity / unknownReserve;
return { ...baseToken, price };
}

return baseToken;
};
}
}
55 changes: 44 additions & 11 deletions src/apps/uniswap-v2/helpers/uniswap-v2.pool.token-helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Inject } from '@nestjs/common';
import BigNumber from 'bignumber.js';
import { BigNumberish } from 'ethers';
import _ from 'lodash';
import _, { chunk, compact } from 'lodash';
import { keyBy, sortBy } from 'lodash';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
Expand All @@ -13,6 +14,7 @@ import { EthersMulticall as Multicall } from '~multicall/multicall.ethers';
import { ContractType } from '~position/contract.interface';
import { AppTokenPosition, Token } from '~position/position.interface';
import { AppGroupsDefinition } from '~position/position.service';
import { BaseToken } from '~position/token.interface';
import { Network } from '~types/network.interface';

import { UniswapFactory, UniswapPair } from '../contracts';
Expand All @@ -39,6 +41,7 @@ export type UniswapV2PoolTokenHelperParams<T = UniswapFactory, V = UniswapPair>
hiddenTokens?: string[];
blockedPools?: string[];
appTokenDependencies?: AppGroupsDefinition[];
priceDerivationWhitelist?: string[];
resolveFactoryContract(opts: { address: string; network: Network }): T;
resolvePoolContract(opts: { address: string; network: Network }): V;
resolvePoolTokenAddresses: (opts: {
Expand All @@ -48,6 +51,17 @@ export type UniswapV2PoolTokenHelperParams<T = UniswapFactory, V = UniswapPair>
resolveFactoryContract(opts: { address: string; network: Network }): T;
resolvePoolContract(opts: { address: string; network: Network }): V;
}) => Promise<ResolvePoolTokenAddressesResponse>;
resolveDerivedUnderlyingToken?(opts: {
appId: string;
network: Network;
factoryAddress: string;
tokenAddress: string;
baseTokensByAddress: Record<string, BaseToken>;
resolveFactoryContract(opts: { address: string; network: Network }): T;
resolvePoolContract(opts: { address: string; network: Network }): V;
resolvePoolUnderlyingTokenAddresses(opts: { multicall: Multicall; poolContract: V }): Promise<[string, string]>;
resolvePoolReserves(opts: { multicall: Multicall; poolContract: V }): Promise<[BigNumberish, BigNumberish]>;
}): Promise<BaseToken | null>;
resolvePoolVolumes?: (opts: {
appId: string;
network: Network;
Expand Down Expand Up @@ -77,6 +91,7 @@ export class UniswapV2PoolTokenHelper {
appTokenDependencies = [],
resolveFactoryContract,
resolvePoolContract,
resolveDerivedUnderlyingToken,
resolvePoolTokenAddresses,
resolvePoolTokenSymbol,
resolvePoolTokenSupply,
Expand Down Expand Up @@ -109,7 +124,7 @@ export class UniswapV2PoolTokenHelper {
resolvePoolContract,
}).catch(() => []);

const poolStatsRaw = await Promise.all(
const poolTokens = await Promise.all(
poolAddresses.map(async address => {
const type = ContractType.APP_TOKEN;
const poolContract = resolvePoolContract({ address, network });
Expand All @@ -122,9 +137,28 @@ export class UniswapV2PoolTokenHelper {
const token1Address = token1AddressRaw.toLowerCase();
if (hiddenTokens.includes(token0Address) || hiddenTokens.includes(token1Address)) return null;

const token0 = appTokensByAddress[token0Address] ?? baseTokensByAddress[token0Address];
const token1 = appTokensByAddress[token1Address] ?? baseTokensByAddress[token1Address];
if (!token0 || !token1) return null;
const resolvedTokens = await Promise.all(
[token0Address, token1Address].map(async tokenAddress => {
const underlyingToken = appTokensByAddress[tokenAddress] ?? baseTokensByAddress[tokenAddress];
if (underlyingToken) return underlyingToken;
if (!resolveDerivedUnderlyingToken) return null;

return resolveDerivedUnderlyingToken({
appId,
baseTokensByAddress,
factoryAddress,
network,
resolveFactoryContract,
resolvePoolContract,
resolvePoolReserves,
resolvePoolUnderlyingTokenAddresses,
tokenAddress,
});
}),
);

const tokens = compact(resolvedTokens);
if (tokens.length !== resolvedTokens.length) return null;

// Retrieve pool reserves and pool token supply
const [symbol, supplyRaw, reservesRaw] = await Promise.all([
Expand All @@ -135,8 +169,8 @@ export class UniswapV2PoolTokenHelper {

// Data Props
const decimals = 18;
const tokens = [token0, token1];
const reserves = reservesRaw.map((r, i) => Number(r) / 10 ** tokens[i].decimals);
const reservesBN = reservesRaw.map((r, i) => new BigNumber(r.toString()).div(10 ** tokens[i].decimals));
const reserves = reservesBN.map(v => v.toNumber());
const liquidity = tokens[0].price * reserves[0] + tokens[1].price * reserves[1];
const reservePercentages = tokens.map((t, i) => reserves[i] * (t.price / liquidity));
const supply = Number(supplyRaw) / 10 ** decimals;
Expand All @@ -148,13 +182,14 @@ export class UniswapV2PoolTokenHelper {

// Display Props
const prefix = resolveTokenDisplayPrefix(symbol);
const label = `${prefix} ${resolveTokenDisplaySymbol(token0)} / ${resolveTokenDisplaySymbol(token1)}`;
const label = `${prefix} ${resolveTokenDisplaySymbol(tokens[0])} / ${resolveTokenDisplaySymbol(tokens[1])}`;
const secondaryLabel = reservePercentages.map(p => `${Math.round(p * 100)}%`).join(' / ');
const images = tokens.map(v => getImagesFromToken(v)).flat();
const statsItems = [
{ label: 'Liquidity', value: buildDollarDisplayItem(liquidity) },
{ label: 'Volume', value: buildDollarDisplayItem(volume) },
{ label: 'Fee', value: buildPercentageDisplayItem(fee) },
{ label: 'Reserves', value: reserves.map(v => (v < 0.01 ? '<0.01' : v.toFixed(2))).join(' / ') },
];

const poolToken: AppTokenPosition<UniswapV2PoolTokenDataProps> = {
Expand Down Expand Up @@ -190,10 +225,8 @@ export class UniswapV2PoolTokenHelper {
}),
);

const poolStats = _.compact(poolStatsRaw);

return sortBy(
poolStats.filter(t => !!t && t.dataProps.liquidity > minLiquidity),
compact(poolTokens).filter(t => t.dataProps.liquidity > minLiquidity),
t => -t!.dataProps.liquidity,
);
}
Expand Down
Loading