diff --git a/frontend/src/constants.tsx b/frontend/src/constants.tsx index fe15e34a..d47c3c86 100644 --- a/frontend/src/constants.tsx +++ b/frontend/src/constants.tsx @@ -42,6 +42,10 @@ export enum TxState { // Staking tokens export enum StakingToken { UNISWAP_V2, + BALANCER_V1, + BALANCER_SMART_POOL_V1, + BALANCER_WEIGHTED_POOL_V2, + // for testing MOCK, WAMPL, diff --git a/frontend/src/utils/price.ts b/frontend/src/utils/price.ts index c89c2e5a..660f4b08 100644 --- a/frontend/src/utils/price.ts +++ b/frontend/src/utils/price.ts @@ -10,7 +10,7 @@ const DEFAULT_PRICES: Record = { USDbC: 1, } -const SYMBOL_TO_QUERY: Record = { +export const SYMBOL_TO_QUERY: Record = { WETH: 'ethereum', USDC: 'usd-coin', diff --git a/frontend/src/utils/stakingToken.ts b/frontend/src/utils/stakingToken.ts index 057ef54c..2b7039ed 100644 --- a/frontend/src/utils/stakingToken.ts +++ b/frontend/src/utils/stakingToken.ts @@ -1,14 +1,18 @@ -import { BigNumber, Contract } from 'ethers' +import { BigNumber, Contract, constants } from 'ethers' import { formatUnits } from 'ethers/lib/utils' import { toChecksumAddress } from 'web3-utils' import { StakingToken } from '../constants' import { WRAPPED_ERC20_ABI } from './abis/WrappedERC20' import { AAVEV2_DEPOSIT_TOKEN } from './abis/AaveV2DepositToken' -import { getCurrentPrice } from './price' +import { getCurrentPrice, SYMBOL_TO_QUERY } from './price' import { defaultTokenInfo, getTokenInfo } from './token' import { UNISWAP_V2_PAIR_ABI } from './abis/UniswapV2Pair' import { SignerOrProvider, StakingTokenInfo, TokenComposition } from '../types' import { ERC20Balance } from '../sdk' +import { BALANCER_BPOOL_V1_ABI } from './abis/BalancerBPoolV1' +import { BALANCER_CRP_V1_ABI } from './abis/BalancerCRPV1' +import { BALANCER_WEIGHTED_POOL_V2_ABI } from './abis/BalancerWeightedPoolV2' +import { BALANCER_VAULT_V2_ABI } from './abis/BalancerVaultV2' export const defaultStakingTokenInfo = (): StakingTokenInfo => ({ ...defaultTokenInfo(), @@ -25,6 +29,12 @@ export const getStakingTokenInfo = async ( switch (token) { case StakingToken.UNISWAP_V2: return getUniswapV2(tokenAddress, signerOrProvider) + case StakingToken.BALANCER_V1: + return getBalancerV1(tokenAddress, signerOrProvider) + case StakingToken.BALANCER_SMART_POOL_V1: + return getBalancerSmartPoolV1(tokenAddress, signerOrProvider) + case StakingToken.BALANCER_WEIGHTED_POOL_V2: + return getBalancerWeightedPoolV2(tokenAddress, signerOrProvider) case StakingToken.MOCK: return getMockLPToken(tokenAddress) case StakingToken.WAMPL: @@ -47,7 +57,7 @@ const getTokenCompositions = async ( ): Promise => { const compositions = tokenAddresses.map(async (tokenAddress, index) => { const { name, symbol, decimals } = await getTokenInfo(tokenAddress, signerOrProvider) - const price = await getCurrentPrice(symbol) + const price = await getTokenPrice(symbol, tokenAddress, signerOrProvider) const balance = await ERC20Balance(tokenAddress, poolAddress, signerOrProvider) const balanceNumber = parseInt(formatUnits(balance as BigNumber, decimals), 10) return { @@ -188,3 +198,153 @@ const getBasicToken = async (tokenAddress: string, signerOrProvider: SignerOrPro wrappedToken: null, } } + +const getBalancerTokenCompositions = async ( + address: string, + signerOrProvider: SignerOrProvider, +): Promise => { + const contract = new Contract(address, BALANCER_BPOOL_V1_ABI, signerOrProvider) + const tokenAddresses: string[] = await contract.getCurrentTokens() + const totalDenormalizedWeight: number = await contract.getTotalDenormalizedWeight() + const tokenDenormalizedWeights: number[] = await Promise.all( + tokenAddresses.map((token) => contract.getDenormalizedWeight(token)), + ) + const tokenWeights = tokenDenormalizedWeights.map((w) => w / totalDenormalizedWeight) + + return getTokenCompositions(tokenAddresses, contract.address, signerOrProvider, tokenWeights) +} + +const getBalancerV1 = async (tokenAddress: string, signerOrProvider: SignerOrProvider): Promise => { + const address = toChecksumAddress(tokenAddress) + const contract = new Contract(address, BALANCER_BPOOL_V1_ABI, signerOrProvider) + + const { name, symbol, decimals } = await getTokenInfo(address, signerOrProvider) + + const totalSupply: BigNumber = await contract.totalSupply() + const totalSupplyNumber = parseFloat(formatUnits(totalSupply, decimals)) + + const tokenCompositions = await getBalancerTokenCompositions(address, signerOrProvider) + const marketCap = getMarketCap(tokenCompositions) + + return { + address, + decimals, + name, + symbol, + price: marketCap / totalSupplyNumber, + composition: tokenCompositions, + wrappedToken: null, + } +} + +const getBalancerSmartPoolV1 = async ( + tokenAddress: string, + signerOrProvider: SignerOrProvider, +): Promise => { + const address = toChecksumAddress(tokenAddress) + const contract = new Contract(address, BALANCER_CRP_V1_ABI, signerOrProvider) + + const bPool: string = await contract.bPool() + const { name, symbol, decimals } = await getTokenInfo(address, signerOrProvider) + + const totalSupply: BigNumber = await contract.totalSupply() + const totalSupplyNumber = parseFloat(formatUnits(totalSupply, decimals)) + + const tokenCompositions = await getBalancerTokenCompositions(bPool, signerOrProvider) + const marketCap = getMarketCap(tokenCompositions) + + return { + address, + decimals, + name, + symbol, + // totalSupply: totalSupplyNumber, + // marketCap, + price: marketCap / totalSupplyNumber, + composition: tokenCompositions, + wrappedToken: null, + } +} + +const getBalancerWeightedPoolV2 = async ( + tokenAddress: string, + signerOrProvider: SignerOrProvider, +): Promise => { + const address = toChecksumAddress(tokenAddress) + const contract = new Contract(address, BALANCER_WEIGHTED_POOL_V2_ABI, signerOrProvider) + + const { name, symbol, decimals } = await getTokenInfo(address, signerOrProvider) + + const totalSupply: BigNumber = await contract.totalSupply() + const totalSupplyNumber = parseFloat(formatUnits(totalSupply, decimals)) + + const tokenCompositions = await getBalancerV2TokenCompositions(address, signerOrProvider) + const marketCap = getMarketCap(tokenCompositions) + + return { + address, + decimals, + name, + symbol, + price: marketCap / totalSupplyNumber, + composition: tokenCompositions, + wrappedToken: null, + } +} + +const getBalancerV2TokenCompositions = async ( + address: string, + signerOrProvider: SignerOrProvider, +): Promise => { + const contract = new Contract(address, BALANCER_WEIGHTED_POOL_V2_ABI, signerOrProvider) + const vault = new Contract(await contract.getVault(), BALANCER_VAULT_V2_ABI, signerOrProvider) + + const r = await vault.getPoolTokens(await contract.getPoolId()) + const tokenAddresses: string[] = r[0] + const tokenBalances: string[] = r[1] + + const totalNormalizedWeight: BigNumber = BigNumber.from(constants.WeiPerEther) + const tokenNormalizedWeights: BigNumber[] = await contract.getNormalizedWeights() + const tokenWeights = tokenNormalizedWeights.map((w) => w.div(totalNormalizedWeight).toNumber()) + + return getTokenCompositionsWithBalances(tokenAddresses, tokenBalances, signerOrProvider, tokenWeights) +} + +const getTokenCompositionsWithBalances = async ( + tokenAddresses: string[], + balances: string[], + signerOrProvider: SignerOrProvider, + weights: number[], +): Promise => { + const compositions = tokenAddresses.map(async (tokenAddress, index) => { + const { name, symbol, decimals } = await getTokenInfo(tokenAddress, signerOrProvider) + const price = await getTokenPrice(symbol, tokenAddress, signerOrProvider) + const balanceNumber = parseInt(formatUnits(BigNumber.from(balances[index]), decimals), 10) + return { + address: tokenAddress, + name, + symbol, + balance: balanceNumber, + decimals, + value: price * balanceNumber, + weight: weights[index], + } + }) + return Promise.all(compositions) +} + +const getTokenPrice = async (symbol: string, tokenAddress: string, signerOrProvider: SignerOrProvider) => { + if (SYMBOL_TO_QUERY[symbol]) { + return getCurrentPrice(symbol) + } + + // If it is sToken, get the underlying token price + const unwrappedSymbol = symbol.substring(1) + if (SYMBOL_TO_QUERY[unwrappedSymbol]) { + return getCurrentPrice(unwrappedSymbol) + } + + // If it is not basic token or sToken then it is a LP token + const { price } = await uniswapV2Pair(tokenAddress, signerOrProvider, 'UniswapV2', 'UNI') + return price +}