Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlucid committed Dec 28, 2020
0 parents commit 1d839b7
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 0 deletions.
104 changes: 104 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions abi/PreferredPriceOracle.json
Original file line number Diff line number Diff line change
@@ -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"}]
1 change: 1 addition & 0 deletions abi/UniswapView.json
Original file line number Diff line number Diff line change
@@ -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"}]
33 changes: 33 additions & 0 deletions ecosystem.config.js
Original file line number Diff line number Diff line change
@@ -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
}
}]
};

56 changes: 56 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -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();
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"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"
}
}

0 comments on commit 1d839b7

Please sign in to comment.