diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..48ed472 --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +COPYRIGHT © 2020 RARI CAPITAL, INC. ALL RIGHTS RESERVED. + +No one is permitted to use the software for any purpose without the explicit permission of David Lucid of Rari Capital, Inc. + +This license is liable to change at any time at the sole discretion of David Lucid of Rari Capital, Inc. diff --git a/README.md b/README.md new file mode 100644 index 0000000..83e283f --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Fuse by Rari Capital: Uniswap V2 TWAP Bot + +This repository contains the JavaScript source code for the Fuse Uniswap V2 TWAP Bot. See [here for the Fuse dApp](https://github.com/Rari-Capital/fuse-dapp), [here for the Fuse SDK](https://github.com/Rari-Capital/fuse-sdk), or [here for the Fuse contracts](https://github.com/Rari-Capital/fuse-contracts). + +## How it works + +The [`UniswapView` price oracle](https://github.com/Rari-Capital/open-oracle/blob/master/contracts/Uniswap/UniswapView.sol) provides on-chain price data to Fuse pools via [TWAPs of Uniswap V2 pairs](https://uniswap.org/docs/v2/core-concepts/oracles/) based in ETH. If you are deploying your own `UniswapView` or using prices for which others are not posting TWAPs, you will need to run a bot to compute TWAPs and post them to the `UniswapView`. Note that the more often you compute and post TWAPs and the more assets you do so for, the ETH you will spend on gas fees. However, also note that infrequent updates to an asset's price could leave room for attackers to profit via arbitrage at the expense of your users. + +## Installation + +We had success building the dApp using Node.js `v12.16.1` with the latest version of NPM. + +To install the bot's dependencies: `npm install` + +## Running the bot + +`npm start` + +## License + +See `LICENSE`. + +## Credits + +Fuse's dApp is developed by [David Lucid](https://github.com/davidlucid) of Rari Capital. Find out more about Rari Capital at [rari.capital](https://rari.capital). diff --git a/abi/PreferredPriceOracle.json b/abi/PreferredPriceOracle.json new file mode 100644 index 0000000..67777ba --- /dev/null +++ b/abi/PreferredPriceOracle.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"contract ChainlinkPriceOracle","name":"_chainlinkOracle","type":"address"},{"internalType":"contract PriceOracle","name":"_secondaryOracle","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"chainlinkOracle","outputs":[{"internalType":"contract ChainlinkPriceOracle","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"contract CToken","name":"cToken","type":"address"}],"name":"getUnderlyingPrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"secondaryOracle","outputs":[{"internalType":"contract PriceOracle","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}] diff --git a/abi/UniswapView.json b/abi/UniswapView.json new file mode 100644 index 0000000..27e3219 --- /dev/null +++ b/abi/UniswapView.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"uint256","name":"anchorPeriod_","type":"uint256"},{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig[]","name":"configs","type":"tuple[]"},{"internalType":"uint256","name":"maxTokens","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"symbol","type":"string"},{"indexed":false,"internalType":"uint256","name":"anchorPrice","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"oldTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newTimestamp","type":"uint256"}],"name":"AnchorPriceUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"symbol","type":"string"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"}],"name":"PriceUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"oldTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"oldPrice","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newPrice","type":"uint256"}],"name":"UniswapWindowUpdated","type":"event"},{"inputs":[{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig[]","name":"configs","type":"tuple[]"}],"name":"add","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"anchorPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newAdmin","type":"address"}],"name":"changeAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"ethBaseUnit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"expScale","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"i","type":"uint256"}],"name":"getTokenConfig","outputs":[{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"cToken","type":"address"}],"name":"getTokenConfigByCToken","outputs":[{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"symbol","type":"string"}],"name":"getTokenConfigBySymbol","outputs":[{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"symbolHash","type":"bytes32"}],"name":"getTokenConfigBySymbolHash","outputs":[{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"underlying","type":"address"}],"name":"getTokenConfigByUnderlying","outputs":[{"components":[{"internalType":"address","name":"underlying","type":"address"},{"internalType":"bytes32","name":"symbolHash","type":"bytes32"},{"internalType":"uint256","name":"baseUnit","type":"uint256"},{"internalType":"enum UniswapConfig.PriceSource","name":"priceSource","type":"uint8"},{"internalType":"uint256","name":"fixedPrice","type":"uint256"},{"internalType":"address","name":"uniswapMarket","type":"address"},{"internalType":"bool","name":"isUniswapReversed","type":"bool"}],"internalType":"struct UniswapConfig.TokenConfig","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"cToken","type":"address"}],"name":"getUnderlyingPrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"newObservations","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint256","name":"acc","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"numTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"oldObservations","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint256","name":"acc","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string[]","name":"symbols","type":"string[]"}],"name":"postPrices","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"symbol","type":"string"}],"name":"price","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"prices","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..7faef7b --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,33 @@ +module.exports = { + apps : [{ + name: 'fuse-twap-bot', + script: 'index.js', + + // Options reference: https://pm2.keymetrics.io/docs/usage/application-declaration/ + // args: 'one two', + // instances: 1, + // autorestart: true, + // watch: false, + // max_memory_restart: '1G', + time: true, + env: { + NODE_ENV: 'development', + ETHEREUM_ADMIN_ACCOUNT: '', + ETHEREUM_ADMIN_PRIVATE_KEY: '', + PRICE_ORACLE_CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000000', + SUPPORTED_SYMBOLS: 'RGT,SFI', + WEB3_HTTP_PROVIDER_URL: "http://localhost:8546", + TWAP_POSTING_INTERVAL_SECONDS: 60 * 60 + }, + env_production: { + NODE_ENV: 'production', + ETHEREUM_ADMIN_ACCOUNT: '', + ETHEREUM_ADMIN_PRIVATE_KEY: '', + PRICE_ORACLE_CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000000', + SUPPORTED_SYMBOLS: 'RGT,SFI', + WEB3_HTTP_PROVIDER_URL: "http://localhost:8546", + TWAP_POSTING_INTERVAL_SECONDS: 60 * 60 * 24 + } + }] + }; + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..cdfa93f --- /dev/null +++ b/index.js @@ -0,0 +1,56 @@ +import Web3 from 'web3'; + +var web3 = new Web3(new Web3.providers.HttpProvider(process.env.WEB3_HTTP_PROVIDER_URL)); +const preferredPriceOracleAbi = JSON.parse(fs.readFileSync(__dirname + '/abi/PreferredPriceOracle.json', 'utf8')); +const uniswapViewAbi = JSON.parse(fs.readFileSync(__dirname + '/abi/UniswapView.json', 'utf8')); +var priceOracleContract = new web3.eth.Contract(preferredPriceOracleAbi, process.env.PRICE_ORACLE_CONTRACT_ADDRESS); + +try { + process.env.PRICE_ORACLE_CONTRACT_ADDRESS = await priceOracleContract.methods.secondaryOracle(); +} catch { } + +priceOracleContract = new web3.eth.Contract(uniswapViewAbi, process.env.PRICE_ORACLE_CONTRACT_ADDRESS); +var symbols = process.env.SUPPORTED_SYMBOLS.split(','); + +async function computeAndPostTwaps() { + // Create swapExactETHForTokens transaction + var data = priceOracleContract.methods.postPrices(symbols).encodeABI(); + + // Build transaction + var tx = { + from: process.env.ETHEREUM_ADMIN_ACCOUNT, + to: process.env.PRICE_ORACLE_CONTRACT_ADDRESS, + value: value, + data: data, + nonce: await web3.eth.getTransactionCount(process.env.ETHEREUM_ADMIN_ACCOUNT) + }; + + if (process.env.NODE_ENV !== "production") console.log("Signing and sending postPrices transaction:", tx); + + // Estimate gas for transaction + try { + tx["gas"] = await web3.eth.estimateGas(tx); + } catch (error) { + throw "Failed to estimate gas before signing and sending postPrices transaction: " + error; + } + + // Sign transaction + try { + var signedTx = await web3.eth.accounts.signTransaction(tx, process.env.ETHEREUM_ADMIN_PRIVATE_KEY); + } catch (error) { + throw "Error signing postPrices transaction: " + error; + } + + // Send transaction + try { + var sentTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); + } catch (error) { + throw "Error sending postPrices transaction: " + error; + } + + console.log("Successfully sent postPrices transaction:", sentTx); + return sentTx; +} + +setInterval(computeAndPostTwaps, process.env.TWAP_POSTING_INTERVAL_SECONDS); +computeAndPostTwaps(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..82dc02a --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "fuse-twap-bot", + "version": "0.1.0", + "description": "Spends gas to compute TWAPs for Uniswap V2 pairs and post to Fuse price oracles.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Rari-Capital/fuse-twap-bot.git" + }, + "author": "David Lucid ", + "license": "SEE LICENSE IN LICENSE", + "bugs": { + "url": "https://github.com/Rari-Capital/fuse-twap-bot/issues" + }, + "homepage": "https://github.com/Rari-Capital/fuse-twap-bot#readme", + "dependencies": { + "web3": "^1.3.1" + } +}