diff --git a/scripts/dataUtils.js b/scripts/dataUtils.js new file mode 100644 index 000000000..c704e1868 --- /dev/null +++ b/scripts/dataUtils.js @@ -0,0 +1,107 @@ +const fetch = require('node-fetch') + +const postQuery = async (endpoint, query) => { + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + } + const url = endpoint + const response = await fetch(url, options) + const data = await response.json() + if (data.errors) { + throw new Error(data.errors[0].message) + } else { + return data + } +} + +const getSubgraphUrl = (network, internal) => { + const prefix = internal ? 'gamma-internal' : 'gamma' + let url = `https://api.thegraph.com/subgraphs/name/opynfinance/${prefix}-${network}` + if (network === 'mainnet' && internal) { + url = 'https://api.thegraph.com/subgraphs/name/opynfinance/playground' + } + console.log(`Using subgraph endpoint`, url) + return url +} + +/** + * Get all oTokens + * @returns {Promise} + */ +module.exports.getOTokens = async (network, internal) => { + const currentTimeStamp = Math.floor(Date.now() / 1000) + + const query = ` + { + otokens ( + first: 1000, + where: { + expiryTimestamp_lt: ${currentTimeStamp}, + } + ) { + strikeAsset { + id + } + underlyingAsset { + id + } + collateralAsset { + id + } + isPut + expiryTimestamp + } + }` + try { + const url = getSubgraphUrl(network, internal) + const response = await postQuery(url, query) + return response.data.otokens + } catch (error) { + console.error(error) + return [] + } +} + +/** + * Get all oTokens + */ +module.exports.getAllSettlementPrices = async (asset, network, internal) => { + const query = ` + { + expiryPrices( + first: 1000, + where: { + asset: "${asset}" + } + ){ + expiry + price + } + } + + ` + try { + const url = getSubgraphUrl(network, internal) + const response = await postQuery(url, query) + return response.data.expiryPrices + } catch (error) { + console.error(error) + throw 'WTF' + return null + } +} + +/** + * + */ +module.exports.getCoinGeckData = async (coinId, expiry) => { + const date = new Date(expiry * 1000) + const yyyymmdd = date.toISOString().split('T')[0] + const [year, month, day] = yyyymmdd.split('-') + const dateString = `${day}-${month}-${year}` + const url = `https://api.coingecko.com/api/v3/coins/${coinId}/history?date=${dateString}&localization=false` + const data = await (await fetch(url)).json() + return Math.floor(data.market_data.current_price.usd * 1e8) +} diff --git a/scripts/migrateOracle.js b/scripts/migrateOracle.js index 59829a243..2c65f76fe 100644 --- a/scripts/migrateOracle.js +++ b/scripts/migrateOracle.js @@ -1,59 +1,114 @@ -const BigNumber = require('bignumber.js') - const yargs = require('yargs') - -const OtokenFactory = artifacts.require('OtokenFactory.sol') const Oracle = artifacts.require('Oracle.sol') -const Otoken = artifacts.require('Otoken.sol') +const apis = require('./dataUtils') + +/** + * @example + * truffle exec scripts/migrateOracle.js + * --network kovan + * --internal true + * --newOracle 0x8b71104e11931775957932728717b0F81461bc0d + * --asset 0x50570256f0da172a1908207aAf0c80d4b279f303 + * --coinId bitcoin + */ module.exports = async function(callback) { try { const options = yargs - .usage('Usage: --network --factory --oldOracle --newOracle --asset --gasPrice ') - .option('network', {describe: '0x exchange address', type: 'string', demandOption: true}) - .option('factory', {describe: 'Otoken factory address', type: 'string', demandOption: true}) - .option('oldOracle', {describe: 'Old oracle module address', type: 'string', demandOption: true}) - .option('newOracle', {describe: 'New oracle module address', type: 'string', demandOption: true}) - .option('asset', {describe: 'Asset address to migrate', type: 'string', demandOption: true}) - .option('gasPrice', {describe: 'Gas price in WEI', type: 'string', demandOption: false}).argv - + .usage( + 'Usage: --network --internal --newOracle --asset --coinId --gasPrice ', + ) + .option('network', { describe: '0x exchange address', type: 'string', demandOption: true }) + .option('newOracle', { describe: 'New oracle module address', type: 'string', demandOption: true }) + .option('asset', { describe: 'Asset address to migrate', type: 'string', demandOption: true }) + .option('internal', { describe: 'Internal network or not', type: 'string', demandOption: true }) + .option('coinId', { + describe: 'coingecko id used to search for price if it was not reported in our oracle', + type: 'string', + }) + .option('gasPrice', { describe: 'Gas price in WEI', type: 'string', demandOption: false }).argv + console.log('Init contracts 🍕') - const factory = await OtokenFactory.at(options.factory) - const oldOracle = await Oracle.at(options.oldOracle) + const asset = options.asset.toLowerCase() + const newOracle = await Oracle.at(options.newOracle) - console.log(`Getting list of create Otokens with asset ${options.asset} 🍕`) + console.log(`Getting list of create Otokens with asset ${asset} 🍕`) + + const otokens = await apis.getOTokens(options.network, options.internal === 'true') + + console.log(`# of otokens from subgraph: ${otokens.length}`) + + const prices = await apis.getAllSettlementPrices(asset, options.network, options.internal === 'true') + + const oTokensWithAsset = otokens.filter( + otoken => + otoken.underlyingAsset.id === asset || otoken.collateralAsset.id === asset || otoken.strikeAsset.id === asset, + ) + + const filteredExpiries = oTokensWithAsset.reduce((prev, curr) => { + if (!prev.includes(curr.expiryTimestamp)) { + return [...prev, curr.expiryTimestamp] + } else { + return [...prev] + } + }, []) + + console.log(`expires needed to fill in ${filteredExpiries.length}`) + + const knownRecordSubgraph = prices.filter(priceObj => filteredExpiries.includes(priceObj.expiry)) + + const knownPrices = knownRecordSubgraph.map(obj => obj.price) + const knownExpires = knownRecordSubgraph.map(obj => obj.expiry) + + let finalPrices = [] + let finalExpires = [] - const currentTimeStamp = Math.floor(Date.now() / 1000); - const otokenLength = new BigNumber(await factory.getOtokensLength()) - - let expiriesToMigrate = [] - let pricesToMigrate = [] + const missingExpires = filteredExpiries.filter( + expiry => !knownRecordSubgraph.map(obj => obj.expiry).includes(expiry), + ) - for(let i = 0; i < otokenLength.toFixed(); i++) { - const otokenAddress = await factory.otokens(i) - const otokenDetails = await (await Otoken.at(otokenAddress)).getOtokenDetails() - const expiry = new BigNumber(otokenDetails[4]) + if (options.coinId) { + console.log(`Using coinID ${options.coinId} to fill in ${missingExpires.length} missing prices! `) - if((otokenDetails[0] == options.asset || otokenDetails[1] == options.asset || otokenDetails[2] == options.asset) && (expiry.toNumber() < currentTimeStamp) && (!expiriesToMigrate.includes(expiry.toString()))) { - const priceToMigrate = new BigNumber((await oldOracle.getExpiryPrice(options.asset, expiry.toString()))[0]) + const missingPrices = [] + for (const expiry of missingExpires) { + const price = await apis.getCoinGeckData(options.coinId, expiry) + missingPrices.push(price.toString()) + console.log(`Got price for expiry ${expiry} from CoinGeck: ${price}`) + } - expiriesToMigrate.push(expiry.toString()) - pricesToMigrate.push(priceToMigrate.toString()) + finalPrices = [...knownPrices, ...missingPrices] + finalExpires = [...knownExpires, ...missingExpires] + } else { + if (missingExpires.length > 0) { + console.log( + `Missing ${missingExpires.length} prices to support redeeming all options. Please put in coinId to fetch those `, + ) } + + finalPrices = knownPrices + finalExpires = knownExpires } - console.log(`Found ${expiriesToMigrate.length} expiry price 🎉`) + console.log(`Final expires`, finalExpires) + console.log(`Final prices`, finalPrices) + + // console.log(`Found ${expiriesToMigrate.length} expiry price 🎉`) + + if (finalPrices.length === 0) throw 'Nothing to Migrate' + console.log(`Migrating prices to new Oracle at ${newOracle.address} 🎉`) - const tx = await newOracle.migrateOracle(options.asset, expiriesToMigrate, pricesToMigrate) + const overrideOptions = options.gasPrice ? { gasPrice: options.gasPrice } : undefined + const tx = await newOracle.migrateOracle(options.asset, finalExpires, finalPrices, overrideOptions) console.log(`Oracle prices migration done for asset ${options.asset}! 🎉`) - console.log(`Transaction hash: ${tx.transactionHash}`) + console.log(`Transaction hash: ${tx.tx}`) callback() } catch (err) { callback(err) } -} \ No newline at end of file +} diff --git a/truffle-config.js b/truffle-config.js index 3724c4dc6..eca765019 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -60,7 +60,7 @@ module.exports = { confirmations: 2, timeoutBlocks: 50, skipDryRun: false, - gasPrice: 100000000000, + gasPrice: 25000000000, }, avax: { provider: () => new HDWalletProvider(mnemonic, 'https://api.avax.network/ext/bc/C/rpc'),