From ca37d90d70666403a3ac8e17ed6c71ce89c72bd3 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Thu, 31 Aug 2023 13:01:47 -0400 Subject: [PATCH] Adds proper USD pricing conversion for token swap (#1788) * Fix pricing * Cleanup * Use oracles for price * Add contract * contract guard * Add in oracle router into cp artifacts * Allow failure * Update to latest oeth router oracle deployment * move to react query impl * Remove default and changed cache time --- contracts/package.json | 2 +- dapp-oeth/network.mainnet.json | 30 +--- .../components/buySell/SwapCurrencyPill.js | 10 +- .../src/components/buySell/SwapHomepage.js | 8 +- dapp-oeth/src/constants/contractAddresses.js | 3 + dapp-oeth/src/hooks/useSwapEstimator.js | 13 +- dapp-oeth/src/hooks/useTokenPrices.js | 162 ++++++++++++++++++ dapp-oeth/src/utils/contracts.js | 9 + 8 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 dapp-oeth/src/hooks/useTokenPrices.js diff --git a/contracts/package.json b/contracts/package.json index 7be99c4808..6662c439d0 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -24,7 +24,7 @@ "test:fork:w_trace": "./fork-test.sh --trace", "fund": "FORK=true npx hardhat fund --network localhost", "copy-interface-artifacts": "mkdir -p ../dapp/abis && cp artifacts/contracts/interfaces/IVault.sol/IVault.json ../dapp/abis/IVault.json && cp artifacts/contracts/liquidity/LiquidityReward.sol/LiquidityReward.json ../dapp/abis/LiquidityReward.json && cp artifacts/contracts/interfaces/uniswap/IUniswapV2Pair.sol/IUniswapV2Pair.json ../dapp/abis/IUniswapV2Pair.json && cp artifacts/contracts/staking/SingleAssetStaking.sol/SingleAssetStaking.json ../dapp/abis/SingleAssetStaking.json && cp artifacts/contracts/compensation/CompensationClaims.sol/CompensationClaims.json ../dapp/abis/CompensationClaims.json && cp artifacts/contracts/flipper/Flipper.sol/Flipper.json ../dapp/abis/Flipper.json", - "copy-interface-artifacts:oeth": "mkdir -p ../dapp-oeth/abis && cp artifacts/contracts/interfaces/IVault.sol/IVault.json ../dapp-oeth/abis/IVault.json && cp artifacts/contracts/liquidity/LiquidityReward.sol/LiquidityReward.json ../dapp-oeth/abis/LiquidityReward.json && cp artifacts/contracts/interfaces/uniswap/IUniswapV2Pair.sol/IUniswapV2Pair.json ../dapp-oeth/abis/IUniswapV2Pair.json && cp artifacts/contracts/staking/SingleAssetStaking.sol/SingleAssetStaking.json ../dapp-oeth/abis/SingleAssetStaking.json && cp artifacts/contracts/compensation/CompensationClaims.sol/CompensationClaims.json ../dapp-oeth/abis/CompensationClaims.json && cp artifacts/contracts/vault/OETHZapper.sol/OETHZapper.json ../dapp-oeth/abis/OETHZapper.json", + "copy-interface-artifacts:oeth": "mkdir -p ../dapp-oeth/abis && cp artifacts/contracts/interfaces/IVault.sol/IVault.json ../dapp-oeth/abis/IVault.json && cp artifacts/contracts/liquidity/LiquidityReward.sol/LiquidityReward.json ../dapp-oeth/abis/LiquidityReward.json && cp artifacts/contracts/interfaces/uniswap/IUniswapV2Pair.sol/IUniswapV2Pair.json ../dapp-oeth/abis/IUniswapV2Pair.json && cp artifacts/contracts/staking/SingleAssetStaking.sol/SingleAssetStaking.json ../dapp-oeth/abis/SingleAssetStaking.json && cp artifacts/contracts/compensation/CompensationClaims.sol/CompensationClaims.json ../dapp-oeth/abis/CompensationClaims.json && cp artifacts/contracts/vault/OETHZapper.sol/OETHZapper.json ../dapp-oeth/abis/OETHZapper.json && cp artifacts/contracts/oracle/OracleRouter.sol/OETHOracleRouter.json ../dapp-oeth/abis/OETHOracleRouter.json", "echidna": "yarn run clean && echidna-test . --contract PropertiesOUSDTransferable --config contracts/crytic/TestOUSDTransferable.yaml", "compute-merkle-proofs-local": "HARDHAT_NETWORK=localhost node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", "compute-merkle-proofs-mainnet": "HARDHAT_NETWORK=mainnet node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", diff --git a/dapp-oeth/network.mainnet.json b/dapp-oeth/network.mainnet.json index 3e1920b4e9..a96fbe843e 100644 --- a/dapp-oeth/network.mainnet.json +++ b/dapp-oeth/network.mainnet.json @@ -12386,42 +12386,24 @@ ] }, "OETHOracleRouter": { - "address": "0x60fF8354e9C0E78e032B7daeA8da2c3265287dBd", + "address": "0x3ccd26e82f7305b12742fbb36708b42f82b61dba", "abi": [ { "inputs": [ - { - "internalType": "address", - "name": "_asset", - "type": "address" - } + { "internalType": "address", "name": "asset", "type": "address" } ], "name": "cacheDecimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "asset", - "type": "address" - } + { "internalType": "address", "name": "asset", "type": "address" } ], "name": "price", "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } + { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" @@ -25365,4 +25347,4 @@ ] } } -} \ No newline at end of file +} diff --git a/dapp-oeth/src/components/buySell/SwapCurrencyPill.js b/dapp-oeth/src/components/buySell/SwapCurrencyPill.js index 7a94a9d40c..cf4d4719cf 100644 --- a/dapp-oeth/src/components/buySell/SwapCurrencyPill.js +++ b/dapp-oeth/src/components/buySell/SwapCurrencyPill.js @@ -482,7 +482,7 @@ const SwapCurrencyPill = ({ swapMode, onErrorChange, coinValue, - ethPrice, + tokenConversions, }) => { const coinBalances = useStoreState(AccountStore, (s) => s.balances) const [error, setError] = useState(null) @@ -628,6 +628,8 @@ const SwapCurrencyPill = ({ onAmountChange(valueNoCommas) } + const usdPrice = tokenConversions?.[selectedCoin] || 0 + return ( <>
{bottomItem ? `$${formatCurrency( - truncateDecimals(expectedAmount, 18) * parseFloat(ethPrice), + truncateDecimals(expectedAmount, 18) * usdPrice, 2 )}` : `$${formatCurrency( - truncateDecimals(coinValue, 18) * parseFloat(ethPrice), + truncateDecimals(coinValue, 18) * usdPrice, 2 )}`}
@@ -723,7 +725,7 @@ const SwapCurrencyPill = ({ } }} options={coinsSelectOptions} - conversion={ethPrice} + conversion={usdPrice} coinBalances={coinBalances} /> diff --git a/dapp-oeth/src/components/buySell/SwapHomepage.js b/dapp-oeth/src/components/buySell/SwapHomepage.js index cde5704a43..1eed28a56d 100644 --- a/dapp-oeth/src/components/buySell/SwapHomepage.js +++ b/dapp-oeth/src/components/buySell/SwapHomepage.js @@ -8,7 +8,7 @@ import { currencies } from 'constants/Contract' import withRpcProvider from 'hoc/withRpcProvider' import usePriceTolerance from 'hooks/usePriceTolerance' import useCurrencySwapper from 'hooks/useCurrencySwapper' -import useEthPrice from 'hooks/useEthPrice' +import useTokenPrices from 'hooks/useTokenPrices' import SwapCurrencyPill from 'components/buySell/SwapCurrencyPill' import PillArrow from 'components/buySell/_PillArrow' import SettingsDropdown from 'components/buySell/SettingsDropdown' @@ -31,7 +31,7 @@ const SwapHomepage = ({ const swapEstimations = useStoreState(ContractStore, (s) => s.swapEstimations) const swapsLoaded = swapEstimations && typeof swapEstimations === 'object' const selectedSwap = useStoreState(ContractStore, (s) => s.selectedSwap) - const ethPrice = useEthPrice() + const { data: prices } = useTokenPrices() // mint / redeem const [swapMode, setSwapMode] = useState( @@ -340,7 +340,7 @@ const SwapHomepage = ({ onSelectChange={userSelectsBuyCoin} topItem onErrorChange={setBalanceError} - ethPrice={ethPrice} + tokenConversions={prices} /> diff --git a/dapp-oeth/src/constants/contractAddresses.js b/dapp-oeth/src/constants/contractAddresses.js index c54fcafa1a..d31361674f 100644 --- a/dapp-oeth/src/constants/contractAddresses.js +++ b/dapp-oeth/src/constants/contractAddresses.js @@ -88,6 +88,9 @@ addresses.mainnet.chainlinkUSDT_ETH = addresses.mainnet.chainlinkFAST_GAS = '0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C' +addresses.mainnet.oethOracleRouter = + '0xbE19cC5654e30dAF04AD3B5E06213D70F4e882eE' + // WETH Token addresses.mainnet.WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' diff --git a/dapp-oeth/src/hooks/useSwapEstimator.js b/dapp-oeth/src/hooks/useSwapEstimator.js index ad40a98740..3e7549ca9e 100644 --- a/dapp-oeth/src/hooks/useSwapEstimator.js +++ b/dapp-oeth/src/hooks/useSwapEstimator.js @@ -6,6 +6,7 @@ import AccountStore from 'stores/AccountStore' import { approveCoinGasLimits, max_price } from 'utils/constants' import { usePrevious } from 'utils/hooks' import useCurrencySwapper from 'hooks/useCurrencySwapper' +import useTokenPrices from 'hooks/useTokenPrices' import ContractStore from 'stores/ContractStore' import { calculateSwapAmounts } from 'utils/math' import fetchWithTimeout from 'utils/fetchWithTimeout' @@ -42,6 +43,9 @@ const useSwapEstimator = ({ (s) => s.vaultRebaseThreshold ) const gasPrice = useStoreState(ContractStore, (s) => s.gasPrice) + + const { data: prices } = useTokenPrices() + const previousGasPrice = usePrevious(gasPrice) const isGasPriceUserOverriden = useStoreState( ContractStore, @@ -190,6 +194,8 @@ const useSwapEstimator = ({ }) let usedGasPrice = gasPrice + const ethPrice = prices?.eth || 0 + const [ vaultResult, zapperResult, @@ -197,14 +203,12 @@ const useSwapEstimator = ({ // uniswapV2Result, // sushiswapResult, curveResult, - ethPrice, ] = await Promise.all([ swapMode === 'mint' ? estimateMintSuitabilityVault() : estimateRedeemSuitabilityVault(), estimateSwapSuitabilityZapper(), estimateSwapSuitabilityCurve(), - fetchEthPrice(), ]) if (!isGasPriceUserOverriden) { @@ -287,7 +291,10 @@ const useSwapEstimator = ({ const costWithGas = amountReceivedNumber + estimation.gasEstimateEth estimation.costMinusGasFees = costWithGas estimation.costMinusGasFeesUsd = costWithGas * ethPrice - estimation.amountReceivedUsd = amountReceivedNumber * ethPrice + + const swapTokenPrice = prices?.[estimation.coinToSwap || 'eth'] || 0 + + estimation.amountReceivedUsd = amountReceivedNumber * swapTokenPrice } }) diff --git a/dapp-oeth/src/hooks/useTokenPrices.js b/dapp-oeth/src/hooks/useTokenPrices.js new file mode 100644 index 0000000000..dca06b5aa8 --- /dev/null +++ b/dapp-oeth/src/hooks/useTokenPrices.js @@ -0,0 +1,162 @@ +import { useEffect, useState } from 'react' +import { useQuery } from 'react-query' +import { useStoreState } from 'pullstate' +import ContractStore from 'stores/ContractStore' +import { utils } from 'ethers' +import { allowancesService } from '../services/allowances.service' + +const tokenConfiguration = { + eth: { + id: 'ethereum', + symbol: 'eth', + name: 'Ethereum', + }, + weth: { + id: 'weth', + symbol: 'weth', + name: 'WETH', + platforms: { + ethereum: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + }, + }, + oeth: { + id: 'origin-ether', + symbol: 'oeth', + name: 'Origin Ether', + platforms: { + ethereum: '0x856c4efb76c1d1ae02e20ceb03a2a6a08b0b8dc3', + }, + }, + frxeth: { + id: 'frax-ether', + symbol: 'frxeth', + name: 'Frax Ether', + platforms: { + ethereum: '0x5e8422345238f34275888049021821e8e08caa1f', + }, + }, + sfrxeth: { + id: 'staked-frax-ether', + symbol: 'sfrxeth', + name: 'Staked Frax Ether', + platforms: { + ethereum: '0xac3e018457b222d93114458476f3e3416abbe38f', + }, + }, + steth: { + id: 'staked-ether', + symbol: 'steth', + name: 'Lido Staked Ether', + platforms: { + ethereum: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + }, + }, + reth: { + id: 'rocket-pool-eth', + symbol: 'reth', + name: 'Rocket Pool ETH', + platforms: { + ethereum: '0xae78736cd615f374d3085123a210448e74fc6393', + }, + }, +} + +const oethOraclePrice = (contract, tokenAddress) => contract.price(tokenAddress) + +const stakedFraxPrice = (contract) => + contract.previewRedeem(utils.parseEther('1')) + +const oraclePrices = async (tokens, contracts) => { + if ( + !contracts.chainlinkEthAggregator || + !contracts.oethOracleRouter || + !contracts.sfrxeth + ) { + return {} + } + + // Fetch baseline ETH price for conversion + const feed = await contracts.chainlinkEthAggregator.latestRoundData() + const ethPrice = Number(utils.formatUnits(feed?.answer, 8)) + + // Fetch token ratios + const tokenToPricingMethod = { + frxeth: oethOraclePrice.bind( + null, + contracts.oethOracleRouter, + tokenConfiguration.frxeth.platforms.ethereum + ), + steth: oethOraclePrice.bind( + null, + contracts.oethOracleRouter, + tokenConfiguration.steth.platforms.ethereum + ), + reth: oethOraclePrice.bind( + null, + contracts.oethOracleRouter, + tokenConfiguration.reth.platforms.ethereum + ), + sfrxeth: stakedFraxPrice.bind(null, contracts.sfrxeth), + } + + // Undefined token will return ratio 1:1 with eth + const fetchTokenRatio = async (token) => + (await tokenToPricingMethod?.[token]?.()) || utils.parseEther('1') + + const generateTokenMapping = (data) => + data.reduce( + (acc, weiRatio, index) => ({ + ...acc, + [tokens[index]]: Number(utils.formatEther(weiRatio)) * ethPrice, + }), + {} + ) + + return Promise.all(tokens.map(fetchTokenRatio)).then(generateTokenMapping) +} + +const coingeckoPrices = async (tokens) => { + const tokenIds = tokens.map((token) => tokenConfiguration[token].id) + + const baseUri = `${ + process.env.NEXT_PUBLIC_COINGECKO_API + }/simple/price?ids=${tokenIds.join(',')}&vs_currencies=usd` + + const generateTokenMapping = (data) => + tokens.reduce((acc, token) => { + const { id, symbol } = tokenConfiguration[token] + return { + ...acc, + [symbol]: data[id]?.usd || 0, + } + }, {}) + + return fetch(baseUri) + .then((res) => res.json()) + .then(generateTokenMapping) +} + +const useTokenPrices = ({ tokens = [] } = {}) => { + const contracts = useStoreState(ContractStore, (s) => s.contracts) + const chainId = useStoreState(ContractStore, (s) => s.chainId) + const queryTokens = + tokens?.length > 0 ? tokens : Object.keys(tokenConfiguration) + + const fetchTokenPrices = async () => { + let prices + + if (chainId === 1) { + prices = await oraclePrices(queryTokens, contracts) + } else { + prices = await coingeckoPrices(queryTokens) + } + + return prices + } + + return useQuery(queryTokens, fetchTokenPrices, { + enabled: contracts !== null, + }) +} + +export default useTokenPrices diff --git a/dapp-oeth/src/utils/contracts.js b/dapp-oeth/src/utils/contracts.js index 5b0f851b2d..f08a22362a 100644 --- a/dapp-oeth/src/utils/contracts.js +++ b/dapp-oeth/src/utils/contracts.js @@ -160,6 +160,7 @@ export async function setupContracts(account, chainId, fetchId) { compensation, chainlinkEthAggregator, chainlinkFastGasAggregator, + oethOracleRouter, curveAddressProvider let iVaultJson, @@ -176,6 +177,7 @@ export async function setupContracts(account, chainId, fetchId) { singleAssetStakingJson, compensationClaimsJson, chainlinkAggregatorV3Json, + oethOracleRouterJson, curveAddressProviderJson try { @@ -192,6 +194,7 @@ export async function setupContracts(account, chainId, fetchId) { uniV2SwapRouterJson = require('../../abis/UniswapV2Router.json') uniV3SwapQuoterJson = require('../../abis/UniswapV3Quoter.json') chainlinkAggregatorV3Json = require('../../abis/ChainlinkAggregatorV3Interface.json') + oethOracleRouterJson = require('../../abis/OETHOracleRouter.json') curveAddressProviderJson = require('../../abis/CurveAddressProvider.json') wousdJSON = require('../../abis/WOUSD.json') } catch (e) { @@ -279,6 +282,11 @@ export async function setupContracts(account, chainId, fetchId) { chainlinkAggregatorV3Json.abi ) + oethOracleRouter = getContract( + addresses.mainnet.oethOracleRouter, + oethOracleRouterJson.abi + ) + curveAddressProvider = getContract( addresses.mainnet.CurveAddressProvider, curveAddressProviderJson.abi @@ -490,6 +498,7 @@ export async function setupContracts(account, chainId, fetchId) { compensation, chainlinkEthAggregator, chainlinkFastGasAggregator, + oethOracleRouter, curveAddressProvider, curveRegistryExchange, curveOETHPool,