diff --git a/.gitmodules b/.gitmodules index 5b2fc0fa..0542e7fc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ [submodule "user-rating"] path = user-rating url = https://github.com/backstop-protocol/user-rating.git +[submodule "open-oracle"] + path = open-oracle + url = https://github.com/compound-finance/open-oracle.git diff --git a/contracts/bprotocol/info/LiquidatorInfo.sol b/contracts/bprotocol/info/LiquidatorInfo.sol index 764cd18b..61062e44 100644 --- a/contracts/bprotocol/info/LiquidatorInfo.sol +++ b/contracts/bprotocol/info/LiquidatorInfo.sol @@ -87,7 +87,7 @@ contract LiquidatorInfo { if(registry.cEther() == cTokens[i]) info.debtTokens[i] = ETH; else - info.debtTokens[i] = address(CTokenInterface(info.debtTokens[i]).underlying()); + info.debtTokens[i] = address(CTokenInterface(cTokens[i]).underlying()); address bToken = bComptroller.c2b(cTokens[i]); info.debtAmounts[i] = IBToken(bToken).borrowBalanceCurrent(user); diff --git a/contracts/bprotocol/info/UserInfo.sol b/contracts/bprotocol/info/UserInfo.sol index c85420d6..1ece3235 100644 --- a/contracts/bprotocol/info/UserInfo.sol +++ b/contracts/bprotocol/info/UserInfo.sol @@ -122,6 +122,7 @@ contract UserInfo { string memory name = ERC20Like(ctoken).name(); if(keccak256(abi.encodePacked(name)) == keccak256(abi.encodePacked("Compound ETH"))) return true; if(keccak256(abi.encodePacked(name)) == keccak256(abi.encodePacked("Compound Ether"))) return true; + if(keccak256(abi.encodePacked(name)) == keccak256(abi.encodePacked("cETH"))) return true; // for testenv return false; } @@ -139,6 +140,7 @@ contract UserInfo { } function getTokenInfo(address comptroller, address bComptroller) public returns(TokenInfo memory info) { + address[] memory markets = ComptrollerLike(comptroller).getAllMarkets(); uint numMarkets = markets.length; info.btoken = new address[](numMarkets); diff --git a/contracts/mock/MockUniswapAnchoredView.sol b/contracts/mock/MockUniswapAnchoredView.sol new file mode 100644 index 00000000..c1b3e8af --- /dev/null +++ b/contracts/mock/MockUniswapAnchoredView.sol @@ -0,0 +1,81 @@ +pragma solidity 0.5.16; +pragma experimental ABIEncoderV2; + +import "hardhat/console.sol"; + +contract MockUniswapAnchoredView { + // cToken => uint + mapping(address => uint) public prices; + // symbol => cToken + mapping(string => address) public symbolToCTokens; + // cToken => symbol + mapping(address => string) public cTokenToSymbol; + // cToken => decimals + mapping(address => uint) public cTokenDecimals; + + constructor(string[] memory symbols, address[] memory cTokens, uint[] memory decimals) public { + + for (uint i = 0; i < symbols.length; i++) { + string memory symbol = symbols[i]; + address cToken = cTokens[i]; + uint decimal = decimals[i]; + symbolToCTokens[symbol] = cToken; + cTokenToSymbol[cToken] = symbol; + cTokenDecimals[cToken] = decimal; + } + + } + + function postPrices(bytes[] calldata messages, bytes[] calldata signatures, string[] calldata symbols) external { + + for (uint i = 0; i < messages.length; i++) { + (address source, uint64 timestamp, string memory key, uint64 value) = decodeMessage(messages[i], signatures[i]); + + console.log("source", source); + console.log("timestamp", timestamp); + console.log("key", key); + console.log("value", value); + address cToken = symbolToCTokens[key]; + // signed `value` is in USD upto 6 decimal places + // hence, `value` already have 6 decimals + // For example: USDT which has 6 decimal tokens: + // oracle price will be `decimalToIncrease` = (36 - `value`-decimals - tokenDecimals) + // `decimalToIncrease` = 36 - 6 - 6 + // `decimalToIncrease` = 30 - 6 = 24 decimals to add more in value + // fixed 30 which is derived from 36 - `value`-decimals = 36 - 6 = 30 + uint decimalToIncrease = 1 * 10**(30 - cTokenDecimals[cToken]); // 1e(30-decimals) + uint convertedValue = value * decimalToIncrease; + console.log("convertedValue", convertedValue); + setPrice(cToken, convertedValue); + } + + // custom code below + //require(source != address(0), "invalid signature"); + //prices[source][key] = value; + } + + // Directly set the prices + function setPrice(address cToken, uint newPrice) public { + prices[cToken] = newPrice; + } + + function getUnderlyingPrice(address cToken) external view returns (uint) { + return prices[cToken]; + } + + function decodeMessage(bytes memory message, bytes memory signature) internal pure returns (address, uint64, string memory, uint64) { + // Recover the source address + address source = source(message, signature); + + // Decode the message and check the kind + (string memory kind, uint64 timestamp, string memory key, uint64 value) = abi.decode(message, (string, uint64, string, uint64)); + require(keccak256(abi.encodePacked(kind)) == keccak256(abi.encodePacked("prices")), "Kind of data must be 'prices'"); + return (source, timestamp, key, value); + } + + function source(bytes memory message, bytes memory signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = abi.decode(signature, (bytes32, bytes32, uint8)); + bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(message))); + return ecrecover(hash, v, r, s); + } +} \ No newline at end of file diff --git a/open-oracle b/open-oracle new file mode 160000 index 00000000..046b32bb --- /dev/null +++ b/open-oracle @@ -0,0 +1 @@ +Subproject commit 046b32bbf239fda2f829d231e5850f4808133b3f diff --git a/package-lock.json b/package-lock.json index db2d5871..bd8b03bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9918,6 +9918,15 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dev": true, + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -23558,7 +23567,7 @@ } }, "ethereumjs-abi": { - "version": "git+ssh://git@github.com/ethereumjs/ethereumjs-abi.git#1ce6a1d64235fabe2aaf827fd606def55693508f", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1a27c59c15ab1e95ee8e5c4ed6ad814c49cc439e", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { diff --git a/package.json b/package.json index 7167964e..4c2d417f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/mocha": "^8.2.0", "@types/node": "^14.14.14", "@types/web3": "^1.2.2", + "axios": "^0.21.1", "chai": "^4.2.0", "chai-bn": "^0.2.1", "eslint": "^7.16.0", @@ -48,7 +49,7 @@ }, "scripts": { "deploy-compound": "cd compound-protocol && PROVIDER='http://localhost:8545/' yarn run repl -s script/scen/scriptFlywheel.scen", - "postinstall": "git submodule update --init --recursive && cd compound-protocol && yarn && cd scenario && yarn", + "postinstall": "git submodule update --init --recursive && cd compound-protocol && yarn && cd scenario && yarn && cd ../../open-oracle/sdk/javascript && yarn", "compile": "hardhat compile", "ganache": "ganache-cli --gasLimit 20000000 --gasPrice 20000 --defaultBalanceEther 1000000000 --allowUnlimitedContractSize true", "test": "hardhat test", @@ -62,7 +63,8 @@ "exec-coverage": "node --max-old-space-size=4096 ./node_modules/.bin/truffle run coverage --network coverage", "coverage": "FILE=coverage npm run clean-snapshot && npm run unzip-snapshot && npm run copy-coverage-files && npm run exec-coverage", "load-snapshot": "echo $FILE && npm run clean-snapshot && npm run unzip-snapshot && npm run start-ganache", - "start-ganache": "echo $FILE && npx ganache-cli -l 1250000000000 --allowUnlimitedContractSize -a 20 -e 100000000000000 -m 'bonus patient judge normal delay supreme sentence confirm fox desk cool estate' --db './snapshot/'$FILE" + "start-ganache": "echo $FILE && npx ganache-cli -l 0xfffffffffff --allowUnlimitedContractSize -a 20 -e 100000000000000 -m 'bonus patient judge normal delay supreme sentence confirm fox desk cool estate' --db './snapshot/'$FILE", + "start-price-reporter": "cd open-oracle/sdk/javascript && yarn run start --private_key 0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10 --script ../../../scripts/FakeOraclePrice.js" }, "repository": { "type": "git", diff --git a/playground/bcompound.json b/playground/bcompound.json index 2a36e8de..b9b119e0 100644 --- a/playground/bcompound.json +++ b/playground/bcompound.json @@ -28,7 +28,12 @@ "Migrate": "0xeC991E2686be779cFaaA8D0C723464435F17bD9f", "Pool": "0x86F99Aa3ff0D9B53eeF319E20bA3a8F5e54990B4", "Registry": "0x066a5D381D02F435190527Ac7c0D2370Ed56B18F", - "BScore": "0x178743c6477774C9e80a5B7644c35f9eC49F3071" + "BScore": "0x178743c6477774C9e80a5B7644c35f9eC49F3071", + + "MEMBER_1": "0xfc046d71fc76629bEc2Cb72E6324A8036BBf8ffF", + "MEMBER_2": "0xa6082e72eE9ed626Af46c5A20B2C8eA72283A695", + "MEMBER_3": "0xdd8dD6A5555e15066ecCbdd185828b9C389Cc881", + "MEMBER_4": "0xE7dE50F74A3533f044eB6edAE3C425b9ca6f2909" } } \ No newline at end of file diff --git a/scripts/FakeOraclePrice.js b/scripts/FakeOraclePrice.js new file mode 100644 index 00000000..f4377235 --- /dev/null +++ b/scripts/FakeOraclePrice.js @@ -0,0 +1,5 @@ +module.exports = async function fetchPrices(now) { + // mainnet prices taken + // ZRX price is actually = 1.605584, but increased 10% (0.1605584) = 1.605584 + 0.1605584 = 1.7661424 + return [now, { eth: 1617.45, zrx: 1.7661424, bat: 0.4, usdt: 1.0, wbtc: 39202.8 }]; +}; diff --git a/test/bot/README.md b/test/bot/README.md new file mode 100644 index 00000000..fa87a91b --- /dev/null +++ b/test/bot/README.md @@ -0,0 +1,43 @@ + +## Testing with Bot + +Testing of the Bot requires four different terminal sessions. + +### Load BCompound snapshot + +Load BCompound contracts snapshot in first CLI session: + +`$ FILE=bcompound npm run load-snapshot` + +Ensure that ganache started before starting the following command. + +### Start open-oracle-reporter + +Open second CLI and run the below command to start the open-oracle-reporter. The prices this command use it configured in `scripts/FakeOraclePrice.js`. You can modify this file to change the token prices that you want bot to post to the oracle contract. + +`$ npm run start-price-reporter` + +### Run bot + +Open third CLI session and run the bot using the below command: + +#### Set coinbase API keys as environment variables +You need to set the coinbase API keys to fetch the mainnet prices. You can configure these as environment variables. + +`$ export COINBASE_SECRET=""` + +`$ export COINBASE_APIKEY=""` + +`$ export COINBASE_PHRASE=""` + +Command to start the bot: + +`$ npx truffle exec test/bot/bot.js` + +Ensure that after running the bot you must wait for the output `listening for blocks...` in the CLI then only start the bot-test (given below). + +### Run bot test + +Open fourth CLI session and run the below command to start the bot test: + +`npx hardhat test test/bot/testBot.ts` \ No newline at end of file diff --git a/test/bot/bot.js b/test/bot/bot.js new file mode 100644 index 00000000..f643885c --- /dev/null +++ b/test/bot/bot.js @@ -0,0 +1,524 @@ +"use strict"; +const express = require("express"); +const crypto = require("crypto"); +const axios = require("axios"); +const json = require("../../playground/bcompound.json"); + +const { BN } = require("@openzeppelin/test-helpers"); +const { exit } = require("shelljs"); + +// Constants +const ZERO = new BN(0); +const ONE_USD = new BN(10).pow(new BN(18)); +const MIN_LIQUIDITY = ONE_USD.mul(new BN(10)); + +// Compound +const Comptroller = artifacts.require("Comptroller"); +const Comp = artifacts.require("Comp"); +const CErc20 = artifacts.require("CErc20"); +const CEther = artifacts.require("CEther"); +const ERC20Detailed = artifacts.require("ERC20Detailed"); +const FakePriceOracle = artifacts.require("FakePriceOracle"); +const MockUniswapAnchoredView = artifacts.require("MockUniswapAnchoredView"); + +// BCompound +const BComptroller = artifacts.require("BComptroller"); +const GovernanceExecutor = artifacts.require("GovernanceExecutor"); +const CompoundJar = artifacts.require("CompoundJar"); +const JarConnector = artifacts.require("JarConnector"); +const Migrate = artifacts.require("Migrate"); +const Pool = artifacts.require("Pool"); +const Registry = artifacts.require("Registry"); +const BScore = artifacts.require("BScore"); +const BErc20 = artifacts.require("BErc20"); +const BEther = artifacts.require("BEther"); +const Avatar = artifacts.require("Avatar"); +const LiquidatorInfo = artifacts.require("LiquidatorInfo"); +const UserInfo = artifacts.require("UserInfo"); + +// variables +let pendingMap = new Map(); + +// TokenInfo +let tokenInfo; +let tokenInfoKeys = { + btoken: "btoken", + ctoken: "ctoken", + ctokenDecimals: "ctokenDecimals", + underlying: "underlying", + underlyingDecimals: "underlyingDecimals", + ctokenExchangeRate: "ctokenExchangeRate", + underlyingPrice: "underlyingPrice", + borrowRate: "borrowRate", + supplyRate: "supplyRate", + listed: "listed", + collateralFactor: "collateralFactor", + bTotalSupply: "bTotalSupply", +}; + +// Structs +let liquidationInfoKeys = { remainingLiquidationSize: "remainingLiquidationSize" }; +let cushionInfoKeys = { + hasCushion: "hasCushion", + shouldTopup: "shouldTopup", + shouldUntop: "shouldUntop", + cushionCurrentToken: "cushionCurrentToken", + cushionCurrentSize: "cushionCurrentSize", + cushionPossibleTokens: "cushionPossibleTokens", + cushionMaxSizes: "cushionMaxSizes", +}; +let avatarInfoKeys = { + user: "user", + totalDebt: "totalDebt", + totalCollateral: "totalCollateral", + weightedCollateral: "weightedCollateral", + debtTokens: "debtTokens", + debtAmounts: "debtAmounts", + collateralTokens: "collateralTokens", + collateralAmounts: "collateralAmounts", + weightedCollateralAmounts: "weightedCollateralAmounts", +}; +let accountInfoKeys = { + avatarInfo: "avatarInfo", + cushionInfo: "cushionInfo", + liquidationInfo: "liquidationInfo", +}; + +// Members +const MEMBER_1 = json.bcompound.MEMBER_1; +const MEMBER_2 = json.bcompound.MEMBER_2; +const MEMBER_3 = json.bcompound.MEMBER_3; +const MEMBER_4 = json.bcompound.MEMBER_4; + +// Compound; +let comptroller; +let comp; +let oracle; + +// BCompound +let bComptroller; +let registry; +let pool; +let liquidatorInfo; +let userInfo; + +// ETH / ERC20 +let cETH; +let bETH; + +let ZRX; +let cZRX; +let bZRX; + +let USDT; +let cUSDT; +let bUSDT; + +let BAT; +let cBAT; +let bBAT; + +let WBTC; +let cWBTC; +let bWBTC; + +// Constant MAINNET token prices +const ONE_ETH_IN_USD_MAINNET = new BN("1617455000000000000000"); // 18 decimals, ($1617.45) +const ONE_ZRX_IN_USD_MAINNET = new BN("1605584000000000000"); // 18 decimal, ($1.6) +const ONE_USDT_IN_USD_MAINNET = new BN("1000000000000000000000000000000"); //30 decimals, ($1) +const ONE_BAT_IN_USD_MAINNET = new BN("409988000000000000"); // 18 decimals, ($0.4) +const ONE_WBTC_IN_USD_MAINNET = new BN("392028400000000000000000000000000"); // 28 decimals, ($39202.8) + +// For ORACLE +const symbols = ["ETH", "ZRX", "USDT", "BAT", "WBTC"]; +const cTokens = [ + json.compound.cETH, + json.compound.cZRX, + json.compound.cUSDT, + json.compound.cBAT, + json.compound.cWBTC, +]; +const decimals = [18, 18, 6, 18, 8]; + +module.exports = async function (callback) { + console.log("starting bot..."); + + try { + await init(); + await validateBCompoundDeployment(); + await validateCompoundDeployment(); + + await loadTokens(); + await setupOracle(); + + console.log("listening for blocks..."); + web3.eth.subscribe("newBlockHeaders", async (error, event) => { + if (error) { + console.log(error); + exit(1); + } + + console.log("Block: " + event.number + " Timestamp: " + event.timestamp); + await processBlock(); + }); + } catch (error) { + console.log(error); + } +}; + +async function setupOracle() { + oracle = await deployUniswapAnchoredView(); + // set new oracle + await comptroller._setPriceOracle(oracle.address); + console.log(oracle.address); + console.log(await comptroller.oracle()); + //set prices in new oracle + await setMainnetTokenPrice(); +} + +async function deployUniswapAnchoredView() { + const mock = await MockUniswapAnchoredView.new(symbols, cTokens, decimals); + return mock; +} + +async function setMainnetTokenPrice() { + // mainnet snapshot prices + await oracle.setPrice(cETH.address, ONE_ETH_IN_USD_MAINNET); + await oracle.setPrice(cZRX.address, ONE_ZRX_IN_USD_MAINNET); + await oracle.setPrice(cUSDT.address, ONE_USDT_IN_USD_MAINNET); + await oracle.setPrice(cBAT.address, ONE_BAT_IN_USD_MAINNET); + await oracle.setPrice(cWBTC.address, ONE_WBTC_IN_USD_MAINNET); +} + +async function loadTokens() { + console.log("Loading cTokens and bTokens..."); + const numMarkets = await userInfo.getNumMarkets.call(comptroller.address); + console.log("Num of Markets: " + numMarkets); + + tokenInfo = await userInfo.getTokenInfo.call(comptroller.address, bComptroller.address); + //console.log(tokenInfo[tokenInfoKeys.btoken]); +} + +async function init() { + try { + web3.setProvider(new web3.providers.WebsocketProvider("ws://localhost:8545")); + + // NOTICE: Keeping these two contracs here so that we dont need to re-create snapshot + // in case any changes are there in both of the contracts. + // Deploy LiquidatorInfo + liquidatorInfo = await LiquidatorInfo.new(); + console.log("LiquidatorInfo: " + liquidatorInfo.address); + // Deploy UserInfo + userInfo = await UserInfo.new(); + console.log("UserInfo: " + userInfo.address); + + // Deploy UniswapAnchoredView Contract + // Set this as Oracle + } catch (error) { + console.log(error); + } +} + +async function validateCompoundDeployment() { + try { + console.log("====================== Compound Contracts ======================"); + comptroller = await Comptroller.at(json.compound.Comptroller); + console.log("Comptroller: " + comptroller.address); + + comp = await Comptroller.at(json.compound.Comp); + console.log("Comp: " + comp.address); + + //oracle = await FakePriceOracle.at(json.compound.PriceOracle); + //console.log("Oracle: " + oracle.address); + + // Tokens + cETH = await CEther.at(json.compound.cETH); + console.log("cETH: " + cETH.address); + bETH = await BEther.at(await bComptroller.c2b(cETH.address)); + console.log("bETH: " + bETH.address); + + ZRX = await ERC20Detailed.at(json.compound.ZRX); + console.log("ZRX: " + ZRX.address); + cZRX = await CErc20.at(json.compound.cZRX); + console.log("cZRX: " + cZRX.address); + bZRX = await BErc20.at(await bComptroller.c2b(cZRX.address)); + console.log("bZRX: " + bZRX.address); + + USDT = await ERC20Detailed.at(json.compound.USDT); + console.log("USDT: " + USDT.address); + cUSDT = await CErc20.at(json.compound.cUSDT); + console.log("cUSDT: " + cUSDT.address); + bUSDT = await BErc20.at(await bComptroller.c2b(cUSDT.address)); + console.log("bUSDT: " + bUSDT.address); + + BAT = await ERC20Detailed.at(json.compound.BAT); + console.log("BAT: " + BAT.address); + cBAT = await CErc20.at(json.compound.cBAT); + console.log("cBAT: " + cBAT.address); + bBAT = await BErc20.at(await bComptroller.c2b(cBAT.address)); + console.log("bBAT: " + bBAT.address); + + WBTC = await ERC20Detailed.at(json.compound.WBTC); + console.log("WBTC: " + WBTC.address); + cWBTC = await CErc20.at(json.compound.cWBTC); + console.log("cWBTC: " + cWBTC.address); + bWBTC = await BErc20.at(await bComptroller.c2b(cWBTC.address)); + console.log("bWBTC: " + bWBTC.address); + } catch (error) { + console.log(error); + } +} + +async function validateBCompoundDeployment() { + try { + console.log("====================== BCompound Contracts ======================"); + registry = await Registry.at(json.bcompound.Registry); + console.log("Registry: " + registry.address); + + bComptroller = await BComptroller.at(json.bcompound.BComptroller); + console.log("BComptroller: " + bComptroller.address); + + pool = await Pool.at(json.bcompound.Pool); + console.log("Pool: " + pool.address); + } catch (error) { + console.log(error); + } +} + +async function processBlock() { + try { + // fetch all avatars + // const numOfAvatars = await liquidatorInfo.getNumAvatars(pool.address); + // console.log("avatar length: " + numOfAvatars); + // const avatars = await registry.avatarList(); + // for (let i = 0; i < avatars.length; i++) { + // const avatar = avatars[i]; + + // // const bcompound = json.bcompound; + // // await liquidatorInfo.getCushionInfo.call( + // // bcompound.Registry, + // // bcompound.BComptroller, + // // bcompound.Pool, + // // tokenInfo[tokenInfoKeys.ctoken], + // // tokenInfo[tokenInfoKeys.underlyingPrice], + // // ZERO, + // // ZERO, + // // avatar, + // // MEMBER_1, + // // ); + + // //const avatarInfo = await getAvatarInfo(avatar); + // //console.log(avatarInfo); + + // // const liqInfo = await liquidatorInfo.getLiquidationInfo.call(avatar); + // // console.log(liqInfo[liquidationInfoKeys.remainingLiquidationSize].toString()); + + // // await processAvatar(avatar); + // } + + // get all AccountInfo + const numOfAvatars = await liquidatorInfo.getNumAvatars(pool.address); + if (numOfAvatars.gt(ZERO)) { + const startIndex = ZERO; + const endIndex = numOfAvatars.sub(new BN(1)); + const cTokens = tokenInfo[tokenInfoKeys.ctoken]; + const priceFeeds = tokenInfo[tokenInfoKeys.underlyingPrice]; + const accountInfoArr = await liquidatorInfo.getInfo.call( + startIndex, + endIndex, + MEMBER_1, + pool.address, + cTokens, + priceFeeds, + ); + console.log("AccountInfo fetched for accounts: " + accountInfoArr.length); + for (let i = 0; i < accountInfoArr.length; i++) { + if (!pendingMap.get(i)) { + const accInfo = accountInfoArr[i]; + pendingMap.set(i, true); + await processAccountInfo(accInfo); + pendingMap.set(i, false); + } else { + console.log("skipping account: " + i); + } + } + } + } catch (error) { + console.log(error); + } +} + +async function processAccountInfo(accInfo) { + try { + // get liquidity + const avatarInfo = accInfo[accountInfoKeys.avatarInfo]; + const user = avatarInfo[avatarInfoKeys.user]; + const avatar = await registry.avatarOf(user); + let liquidity; + let shortFall; + [liquidity, shortFall] = await getAccountLiquidity(user); + + console.log("liquidity: " + liquidity.toString()); + console.log("shortFall: " + shortFall.toString()); + + // If an avatar has low liquidity allow a member to topup + if (liquidity.gt(ZERO) && liquidity.lt(MIN_LIQUIDITY)) { + let zrxPrice = await oracle.getUnderlyingPrice(cZRX.address); + console.log("## before zrxPrice: " + zrxPrice.toString()); + console.log("## before liquidity: " + liquidity.toString()); + console.log("## before shortFall: " + shortFall.toString()); + + await updatePrice(); + // get liquidity again + [liquidity, shortFall] = await getAccountLiquidity(user); + // get accInfo again + accInfo = await getSingleAccountInfo(avatar); + + zrxPrice = await oracle.getUnderlyingPrice(cZRX.address); + console.log("## after zrxPrice: " + zrxPrice.toString()); + console.log("## after liquidity: " + liquidity.toString()); + console.log("## after shortFall: " + shortFall.toString()); + } + + // If avatar can be liquidated after price update. Then member performs liquidation. + if (shortFall.gt(ZERO)) { + console.log("shortFall: " + shortFall.toString()); + const cushionInfo = accInfo[accountInfoKeys.cushionInfo]; + const shouldTopup = cushionInfo[cushionInfoKeys.shouldTopup]; + const hasCushion = cushionInfo[cushionInfoKeys.hasCushion]; + console.log("shouldTopup: " + shouldTopup); + console.log("hasCushion: " + hasCushion); + // not has cushion && shouldTopup + if (!hasCushion && shouldTopup) { + await memberDepositAndTopup(accInfo); + await liquidate(accInfo); + } + } + } catch (error) { + console.log(error); + } +} + +async function getSingleAccountInfo(avatar) { + const bcompound = json.bcompound; + return await liquidatorInfo.getSingleAccountInfo.call( + bcompound.Pool, + bcompound.Registry, + bcompound.BComptroller, + MEMBER_1, + avatar, + tokenInfo[tokenInfoKeys.ctoken], + tokenInfo[tokenInfoKeys.underlyingPrice], + ); +} + +async function getAccountLiquidity(user) { + const accountLiquidity = await bComptroller.getAccountLiquidity(user); + const liquidity = accountLiquidity["liquidity"]; + const shortFall = accountLiquidity["shortFall"]; + return [liquidity, shortFall]; +} + +async function updatePrice() { + await postPrices(); + console.log(await comptroller.oracle()); + console.log(oracle.address); + // load latest prices again + await loadTokens(); +} + +async function memberDepositAndTopup(accInfo) { + console.log("member depositing ..."); + const avatarInfo = accInfo[accountInfoKeys.avatarInfo]; + const user = avatarInfo[avatarInfoKeys.user]; + const debtInfo = await pool.getDebtTopupInfo.call(user, bZRX.address); + const minTopup = debtInfo["minTopup"]; + console.log("minTopup: " + minTopup.toString()); + await ZRX.approve(pool.address, minTopup, { from: MEMBER_1 }); + await pool.methods["deposit(address,uint256)"](ZRX.address, minTopup, { + from: MEMBER_1, + }); + + // member topup + console.log("member doing topup ..."); + await pool.topup(user, bZRX.address, minTopup, false, { from: MEMBER_1 }); +} + +async function liquidate(accInfo) { + console.log("member liquidating ..."); + const avatarInfo = accInfo[accountInfoKeys.avatarInfo]; + const user = avatarInfo[avatarInfoKeys.user]; + const avatar_addr = await registry.avatarOf(user); + const avatar = await Avatar.at(avatar_addr); + // Check if an avatar has low liquidity and after price update + + // avatar will have short-fall and it can be liquidated + // deposit & Liquidate + const debtInfo = await pool.getDebtTopupInfo.call(user, bZRX.address); + const minTopup = debtInfo["minTopup"]; + const maxLiquidationAmt = await avatar.getMaxLiquidationAmount.call(cZRX.address); + const remainingBalToDeposit = maxLiquidationAmt.sub(minTopup); + await ZRX.approve(pool.address, remainingBalToDeposit, { from: MEMBER_1 }); + await pool.methods["deposit(address,uint256)"](ZRX.address, remainingBalToDeposit, { + from: MEMBER_1, + }); + + await pool.liquidateBorrow(user, bETH.address, bZRX.address, maxLiquidationAmt, { + from: MEMBER_1, + }); + console.log("member liquidated successfully."); +} + +async function getAvatarInfo( + avatar, + ctoken = tokenInfo[tokenInfoKeys.ctoken], + priceFeed = tokenInfo[tokenInfoKeys.underlyingPrice], +) { + return await liquidatorInfo.getAvatarInfo.call( + json.bcompound.Registry, + json.bcompound.BComptroller, + ctoken, + priceFeed, + avatar, + ); +} + +async function postPrices() { + console.log("in postPrices"); + + console.log("get signed prices from open-oracle-reporter ..."); + console.log(process.env.COINBASE_SECRET); + console.log(process.env.COINBASE_APIKEY); + console.log(process.env.COINBASE_PHRASE); + const path = "/prices.json"; + + let timestamp = Date.now() / 1000; + let method = "GET"; + // create the prehash string by concatenating required parts + let what = timestamp + method + path; + // decode the base64 secret + let key = Buffer.from(process.env.COINBASE_SECRET, "base64"); + // create a sha256 hmac with the secret + let hmac = crypto.createHmac("sha256", key); + // sign the require message with the hmac + // and finally base64 encode the result + let signature = hmac.update(what).digest("base64"); + let headers = { + "CB-ACCESS-KEY": process.env.COINBASE_APIKEY, + "CB-ACCESS-SIGN": signature, + "CB-ACCESS-TIMESTAMP": timestamp, + "CB-ACCESS-PASSPHRASE": process.env.COINBASE_PHRASE, + "Content-Type": "application/json", + }; + + const res = await axios.get("http://localhost:3000" + path, { + headers, + }); + + //console.log(res.data); + const messages = res.data.messages; + const signatures = res.data.signatures; + + await oracle.postPrices(messages, signatures, symbols); + console.log("total " + messages.length + " prices posted."); +} diff --git a/test/bot/testBot.ts b/test/bot/testBot.ts new file mode 100644 index 00000000..513a539e --- /dev/null +++ b/test/bot/testBot.ts @@ -0,0 +1,380 @@ +import * as b from "../../types/index"; +import * as json from "../../playground/bcompound.json"; +import BN from "bn.js"; + +import { BAccounts } from "../../test-utils/BAccounts"; +import { BProtocolEngine, BProtocol } from "../../test-utils/BProtocolEngine"; +import { takeSnapshot, revertToSnapShot } from "../../test-utils/SnapshotUtils"; +const { balance, expectEvent, expectRevert, time } = require("@openzeppelin/test-helpers"); +const { ZERO_ADDRESS } = require("@openzeppelin/test-helpers/src/constants"); + +const chai = require("chai"); +const expect = chai.expect; + +const ZERO = new BN(0); + +// Compound +const Comptroller: b.ComptrollerContract = artifacts.require("Comptroller"); +const Comp: b.CompContract = artifacts.require("Comp"); +const CErc20: b.CErc20Contract = artifacts.require("CErc20"); +const CEther: b.CEtherContract = artifacts.require("CEther"); +const ERC20Detailed: b.Erc20DetailedContract = artifacts.require("ERC20Detailed"); +const FakePriceOracle: b.FakePriceOracleContract = artifacts.require("FakePriceOracle"); + +// BCompound +const BComptroller: b.BComptrollerContract = artifacts.require("BComptroller"); +const GovernanceExecutor: b.GovernanceExecutorContract = artifacts.require("GovernanceExecutor"); +const CompoundJar: b.CompoundJarContract = artifacts.require("CompoundJar"); +const JarConnector: b.JarConnectorContract = artifacts.require("JarConnector"); +const Migrate: b.MigrateContract = artifacts.require("Migrate"); +const Pool: b.PoolContract = artifacts.require("Pool"); +const Registry: b.RegistryContract = artifacts.require("Registry"); +const BScore: b.BScoreContract = artifacts.require("BScore"); +const BErc20: b.BErc20Contract = artifacts.require("BErc20"); +const BEther: b.BEtherContract = artifacts.require("BEther"); +const Avatar: b.AvatarContract = artifacts.require("Avatar"); + +let engine: BProtocolEngine; +let a: BAccounts; + +// Compound +let comptroller: b.ComptrollerInstance; +let comp: b.CompInstance; +//let oracle: b.FakePriceOracleInstance; + +// BProtocol +let registry: b.RegistryInstance; +let bComptroller: b.BComptrollerInstance; +let governanceExecutor: b.GovernanceExecutorInstance; +let compoundJar: b.CompoundJarInstance; +let jarConnector: b.JarConnectorInstance; +let migrate: b.MigrateInstance; +let pool: b.PoolInstance; +let bScore: b.BScoreInstance; + +// ETH +let cETH: b.CEtherInstance; +let bETH: b.BEtherInstance; + +// ZRX +let ZRX: b.Erc20DetailedInstance; +let cZRX: b.CErc20Instance; +let bZRX: b.BErc20Instance; + +// BAT +let BAT: b.Erc20DetailedInstance; +let cBAT: b.CErc20Instance; +let bBAT: b.BErc20Instance; + +// USDT +let USDT: b.Erc20DetailedInstance; +let cUSDT: b.CErc20Instance; +let bUSDT: b.BErc20Instance; + +// WBTC +let WBTC: b.Erc20DetailedInstance; +let cWBTC: b.CErc20Instance; +let bWBTC: b.BErc20Instance; + +// VALUES +const SCALE = new BN(10).pow(new BN(18)); +const ONE_ETH = new BN(10).pow(new BN(18)); +const ONE_ZRX = new BN(10).pow(new BN(18)); +const ONE_BAT = new BN(10).pow(new BN(18)); +const ONE_USDT = new BN(10).pow(new BN(6)); +const ONE_WBTC = new BN(10).pow(new BN(8)); + +// MAINNET DATA +// ============= +// TOKEN PRICES +const ONE_ETH_IN_USD_MAINNET = new BN("1617455000000000000000"); // 18 decimals, ($1617.45) +const ONE_ZRX_IN_USD_MAINNET = new BN("1605584000000000000"); // 18 decimal, ($1.6) +const ONE_USDT_IN_USD_MAINNET = new BN("1000000000000000000000000000000"); //30 decimals, ($1) +const ONE_BAT_IN_USD_MAINNET = new BN("409988000000000000"); // 18 decimals, ($0.4) +const ONE_WBTC_IN_USD_MAINNET = new BN("392028400000000000000000000000000"); // 28 decimals, ($39202.8) + +contract("PlayGround", async (accounts) => { + engine = new BProtocolEngine(accounts); + a = new BAccounts(accounts); + + let snapshotId: string; + + beforeEach(async () => { + snapshotId = await takeSnapshot(); + }); + + afterEach(async () => { + await revertToSnapShot(snapshotId); + }); + + before(async () => { + // ensure that Compound contracts are already deployed + await validateCompoundDeployed(); + + await validateBCompoundDeployed(); + + await validateBTokens(); + + //await setMainnetTokenPrice(); + + await printDetails(); + }); + + describe("PlayGround", async () => { + it("Test liquidateBorrow by member", async () => { + const oracle_addr = await comptroller.oracle(); + const oracle = await FakePriceOracle.at(oracle_addr); + console.log((await oracle.getUnderlyingPrice(cZRX.address)).toString()); + + const ONE_USD = SCALE; + const _50Percent = SCALE.div(new BN(2)); // 50% + // validate values + const closeFactor = await comptroller.closeFactorMantissa(); + expect(closeFactor).to.be.bignumber.equal(_50Percent); + const market = await comptroller.markets(cETH.address); + expect(market["collateralFactorMantissa"]).to.be.bignumber.equal(_50Percent); + + // deployer sends ZRX to user2 + const ONE_THOUSAND_ZRX = ONE_ZRX.mul(new BN(1000)); + await ZRX.transfer(a.user2, ONE_THOUSAND_ZRX, { from: a.deployer }); + + // user2 mints ZRX to provide liquidity to market + await ZRX.approve(bZRX.address, ONE_THOUSAND_ZRX, { from: a.user2 }); + await bZRX.mint(ONE_THOUSAND_ZRX, { from: a.user2 }); + expect(await bZRX.balanceOf(a.user2)).to.be.bignumber.greaterThan(ZERO); + + // user1 mint ETH + await bETH.mint({ from: a.user1, value: ONE_ETH }); // $1617.45 collateral + expect(await bETH.balanceOf(a.user1)).to.be.bignumber.greaterThan(ZERO); + + // $1617.45 * 50% = $808.725 + // Borrow $808 worth of ZRX + // user1 borrow ZRX + const ONE_USD_WORTH_OF_ZRX = ONE_ZRX.mul(SCALE).div(ONE_ZRX_IN_USD_MAINNET); + const _808USD_WO_ZRX = ONE_USD_WORTH_OF_ZRX.mul(new BN(808)); + await bZRX.borrow(_808USD_WO_ZRX, { from: a.user1 }); + expect(await bZRX.borrowBalanceCurrent.call(a.user1)).to.be.bignumber.greaterThan(ZERO); + + const avatar1 = await Avatar.at(await registry.avatarOf(a.user1)); + + let accLiquidity = await bComptroller.getAccountLiquidity(a.user1); + const prevLiquidity = accLiquidity["liquidity"]; + // // validate account liquidity + // let accLiquidity = await bComptroller.getAccountLiquidity(a.user1); + // expect(accLiquidity["err"]).to.be.bignumber.equal(ZERO); + // expect(accLiquidity["liquidity"]).to.be.bignumber.lessThan(ONE_USD); + // expect(accLiquidity["shortFall"]).to.be.bignumber.equal(ZERO); + + console.log((await oracle.getUnderlyingPrice(cZRX.address)).toString()); + + console.log("Bot should topup, change-price, liquidate"); + await sleep(10000); + + console.log((await oracle.getUnderlyingPrice(cZRX.address)).toString()); + // BOT will update the ZRX Price + // validate that Bot updated price + + await time.advanceBlock(); + + console.log((await oracle.getUnderlyingPrice(cZRX.address)).toString()); + + console.log("Bot should topup, change-price, liquidate"); + await sleep(10000); + + console.log((await oracle.getUnderlyingPrice(cZRX.address)).toString()); + + accLiquidity = await bComptroller.getAccountLiquidity(a.user1); + const newLiquidity = accLiquidity["liquidity"]; + expect(prevLiquidity).to.be.bignumber.not.equal(newLiquidity); + console.log("prevLiquidity: " + prevLiquidity.toString()); + console.log("newLiquidity: " + newLiquidity.toString()); + + console.log((await oracle.getUnderlyingPrice(cZRX.address)).toString()); + + /* + // increase ZRX price by 5% + const new_ONE_ZRX_IN_USD_MAINNET = ONE_ZRX_IN_USD_MAINNET.mul(new BN(105)).div(new BN(100)); + await oracle.setPrice(cZRX.address, new_ONE_ZRX_IN_USD_MAINNET); + + // validate account liquidity + accLiquidity = await bComptroller.getAccountLiquidity(a.user1); + expect(accLiquidity["err"]).to.be.bignumber.equal(ZERO); + expect(accLiquidity["liquidity"]).to.be.bignumber.equal(ZERO); + expect(accLiquidity["shortFall"]).to.be.bignumber.greaterThan(ZERO); // shortFall increased + */ + + // wait for bot to deposit and topup + + // wait for bot to deposit and liquidate + /* + // member deposit + const debtInfo = await pool.getDebtTopupInfo.call(a.user1, bZRX.address); + const minTopup = debtInfo["minTopup"]; + await ZRX.approve(pool.address, minTopup, { from: a.member1 }); + await pool.methods["deposit(address,uint256)"](ZRX.address, minTopup, { + from: a.member1, + }); + + // member topup + await pool.topup(a.user1, bZRX.address, minTopup, false, { from: a.member1 }); + + // deposit & Liquidate + const maxLiquidationAmt = await avatar1.getMaxLiquidationAmount.call(cZRX.address); + const remainingBalToDeposit = maxLiquidationAmt.sub(minTopup); + await ZRX.approve(pool.address, remainingBalToDeposit, { from: a.member1 }); + await pool.methods["deposit(address,uint256)"](ZRX.address, remainingBalToDeposit, { + from: a.member1, + }); + + await pool.liquidateBorrow(a.user1, bETH.address, bZRX.address, maxLiquidationAmt, { + from: a.member1, + }); + */ + }); + }); +}); + +async function printDetails() { + // Compound + // ========== + console.log("==============="); + console.log("Compound"); + console.log("==============="); + + console.log("------------------------"); + console.log("Deployer Token Balances:"); + console.log("------------------------"); + await printAllTokenBalanceOf(a.deployer); + + console.log("------------------------"); + console.log("Member-1 Token Balances:"); + console.log("------------------------"); + await printAllTokenBalanceOf(a.member1); + + console.log("------------------------"); + console.log("Member-2 Token Balances:"); + console.log("------------------------"); + await printAllTokenBalanceOf(a.member2); + + console.log("------------------------"); + console.log("Member-3 Token Balances:"); + console.log("------------------------"); + await printAllTokenBalanceOf(a.member3); + + console.log("------------------------"); + console.log("Member-4 Token Balances:"); + console.log("------------------------"); + await printAllTokenBalanceOf(a.member4); + + // BProtocol + // ========== + console.log("==============="); + console.log("BProtocol"); + console.log("==============="); + console.log("Members: " + (await pool.getMembers())); + console.log("Members length: " + (await pool.membersLength())); +} + +async function printAllTokenBalanceOf(user: string) { + await printTokenBalanceOf(ZRX, user); + await printTokenBalanceOf(BAT, user); + await printTokenBalanceOf(USDT, user); + await printTokenBalanceOf(WBTC, user); +} + +async function printTokenBalanceOf(token: b.Erc20DetailedInstance, user: string) { + const symbol = await token.symbol(); + const userBalance = await token.balanceOf(user); + const decimals = await token.decimals(); + const ONE_TOKEN = new BN(10).pow(new BN(decimals)); + console.log(symbol + " Balance: " + userBalance + "(" + userBalance.div(ONE_TOKEN) + ")"); +} + +async function validateBTokens() { + bETH = await BEther.at(await bComptroller.c2b(cETH.address)); + expect(bETH.address).to.be.not.equal(ZERO_ADDRESS); + bZRX = await BErc20.at(await bComptroller.c2b(cZRX.address)); + expect(bZRX.address).to.be.not.equal(ZERO_ADDRESS); + bBAT = await BErc20.at(await bComptroller.c2b(cBAT.address)); + expect(bBAT.address).to.be.not.equal(ZERO_ADDRESS); + bUSDT = await BErc20.at(await bComptroller.c2b(cUSDT.address)); + expect(bUSDT.address).to.be.not.equal(ZERO_ADDRESS); + bWBTC = await BErc20.at(await bComptroller.c2b(cWBTC.address)); + expect(bWBTC.address).to.be.not.equal(ZERO_ADDRESS); +} + +async function validateCompoundDeployed() { + console.log("Validating Compound Contracts ..."); + comptroller = await Comptroller.at(json.compound.Comptroller); + expect(comptroller.address).to.be.not.equal(ZERO_ADDRESS); + + comp = await Comp.at(json.compound.Comp); + expect(comp.address).to.be.not.equal(ZERO_ADDRESS); + + // oracle = await FakePriceOracle.at(json.compound.PriceOracle); + // expect(oracle.address).to.be.not.equal(ZERO_ADDRESS); + + // cTokens and Underlying + cETH = await CEther.at(json.compound.cETH); + expect(cETH.address).to.be.not.equal(ZERO_ADDRESS); + + ZRX = await ERC20Detailed.at(json.compound.ZRX); + expect(ZRX.address).to.be.not.equal(ZERO_ADDRESS); + cZRX = await CErc20.at(json.compound.cZRX); + expect(cZRX.address).to.be.not.equal(ZERO_ADDRESS); + + BAT = await ERC20Detailed.at(json.compound.BAT); + expect(BAT.address).to.be.not.equal(ZERO_ADDRESS); + cBAT = await CErc20.at(json.compound.cBAT); + expect(cBAT.address).to.be.not.equal(ZERO_ADDRESS); + + USDT = await ERC20Detailed.at(json.compound.USDT); + expect(USDT.address).to.be.not.equal(ZERO_ADDRESS); + cUSDT = await CErc20.at(json.compound.cUSDT); + expect(cUSDT.address).to.be.not.equal(ZERO_ADDRESS); + + WBTC = await ERC20Detailed.at(json.compound.WBTC); + expect(WBTC.address).to.be.not.equal(ZERO_ADDRESS); + cWBTC = await CErc20.at(json.compound.cWBTC); + expect(cWBTC.address).to.be.not.equal(ZERO_ADDRESS); +} + +async function validateBCompoundDeployed() { + console.log("Validating BCompound Contracts ..."); + bComptroller = await BComptroller.at(json.bcompound.BComptroller); + expect(bComptroller.address).to.be.not.equal(ZERO_ADDRESS); + + governanceExecutor = await GovernanceExecutor.at(json.bcompound.GovernanceExecutor); + expect(governanceExecutor.address).to.be.not.equal(ZERO_ADDRESS); + + compoundJar = await CompoundJar.at(json.bcompound.CompoundJar); + expect(compoundJar.address).to.be.not.equal(ZERO_ADDRESS); + + jarConnector = await JarConnector.at(json.bcompound.JarConnector); + expect(jarConnector.address).to.be.not.equal(ZERO_ADDRESS); + + migrate = await Migrate.at(json.bcompound.Migrate); + expect(migrate.address).to.be.not.equal(ZERO_ADDRESS); + + pool = await Pool.at(json.bcompound.Pool); + expect(pool.address).to.be.not.equal(ZERO_ADDRESS); + + registry = await Registry.at(json.bcompound.Registry); + expect(registry.address).to.be.not.equal(ZERO_ADDRESS); + + bScore = await BScore.at(json.bcompound.BScore); + expect(bScore.address).to.be.not.equal(ZERO_ADDRESS); +} + +// async function setMainnetTokenPrice() { +// // mainnet snapshot prices +// await oracle.setPrice(cETH.address, ONE_ETH_IN_USD_MAINNET); +// await oracle.setPrice(cZRX.address, ONE_ZRX_IN_USD_MAINNET); +// await oracle.setPrice(cUSDT.address, ONE_USDT_IN_USD_MAINNET); +// await oracle.setPrice(cBAT.address, ONE_BAT_IN_USD_MAINNET); +// await oracle.setPrice(cWBTC.address, ONE_WBTC_IN_USD_MAINNET); +// } + +function sleep(ms: any) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/truffle-config.js b/truffle-config.js index bb5ac1af..92ba2f8c 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -13,7 +13,7 @@ module.exports = { development: { host: "127.0.0.1", port: 8545, - gas: 1250000000000, + gas: 0xfffffffffff, gasPrice: 1, network_id: "*", disableConfirmationListener: true,