Skip to content

Commit

Permalink
Adds proper USD pricing conversion for token swap (#1788)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
smitch88 authored Aug 31, 2023
1 parent 67fa12a commit ca37d90
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 36 deletions.
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 6 additions & 24 deletions dapp-oeth/network.mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -25365,4 +25347,4 @@
]
}
}
}
}
10 changes: 6 additions & 4 deletions dapp-oeth/src/components/buySell/SwapCurrencyPill.js
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ const SwapCurrencyPill = ({
swapMode,
onErrorChange,
coinValue,
ethPrice,
tokenConversions,
}) => {
const coinBalances = useStoreState(AccountStore, (s) => s.balances)
const [error, setError] = useState(null)
Expand Down Expand Up @@ -628,6 +628,8 @@ const SwapCurrencyPill = ({
onAmountChange(valueNoCommas)
}

const usdPrice = tokenConversions?.[selectedCoin] || 0

return (
<>
<div
Expand Down Expand Up @@ -673,11 +675,11 @@ const SwapCurrencyPill = ({
<div className="usd-balance mt-auto">
{bottomItem
? `$${formatCurrency(
truncateDecimals(expectedAmount, 18) * parseFloat(ethPrice),
truncateDecimals(expectedAmount, 18) * usdPrice,
2
)}`
: `$${formatCurrency(
truncateDecimals(coinValue, 18) * parseFloat(ethPrice),
truncateDecimals(coinValue, 18) * usdPrice,
2
)}`}
</div>
Expand Down Expand Up @@ -723,7 +725,7 @@ const SwapCurrencyPill = ({
}
}}
options={coinsSelectOptions}
conversion={ethPrice}
conversion={usdPrice}
coinBalances={coinBalances}
/>
</div>
Expand Down
8 changes: 4 additions & 4 deletions dapp-oeth/src/components/buySell/SwapHomepage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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(
Expand Down Expand Up @@ -340,7 +340,7 @@ const SwapHomepage = ({
onSelectChange={userSelectsBuyCoin}
topItem
onErrorChange={setBalanceError}
ethPrice={ethPrice}
tokenConversions={prices}
/>
<PillArrow swapMode={swapMode} setSwapMode={setSwapMode} />
<SwapCurrencyPill
Expand All @@ -350,7 +350,7 @@ const SwapHomepage = ({
priceToleranceValue={priceToleranceValue}
selectedCoin={selectedRedeemCoin}
onSelectChange={userSelectsRedeemCoin}
ethPrice={ethPrice}
tokenConversions={prices}
/>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions dapp-oeth/src/constants/contractAddresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ addresses.mainnet.chainlinkUSDT_ETH =
addresses.mainnet.chainlinkFAST_GAS =
'0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C'

addresses.mainnet.oethOracleRouter =
'0xbE19cC5654e30dAF04AD3B5E06213D70F4e882eE'

// WETH Token
addresses.mainnet.WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'

Expand Down
13 changes: 10 additions & 3 deletions dapp-oeth/src/hooks/useSwapEstimator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -190,21 +194,21 @@ const useSwapEstimator = ({
})
let usedGasPrice = gasPrice

const ethPrice = prices?.eth || 0

const [
vaultResult,
zapperResult,
// uniswapResult,
// uniswapV2Result,
// sushiswapResult,
curveResult,
ethPrice,
] = await Promise.all([
swapMode === 'mint'
? estimateMintSuitabilityVault()
: estimateRedeemSuitabilityVault(),
estimateSwapSuitabilityZapper(),
estimateSwapSuitabilityCurve(),
fetchEthPrice(),
])

if (!isGasPriceUserOverriden) {
Expand Down Expand Up @@ -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
}
})

Expand Down
162 changes: 162 additions & 0 deletions dapp-oeth/src/hooks/useTokenPrices.js
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit ca37d90

Please sign in to comment.