From df4531c505e760bce3a523a2897cd5855ade6a47 Mon Sep 17 00:00:00 2001 From: Benjamin Hughes <32072172+bghughes@users.noreply.github.com> Date: Sat, 25 Sep 2021 12:53:27 -0500 Subject: [PATCH] Rubicon Protocol v1.1 (#24) * clean * clean * 1.1 * clean v1.1 * truffle.yml update * OVM Proxy * clean --- .github/workflows/truffle.yml | 2 +- contracts/interfaces/IWETH.sol | 13 - .../peripheral_contracts/EquityToken.sol | 22 - contracts/proxy/OVMProxy.sol | 5 +- .../proxy/TransparentUpgradeableProxy.sol | 2 +- contracts/rubiconPools/BathPair.sol | 274 ++++----- contracts/rubiconPools/BathToken.sol | 9 +- hardhat.config.ts | 93 ++- migrations/1_deploy_asset_contracts.js | 9 + migrations/2_deploy_asset_contracts.js | 28 - ...e_and_pools.js => 2_exchange_and_pools.js} | 15 - strategist/StrategistTutorialComingSoon.txt | 1 + strategist/kovanPoolsStrategist.js | 550 ------------------ strategist/nonceManager/example.js | 29 - test/{3_pool_test.js => 1_pool_test.js} | 29 +- 15 files changed, 233 insertions(+), 848 deletions(-) delete mode 100644 contracts/interfaces/IWETH.sol delete mode 100644 contracts/peripheral_contracts/EquityToken.sol create mode 100644 migrations/1_deploy_asset_contracts.js delete mode 100644 migrations/2_deploy_asset_contracts.js rename migrations/{3_exchange_and_pools.js => 2_exchange_and_pools.js} (58%) create mode 100644 strategist/StrategistTutorialComingSoon.txt delete mode 100644 strategist/kovanPoolsStrategist.js delete mode 100644 strategist/nonceManager/example.js rename test/{3_pool_test.js => 1_pool_test.js} (94%) diff --git a/.github/workflows/truffle.yml b/.github/workflows/truffle.yml index 9173426..8130b78 100644 --- a/.github/workflows/truffle.yml +++ b/.github/workflows/truffle.yml @@ -15,7 +15,7 @@ jobs: steps: # Checks out a copy of your repository on the ubuntu-latest machine - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v1.1.0 - name: Use Node.js uses: actions/setup-node@v1 diff --git a/contracts/interfaces/IWETH.sol b/contracts/interfaces/IWETH.sol deleted file mode 100644 index d83335c..0000000 --- a/contracts/interfaces/IWETH.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity >=0.7.6; - -interface IWETH { - function deposit() external payable; - - function transfer(address to, uint256 value) external returns (bool); - - function withdraw(uint256) external; - - function approve(address guy, uint256 wad) external returns (bool); -} diff --git a/contracts/peripheral_contracts/EquityToken.sol b/contracts/peripheral_contracts/EquityToken.sol deleted file mode 100644 index 9652455..0000000 --- a/contracts/peripheral_contracts/EquityToken.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity ^0.7.6; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -// https://github.com/ethereum/eips/issues/1404 -// contract ERC1404 is ERC20 { -// function detectTransferRestriction (address from, address to, uint256 value) public view returns (uint8); -// function messageForTransferRestriction (uint8 restrictionCode) public view returns (string memory); -// } - -contract EquityToken is ERC20 { - constructor( - address admin, - uint256 initialSupply, - string memory _name, - string memory _symbol - ) ERC20(_name, _symbol) { - _mint(admin, initialSupply); - } -} diff --git a/contracts/proxy/OVMProxy.sol b/contracts/proxy/OVMProxy.sol index 4614898..2dda022 100644 --- a/contracts/proxy/OVMProxy.sol +++ b/contracts/proxy/OVMProxy.sol @@ -3,7 +3,7 @@ pragma solidity >=0.6.0 <0.8.0; /** - * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM + * @dev This abstract contract provides an OVM-safe fallback function that delegates all calls to another contract using the EVM * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to * be specified by overriding the virtual {_implementation} function. * @@ -14,12 +14,11 @@ pragma solidity >=0.6.0 <0.8.0; */ abstract contract OVMProxy { /** - * @dev Delegates the current call to `implementation`. + * @dev Delegates the current call to `implementation` with OVM-safe logic. * * This function does not return to its internall call site, it will return directly to the external caller. */ function _delegate(address implementation) internal virtual { - // solhint-disable-next-line no-inline-assembly (bool success, bytes memory returndata) = implementation.delegatecall( msg.data ); diff --git a/contracts/proxy/TransparentUpgradeableProxy.sol b/contracts/proxy/TransparentUpgradeableProxy.sol index 84dbfbc..b4eb6bd 100644 --- a/contracts/proxy/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/TransparentUpgradeableProxy.sol @@ -48,7 +48,7 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { event AdminChanged(address previousAdmin, address newAdmin); /** - * @dev Emitted when the admin calls implementation() + * @dev Emitted when the admin calls implementation() and provides a way to publicly call the implementation's address */ event Implementation(address currentImplementation); diff --git a/contracts/rubiconPools/BathPair.sol b/contracts/rubiconPools/BathPair.sol index 5f7fc7f..8fd4cd6 100644 --- a/contracts/rubiconPools/BathPair.sol +++ b/contracts/rubiconPools/BathPair.sol @@ -112,11 +112,11 @@ contract BathPair { maxOrderSizeBPS = _maxOrderSizeBPS; shapeCoefNum = _shapeCoefNum; start = 0; - searchRadius = 3; + searchRadius = 2; initialized = true; } - modifier onlyBathHouse { + modifier onlyBathHouse() { require(msg.sender == bathHouse); _; } @@ -129,7 +129,7 @@ contract BathPair { _; } - modifier enforceReserveRatio { + modifier enforceReserveRatio() { _; require( ( @@ -179,8 +179,7 @@ contract BathPair { return searchRadius; } - // ** Internal Functions ** - // Takes the proposed bid and ask as a parameter - that the offers placed won't match and are maker orders + // *** Internal Functions *** function getMidpointPrice() internal view returns (int128) { address _RubiconMarketAddress = RubiconMarketAddress; @@ -209,40 +208,6 @@ contract BathPair { return midpoint; } - // Returns filled liquidity to the correct bath pool - function rebalancePair( - address _bathHouse, - address _underlyingAsset, - address _underlyingQuote - ) internal { - address _bathAssetAddress = bathAssetAddress; - address _bathQuoteAddress = bathQuoteAddress; - uint256 bathAssetYield = ERC20(_underlyingQuote).balanceOf( - _bathAssetAddress - ); - uint256 bathQuoteYield = ERC20(_underlyingAsset).balanceOf( - _bathQuoteAddress - ); - uint16 stratReward = BathHouse(_bathHouse).getBPSToStrats( - address(this) - ); - if (bathAssetYield > 0) { - BathToken(_bathAssetAddress).rebalance( - _bathQuoteAddress, - _underlyingQuote, - stratReward - ); - } - - if (bathQuoteYield > 0) { - BathToken(_bathQuoteAddress).rebalance( - _bathAssetAddress, - _underlyingAsset, - stratReward - ); - } - } - // orderID of the fill // only log fills for each strategist - needs to be asset specific // isAssetFill are *quotes* that result in asset yield @@ -268,91 +233,51 @@ contract BathPair { outstandingPairIDs.pop(); } - function cancelPartialFills() internal { - uint256 timeDelay = BathHouse(bathHouse).timeDelay(); - uint256 len = outstandingPairIDs.length; - uint256 _start = start; - - uint256 _searchRadius = searchRadius; - if (_start + _searchRadius >= len) { - // start over from beggining - if (_searchRadius >= len) { - _start = 0; - _searchRadius = len; - } else { - _searchRadius = len - _start; + function handleStratOrderAtIndex(uint256 index) internal { + StrategistTrade memory info = outstandingPairIDs[index]; + order memory offer1 = getOfferInfo(info.askId); //ask + order memory offer2 = getOfferInfo(info.bidId); //bid + uint256 askDelta = info.askAmt - offer1.pay_amt; + uint256 bidDelta = info.bidAmt - offer2.pay_amt; + + // if real + if (info.askId != 0) { + // if delta > 0 - delta is fill => handle any amount of fill here + if (askDelta > 0) { + logFill(askDelta, true, info.strategist); + BathToken(bathAssetAddress).removeFilledTradeAmount(askDelta); + // not a full fill + if (askDelta != info.askAmt) { + BathToken(bathAssetAddress).cancel( + info.askId, + info.askAmt.sub(askDelta) + ); + } + } + // otherwise didn't fill so cancel + else { + BathToken(bathAssetAddress).cancel(info.askId, info.askAmt); // pas amount too } } - for (uint256 x = _start; x < _start + _searchRadius; x++) { - if ( - outstandingPairIDs[x].timestamp < (block.timestamp - timeDelay) - ) { - StrategistTrade memory info = outstandingPairIDs[x]; - order memory offer1 = getOfferInfo(info.askId); //ask - order memory offer2 = getOfferInfo(info.bidId); //bid - uint256 askDelta = info.askAmt - offer1.pay_amt; - uint256 bidDelta = info.bidAmt - offer2.pay_amt; - - // if real - if (info.askId != 0) { - // if delta > 0 - delta is fill => handle any amount of fill here - if (askDelta > 0) { - logFill(askDelta, true, info.strategist); - BathToken(bathAssetAddress).removeFilledTradeAmount( - askDelta - ); - // not a full fill - if (askDelta != info.askAmt) { - BathToken(bathAssetAddress).cancel( - info.askId, - info.askAmt.sub(askDelta) - ); - } - } - // otherwise didn't fill so cancel - else { - BathToken(bathAssetAddress).cancel( - info.askId, - info.askAmt - ); // pas amount too - } - } - - // if real - if (info.bidId != 0) { - // if delta > 0 - delta is fill => handle any amount of fill here - if (bidDelta > 0) { - logFill(bidDelta, false, info.strategist); - BathToken(bathQuoteAddress).removeFilledTradeAmount( - bidDelta - ); - // not a full fill - if (bidDelta != info.bidAmt) { - BathToken(bathQuoteAddress).cancel( - info.bidId, - info.bidAmt.sub(bidDelta) - ); - } - } - // otherwise didn't fill so cancel - else { - BathToken(bathQuoteAddress).cancel( - info.bidId, - info.bidAmt - ); // pass amount too - } + // if real + if (info.bidId != 0) { + // if delta > 0 - delta is fill => handle any amount of fill here + if (bidDelta > 0) { + logFill(bidDelta, false, info.strategist); + BathToken(bathQuoteAddress).removeFilledTradeAmount(bidDelta); + // not a full fill + if (bidDelta != info.bidAmt) { + BathToken(bathQuoteAddress).cancel( + info.bidId, + info.bidAmt.sub(bidDelta) + ); } - - removeElement(x); - x--; - _searchRadius--; } - } - if (_start + searchRadius >= len) { - start = 0; - } else { - start = _start + searchRadius; + // otherwise didn't fill so cancel + else { + BathToken(bathQuoteAddress).cancel(info.bidId, info.bidAmt); // pass amount too + } } } @@ -368,7 +293,8 @@ contract BathPair { return offerInfo; } - // ** External functions that can be called by Strategists ** + // *** External functions that can be called by Strategists *** + function executeStrategy( uint256 askNumerator, // Quote / Asset uint256 askDenominator, // Asset / Quote @@ -458,15 +384,100 @@ contract BathPair { outgoing.timestamp, outgoing.strategist ); + } + + // Returns filled liquidity to the correct bath pool - enforce this on permissionless + function rebalancePair() public { + address _bathAssetAddress = bathAssetAddress; + address _bathQuoteAddress = bathQuoteAddress; + address _underlyingAsset = underlyingAsset; + address _underlyingQuote = underlyingQuote; + uint256 bathAssetYield = ERC20(_underlyingQuote).balanceOf( + _bathAssetAddress + ); + uint256 bathQuoteYield = ERC20(_underlyingAsset).balanceOf( + _bathQuoteAddress + ); + uint16 stratReward = BathHouse(bathHouse).getBPSToStrats(address(this)); + if (bathAssetYield > 0) { + BathToken(_bathAssetAddress).rebalance( + _bathQuoteAddress, + _underlyingQuote, + stratReward + ); + } - // Return any filled yield to the appropriate bathToken/liquidity pool - rebalancePair(_bathHouse, _underlyingAsset, _underlyingQuote); + if (bathQuoteYield > 0) { + BathToken(_bathQuoteAddress).rebalance( + _bathAssetAddress, + _underlyingAsset, + stratReward + ); + } } - // This function cleans outstanding orders and rebalances yield between bathTokens + // This function cleans outstanding orders on a time basis and rebalances yield between bathTokens function bathScrub() external { - // Cancel Outstanding Orders that need to be cleared or logged for yield - cancelPartialFills(); + uint256 timeDelay = BathHouse(bathHouse).timeDelay(); + uint256 len = outstandingPairIDs.length; + uint256 _start = start; + + uint256 _searchRadius = searchRadius; + if (_start + _searchRadius >= len) { + // start over from beggining + if (_searchRadius >= len) { + _start = 0; + _searchRadius = len; + } else { + _searchRadius = len - _start; + } + } + + for (uint256 x = _start; x < _start + _searchRadius; x++) { + if ( + outstandingPairIDs[x].timestamp < (block.timestamp - timeDelay) + ) { + handleStratOrderAtIndex(x); + + removeElement(x); + x--; + _searchRadius--; + } + } + if (_start + searchRadius >= len) { + start = 0; + } else { + start = _start + searchRadius; + } + } + + // Inputs are indices through which to scrub + // Zero indexed indices! + function indexScrub(uint8 _start, uint8 _end) + external + onlyApprovedStrategist(msg.sender) + { + uint256 len = outstandingPairIDs.length; + uint256 delta = len - 1 - _end; + require( + _end - _start <= len, + "range of indices too great for outstandingPairs length" + ); + for (uint8 x = _start; x <= _end; x++) { + handleStratOrderAtIndex(x); + + //delta is indices delta from _end through end of array + if (delta > 0) { + removeElement(x); + delta--; + } else if (x < _end) { + removeElement(x); + x--; + _end--; + } else if (x == _end) { + outstandingPairIDs.pop(); + } + } } // Return the largest order size that can be placed as a strategist for given asset and liquidity pool @@ -538,8 +549,9 @@ contract BathPair { } } - // This function allows a strategist to remove Pools liquidity from the order book + // This function allows a strategist to remove Pools liquidity from the order book from a trade id function removeLiquidity(uint256 id) external { + require(id != 0, "cant remove a zero order"); order memory ord = getOfferInfo(id); if (ord.pay_gem == ERC20(underlyingAsset)) { uint256 len = outstandingPairIDs.length; @@ -549,10 +561,7 @@ contract BathPair { msg.sender == outstandingPairIDs[x].strategist, "only strategist can cancel their orders" ); - BathToken(bathAssetAddress).cancel( - id, - outstandingPairIDs[x].askAmt - ); + handleStratOrderAtIndex(x); removeElement(x); break; } @@ -565,10 +574,7 @@ contract BathPair { msg.sender == outstandingPairIDs[x].strategist, "only strategist can cancel their orders" ); - BathToken(bathAssetAddress).cancel( - id, - outstandingPairIDs[x].bidAmt - ); + handleStratOrderAtIndex(x); removeElement(x); break; } diff --git a/contracts/rubiconPools/BathToken.sol b/contracts/rubiconPools/BathToken.sol index 97a7e50..4779293 100644 --- a/contracts/rubiconPools/BathToken.sol +++ b/contracts/rubiconPools/BathToken.sol @@ -239,7 +239,14 @@ contract BathToken { uint256 _fee = r.mul(feeBPS).div(10000); IERC20(underlyingToken).transfer(feeTo, _fee); underlyingToken.transfer(msg.sender, r.sub(_fee)); - emit Withdraw(r.sub(_fee), underlyingToken, _shares, msg.sender, _fee, feeTo); + emit Withdraw( + r.sub(_fee), + underlyingToken, + _shares, + msg.sender, + _fee, + feeTo + ); } // ** Internal Functions ** diff --git a/hardhat.config.ts b/hardhat.config.ts index 869db81..a304693 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,75 +1,70 @@ require("dotenv").config(); -import '@nomiclabs/hardhat-ethers' -import '@nomiclabs/hardhat-waffle' -import 'hardhat-deploy' -import '@eth-optimism/hardhat-ovm' -import '@openzeppelin/hardhat-upgrades' -import "@nomiclabs/hardhat-web3" -import { LedgerSigner } from "@ethersproject/hardware-wallets"; +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-waffle"; +import "hardhat-deploy"; +import "@eth-optimism/hardhat-ovm"; +import "@openzeppelin/hardhat-upgrades"; +import "@nomiclabs/hardhat-web3"; module.exports = { ovm: { - solcVersion: "0.7.6" + solcVersion: "0.7.6", }, solidity: { compilers: [ - { - version:"0.5.16", - settings: { - optimizer: { - enabled: true, - runs: 200 + { + version: "0.5.16", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, }, - } }, { - version:"0.7.6", + version: "0.7.6", settings: { - optimizer: { - enabled: true, - runs: 1 + optimizer: { + enabled: true, + runs: 1, + }, }, - } - } - ] - + }, + ], }, paths: { - tests: "./ovm/ovmTests" + tests: "./ovm/ovmTests", }, networks: { // https://community.optimism.io/docs/developers/integration.html#using-the-optimism-repo - optimismLocal: { - url: 'http://127.0.0.1:8545', - accounts: { - mnemonic: 'test test test test test test test test test test test junk' - }, - ovm: true, - gasPrice: 0, + optimismLocal: { + url: "http://127.0.0.1:8545", + accounts: { + mnemonic: "test test test test test test test test test test test junk", }, - optimismKovan: { - url: 'https://optimism-kovan.infura.io/v3/' + process.env.INFURA_API_KEY, - ovm: true, - gasPrice: 15000000, - chainId: 69, - accounts: [process.env.OP_KOVAN_ADMIN_KEY, process.env.OP_KOVAN_PROXY_ADMIN_KEY], - gasLimit: 18547064, - timeout: 40000 - } + ovm: true, + gasPrice: 0, + }, + optimismKovan: { + url: "https://optimism-kovan.infura.io/v3/" + process.env.INFURA_API_KEY, + ovm: true, + gasPrice: 15000000, + chainId: 69, + accounts: [ + process.env.OP_KOVAN_ADMIN_KEY, + process.env.OP_KOVAN_PROXY_ADMIN_KEY, + ], + gasLimit: 18547064, + timeout: 40000, + }, }, - // etherscan: { - // // Your API key for Etherscan - // // Obtain one at https://etherscan.io/ - // apiKey: process.env.ETHERSCAN_API - // }, namedAccounts: { deployer: { default: 0, }, proxyAdmin: { - default: 1 - } - } + default: 1, + }, + }, }; - diff --git a/migrations/1_deploy_asset_contracts.js b/migrations/1_deploy_asset_contracts.js new file mode 100644 index 0000000..80c2349 --- /dev/null +++ b/migrations/1_deploy_asset_contracts.js @@ -0,0 +1,9 @@ +var WETH = artifacts.require("./contracts/WETH9.sol"); +var DAI = artifacts.require("./contracts/peripheral_contracts/USDCWithFaucet.sol"); + +module.exports = function(deployer, network, accounts) { + var admin = accounts[0]; + deployer.deploy(WETH); + deployer.deploy(DAI, 42, admin, "USDC", "USDC"); + +}; diff --git a/migrations/2_deploy_asset_contracts.js b/migrations/2_deploy_asset_contracts.js deleted file mode 100644 index 187bfb1..0000000 --- a/migrations/2_deploy_asset_contracts.js +++ /dev/null @@ -1,28 +0,0 @@ -var WETH = artifacts.require("./contracts/WETH9.sol"); -var DAI = artifacts.require("./contracts/peripheral_contracts/USDCWithFaucet.sol"); -var WAYNE = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); -var STARK = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); - -var GME = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); -var OPT = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); -var SPXE = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); -var WBTC = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); -var COIN = artifacts.require("./contracts/peripheral_contracts/EquityToken.sol"); - -const BigNumber = require('bignumber.js'); - -module.exports = function(deployer, network, accounts) { - - var admin = "0xC96495C314879586761d991a2B68ebeab12C03FE"; - // Core Assets (Asset / Quote) used in protocol testing - deployer.deploy(WETH); - deployer.deploy(DAI, 42, admin, "USDC", "USDC"); - // deployer.deploy(STARK, admin, new BigNumber(1000e18)); - - // deployer.deploy(GME, admin, new BigNumber(1000e18)); - // deployer.deploy(OPT, admin, new BigNumber(1000e18)); - // deployer.deploy(SPXE, admin, new BigNumber(1000e18)); - // deployer.deploy(WBTC, admin, new BigNumber(1000e18)); - // deployer.deploy(COIN, admin, new BigNumber(1000e18)); - // deployer.deploy(RBCN, admin, admin); -}; diff --git a/migrations/3_exchange_and_pools.js b/migrations/2_exchange_and_pools.js similarity index 58% rename from migrations/3_exchange_and_pools.js rename to migrations/2_exchange_and_pools.js index 339e1e8..f785782 100644 --- a/migrations/3_exchange_and_pools.js +++ b/migrations/2_exchange_and_pools.js @@ -2,15 +2,7 @@ require('dotenv').config(); var RubiconMarket = artifacts.require("./contracts/RubiconMarket.sol"); var BathHouse = artifacts.require("./contracts/rubiconPoolsv0/BathHouse.sol"); var BathPair = artifacts.require("./contracts/rubiconPoolsv0/BathPair.sol"); -var BathToken = artifacts.require("./contracts/rubiconPoolsv0/BathToken.sol"); -var WETH = artifacts.require("./contracts/WETH9.sol"); -var DAI = artifacts.require("./contracts/peripheral_contracts/USDCWithFaucet.sol"); - -const { deployProxy } = require('@openzeppelin/truffle-upgrades'); -const { deploy } = require('@openzeppelin/truffle-upgrades/dist/utils'); - -// This file will deploy Rubicon Market and Pools while wrapping everything in upgradeable proxies // @dev - use: ganache-cli --gasLimit=0x1fffffffffffff --gasPrice=0x1 --allowUnlimitedContractSize --defaultBalanceEther 9000 module.exports = async function(deployer, network, accounts) { // Use accounts[0] for testing purposes @@ -23,13 +15,6 @@ module.exports = async function(deployer, network, accounts) { // Initialize immediately on deployment await rubiconMarketInstance.initialize(false, admin); - // Deploy liquidity pools for WETH / DAI - see 3_pool_test.js - wethInstance = await WETH.deployed(); - daiInstance = await DAI.deployed(); - - // await deployer.deploy(BathToken) - // rubiconMarketInstance = await RubiconMarket.deployed(); - await deployer.deploy(BathHouse).then(async function() { await deployer.deploy(BathPair); return; diff --git a/strategist/StrategistTutorialComingSoon.txt b/strategist/StrategistTutorialComingSoon.txt new file mode 100644 index 0000000..5031996 --- /dev/null +++ b/strategist/StrategistTutorialComingSoon.txt @@ -0,0 +1 @@ +Please see our docs for details on Rubicon Pools and the role of the strategist. An example strategist bot will be provided here soon! If you're reading this and are interested in becoming a strategist, send an email to contact@rubicon.finance. \ No newline at end of file diff --git a/strategist/kovanPoolsStrategist.js b/strategist/kovanPoolsStrategist.js deleted file mode 100644 index d61a537..0000000 --- a/strategist/kovanPoolsStrategist.js +++ /dev/null @@ -1,550 +0,0 @@ -const Web3 = require("web3"); -const noncemanager = require("./nonceManager/noncemanager.js"); - -// var Contract = require('web3-eth-contract'); -var fs = require("fs"); -require("dotenv").config(); -const BigNumber = require("bignumber.js"); -BigNumber.config({ DECIMAL_PLACES: 18 }); -BigNumber.config({ ROUNDING_MODE: 4 }); -// ************ Rubicon Pools Kovan Setup *************** - -// Initialize Web3 -// Kovan -let web3 = new Web3( - "https://optimism-kovan.infura.io/v3/" + process.env.INFURA_API_KEY -); - -// OP Kovan -// let web3 = new Web3("https://kovan.optimism.io"); -// console.log("Web3 Version: ", web3.version); - -// Load the RubiconMarket contract -var { abi } = require("../build/contracts/RubiconMarket.json"); -// var rubiconMarketKovanAddr = process.env.OP_KOVAN_5_MARKET; -var rubiconMarketKovanAddr = process.env.OP_KOVAN_5_MARKET; -var RubiconMarketContractKovan = new web3.eth.Contract( - abi, - rubiconMarketKovanAddr -); - -// Load in Pools contract addresses on Kovan -var { abi } = require("../build/contracts/BathHouse.json"); -var bathHouseKovanAddr = process.env.OP_KOVAN_5_BATHHOUSE; -var bathHouseContractKovan = new web3.eth.Contract(abi, bathHouseKovanAddr); - -// Load in bath token asset contract addresses on Kovan -var { abi } = require("../build/contracts/BathToken.json"); -var bathWayneKovanAddr = process.env.OP_KOVAN_5_BATHWBTC; -var bathWayneContractKovan = new web3.eth.Contract(abi, bathWayneKovanAddr); - -// Load in bath token quote contract addresses on Kovan -var { abi } = require("../build/contracts/BathToken.json"); -var bathUsdcKovanAddr = process.env.OP_KOVAN_5_BATHUSDC; -var bathUsdcContractKovan = new web3.eth.Contract(abi, bathUsdcKovanAddr); - -// Load in WAYNE Contract -var { abi } = require("../build/contracts/EquityToken.json"); -var WAYNEKovanAddr = process.env.OP_KOVAN_5_WBTC; -var WAYNEContractKovan = new web3.eth.Contract(abi, WAYNEKovanAddr); - -// Load in Dai Contract -var { abi } = require("../build/contracts/USDCWithFaucet.json"); -var USDC_OP_KOVAN = process.env.OP_KOVAN_5_USDC; -var DAIContractKovan = new web3.eth.Contract(abi, USDC_OP_KOVAN); - -// Load in BathPair Contract -var { abi } = require("../build/contracts/BathPair.json"); -const { ethers } = require("ethers"); -var bathPairKovanAddr = process.env.OP_KOVAN_5_BATHWBTCUSDC; -var bathPairContractKovan = new web3.eth.Contract(abi, bathPairKovanAddr); - -var sender = process.env.OP_KOVAN_ADMIN; -var key = process.env.OP_KOVAN_ADMIN_KEY; - -// TODO: make this work at scale -// *** Nonce Manager *** -// https://ethereum.stackexchange.com/questions/39790/concurrency-patterns-for-account-nonce -// let nonceOffset = 0; -baseNonce = web3.eth.getTransactionCount( - process.env.OP_KOVAN_ADMIN //, "pending" -); -// async function getNonce() { -// return await baseNonce.then((nonce) => nonce + nonceOffset++); -// } - -var nonceFunctionWeb3 = () => { - return new Promise(async (resolve, reject) => { - // console.log("asking web3 for current nonce.."); - const updatedNonce = - (await web3.eth.getTransactionCount(process.env.OP_KOVAN_ADMIN)) - 1; - setTimeout(resolve, 2000, updatedNonce); - }); -}; - -// returns the next Nonce -function getNonce() { - return noncemanager - .getInstance() - .getTransactionPermission() - .then(() => { - const next = noncemanager.getInstance().getNextNonce(); - // console.log("Next nonce", next); - return next; - }); -} - -async function initNonceManager() { - // console.log("base", await baseNonce); - noncemanager.getInstance((await baseNonce) - 1, nonceFunctionWeb3); -} - -async function sendTx(tx, msg, ticker) { - tx.nonce = await getNonce(); - // console.log("new nonce for " + msg, tx.nonce); - tx.gasPrice = 15000000; - // tx.gasLimit = 874432; - tx.gas = 880000; - // console.log('outgoing transaction: ', tx); - return web3.eth.accounts.signTransaction(tx, key).then((signedTx) => { - web3.eth - .sendSignedTransaction(signedTx.rawTransaction) - .on("receipt", () => {}) - .then((r) => { - console.log("*transaction success* " + ticker + " => " + msg); - return true; - // console.log(r); - }) - .catch((c) => { - console.log("** " + ticker + " Transaction Failed **"); - console.log(c); - console.log("**************************************"); - return false; - }); - }); -} - -async function getContractFromToken(ticker, contract) { - // Load in Dai Contract - var { abi } = require("../build/contracts/" + contract + ".json"); - if (contract == "BathToken") { - var address = process.env["OP_KOVAN_5_BATH" + ticker]; - } else if (contract == "BathPair") { - var address = process.env["OP_KOVAN_5_BATH" + ticker + "USDC"]; - } else if (contract == "EquityToken") { - var address = process.env["OP_KOVAN_5_" + ticker]; - } else { - throw "unhandled contract type"; - } - return new web3.eth.Contract(abi, address); -} - -//#region - -// // // **Approve bathPair to recieve WAYNE and DAI first** -// var txData = WAYNEContractKovan.methods.approve(process.env.OP_KOVAN_5_BATHWBTC, web3.utils.toWei("10000000")).encodeABI(); -// var tx = { -// gas: 12500000, -// data: txData.toString(), -// from: sender, -// to: WAYNEKovanAddr, -// gasPrice: web3.utils.toWei("0", "Gwei") -// } -// // Send the transaction -// sendTx(tx, "Approve bathPair to recieve WAYNE"); - -// // setTimeout(() => {console.log('waiting for nonce update')}, 2000) - -// var txData = DAIContractKovan.methods.approve(process.env.OP_KOVAN_5_BATHUSDC, web3.utils.toWei("30000000")).encodeABI(); -// var tx = { -// gas: 12500000, -// data: txData.toString(), -// from: sender, -// to: USDC_OP_KOVAN, -// gasPrice: web3.utils.toWei("0", "Gwei") -// } -// // Send the transaction -// sendTx(tx, "dai approve"); -// --------------------------------------------------------- -// // Deposit WAYNE into BathToken WAYNE -// var txData = bathWayneContractKovan.methods.deposit(web3.utils.toWei("50")).encodeABI(); -// var tx = { -// gas: 12500000, -// data: txData.toString(), -// from: sender, -// to: process.env.OP_KOVAN_BATHWAYNE, -// gasPrice: web3.utils.toWei("0", "Gwei") -// } -// // Send the transaction -// sendTx(tx, "Deposit WAYNE into BathToken WAYNE"); - -// // console.log(bathUsdcContractKovan.methods.symbol().call().then((r) => console.log(r))); -// // console.log(DAIContractKovan.methods.allowance(sender,process.env.OP_KOVAN_BATHUSDC ).call().then((r) => console.log(r))); - -// // // Deposit USDC into BathToken USDC -// var txData = bathUsdcContractKovan.methods.deposit(web3.utils.toWei("100")).encodeABI(); -// var tx = { -// gas: 12500000, -// data: txData.toString(), -// from: sender, -// to: process.env.OP_KOVAN_BATHUSDC, -// gasPrice: "0" -// } -// // Send the transaction -// sendTx(tx, "Deposit USDC into BathToken USDC"); - -// Will revert if no bathToken liquidity -// console.log(bathPairContractKovan.methods.getMaxOrderSize(process.env.OP_KOVAN_5_WBTC, process.env.OP_KOVAN_5_BATHWBTC).call().then((r) => console.log("POOLS Max order size for WBTC: " + web3.utils.fromWei(r)))); -// console.log(bathPairContractKovan.methods.getMaxOrderSize(process.env.OP_KOVAN_5_USDC, process.env.OP_KOVAN_5_BATHUSDC).call().then((r) => console.log("POOLS Max order size for USDC: " + web3.utils.fromWei(r)))); -// bathUsdcContractKovan.methods.totalSupply().call().then((r) =>{ -// console.log("Total supply of BathUSDC", web3.utils.fromWei(r)) -// }); -// bathPairContractKovan.methods.maxOrderSizeBPS().call().then((r) =>{ -// console.log("Max order sizeBPD of BathPair", (r)) -// }); - -//#endregion -// ------------------------------------ - -// MarketMake: -// Pseudocode - As a loop: -// 1. Grab the current price for a Kovan pair -// 2. executeStrategy --> Place better a bid and ask at the best bid/ask - 1 -// 2a. Make sure that dynamic order sizes are placed to manage inventory... - -async function stoikov(token) { - var bestAsk = await RubiconMarketContractKovan.methods - .getBestOffer(process.env["OP_KOVAN_5_" + token], USDC_OP_KOVAN) - .call(); - var askInfo = await RubiconMarketContractKovan.methods - .getOffer(bestAsk) - .call(); - var bestAskPrice = askInfo[2] / askInfo[0]; - - var bestBid = await RubiconMarketContractKovan.methods - .getBestOffer(USDC_OP_KOVAN, process.env["OP_KOVAN_5_" + token]) - .call(); - var bidInfo = await RubiconMarketContractKovan.methods - .getOffer(bestBid) - .call(); - var bestBidPrice = bidInfo[0] / bidInfo[2]; - return [bestAskPrice, bestBidPrice]; -} - -async function logInfo(mA, mB, a, b, im) { - console.log("---------- Market Information ----------"); - console.log("Current Best Ask Price: ", mA); - console.log("Current Best Bid Price: ", mB); - console.log("Current Midpoint Price: ", (mA + mB) / 2); - console.log("\n---------- Pools Information ----------"); - console.log("New Pools Ask Price: ", a); - console.log("New Pools Bid Price: ", b); - console.log("Pools Inventory Ratio [(Quote / Asset) ~ 1]: ", im); - - // APR CALCULATIONS - await bathWayneContractKovan.methods - .totalSupply() - .call() - .then(async function (r) { - // console.log("Total Supply of bathWAYNE: ", r); - var underlying = await WAYNEContractKovan.methods - .balanceOf(process.env.OP_KOVAN_5_BATHWBTC) - .call(); - // console.log("Total Underlying: ", underlying); - var uOverC = await (underlying / r); - let naiveAPR; - console.log("balanceOf underlying", underlying); - console.log("totalSupply", r); - - if (uOverC >= 1) { - naiveAPR = - "+" + - (((await (underlying / r)) - 1) * 100).toFixed(3).toString() + - "%"; - } else { - naiveAPR = - "-" + - ((1 - (await (underlying / r))) * 100).toFixed(3).toString() + - "%"; - } - console.log("Return on Assets for bathWAYNE since Inception: ", naiveAPR); - }); - - await bathUsdcContractKovan.methods - .totalSupply() - .call() - .then(async function (r) { - // console.log("Total Supply of bathWAYNE: ", r); - var underlying = await DAIContractKovan.methods - .balanceOf(process.env.OP_KOVAN_5_BATHUSDC) - .call(); - // console.log("Total Underlying: ", underlying); - var uOverC = await (underlying / r); - let naiveAPR; - if (uOverC >= 1) { - naiveAPR = - "+" + - (((await (underlying / r)) - 1) * 100).toFixed(3).toString() + - "%"; - } else { - naiveAPR = - "-" + - ((1 - (await (underlying / r))) * 100).toFixed(3).toString() + - "%"; - } - console.log("Return on Assets for bathUSDC since Inception: ", naiveAPR); - }); - console.log("--------------------------------------\n"); -} - -async function checkForScrub(ticker) { - const contract = await getContractFromToken(ticker, "BathPair"); - // console.log("got this contract", contract); - await contract.methods - .getOutstandingPairCount() - .call() - .then(async (r) => { - // console.log("THIS MANY PAIRS for", ticker + ":", r); - if (r > -1) { - // Scrub the bath - var txData = await contract.methods.bathScrub().encodeABI(); - var tx = { - gas: 9530000, - data: txData, - from: process.env.OP_KOVAN_ADMIN.toString(), - to: process.env["OP_KOVAN_5_BATH" + ticker + "USDC"], - gasPrice: web3.utils.toWei("0.015", "Gwei"), - }; - try { - // await contract.methods - // .bathScrub() - // .estimateGas(tx, async function (r, d) { - // if (r != null) { - // console.log( - // "Got a problem estimating bathScrub for " + ticker, - // r - // ); - // } - // if (d > 0) { - await sendTx( - tx, - "\n<* I have successfully scrubbed the " + - ticker + - " bath, Master *>\n", - ticker + " bath Scrub!" - ); - // } else { - // throw ("gas estimation in bathScrub failed for", ticker); - // } - // }); //.catch((e) => {console.log("failed to estimate gas for " + ticker + "bathScrub")}) - } catch (error) { - console.log("failed to estimate gas for " + ticker + "bathScrub"); - } - } - }); -} - -let oldMidpoint = []; -let targetMidpoint = []; -let midPoint = []; -async function marketMake(a, b, t, im, spread, tM) { - const ticker = await t; - const contract = await getContractFromToken(await ticker, "BathPair"); - // ***Market Maker Inputs*** - const targetSpread = await spread; // the % of the spread we want to improve - const scaleBack = new BigNumber(1); // used to scale back maxOrderSize - // ************************* - // Check if midpoint is unchanged before market making - midPoint[ticker] = ((await a) + (await b)) / 2; - - if (midPoint[ticker] == oldMidpoint[ticker]) { - // console.log( - // "\n<* Midpoint is Unchanged, Therefore I Continue My Watch*>\n" - // ); - return; - } //make it target midpoint - else if (midPoint[ticker] == 0 || isNaN(midPoint[ticker])) { - if (targetMidpoint[ticker] == undefined) { - // console.log("new target", tM); - targetMidpoint[ticker] = await tM; - } - midPoint[ticker] = targetMidpoint[ticker]; - } else { - oldMidpoint[ticker] = midPoint[ticker]; - targetMidpoint[ticker] = midPoint[ticker]; - } - // console.log("midPoint", midPoint); - // console.log("target midPoint", tM); - - await checkForScrub(t); - - var newBidPrice = new BigNumber( - parseFloat(midPoint[ticker] * (1 - targetSpread)) - ); - var newAskPrice = new BigNumber( - parseFloat(midPoint[ticker] * (1 + targetSpread)) - ); - - // getMaxOrderSize from contract for bid and ask - const maxAskSize = new BigNumber( - await contract.methods - .getMaxOrderSize( - process.env["OP_KOVAN_5_" + (await ticker)], - process.env["OP_KOVAN_5_BATH" + (await ticker)] - ) - .call() - ); - const maxBidSize = new BigNumber( - await contract.methods - .getMaxOrderSize( - process.env.OP_KOVAN_5_USDC, - process.env.OP_KOVAN_5_BATHUSDC - ) - .call() - ); - // const maxAskSize = new BigNumber(1); - // const maxBidSize = new BigNumber(1); - - // in wei - const askNum = maxAskSize.dividedBy(scaleBack); - const askDen = askNum.multipliedBy(newAskPrice); - - const bidNum = maxBidSize.dividedBy(scaleBack); - const bidDen = bidNum.dividedBy(newBidPrice); - - // await logInfo(a, b, askDen / askNum, bidNum / bidDen, await im); - - // console.log( - // // askNum.decimalPlaces(0).toString(), - // // askDen.decimalPlaces(0).toString(), - // bidNum.decimalPlaces(0).toString(), - // bidDen.decimalPlaces(0).toString() - // ); - var txData = contract.methods - .executeStrategy( - web3.utils.toBN(askNum.decimalPlaces(0)), - web3.utils.toBN(askDen.decimalPlaces(0)), - web3.utils.toBN(bidNum.decimalPlaces(0)), - web3.utils.toBN(bidDen.decimalPlaces(0)) - ) - .encodeABI(); - var tx = { - gas: 9000000, - data: txData.toString(), - from: process.env.OP_KOVAN_ADMIN.toString(), - to: process.env["OP_KOVAN_5_BATH" + (await ticker) + "USDC"], - gasPrice: web3.utils.toWei("0", "Gwei"), - }; - // console.log( - // "ATTEMPTING " + - // ticker + - // " trades placed at [bid]: " + - // newBidPrice.toString() + - // "$ and [ask]: " + - // newAskPrice.toString() + - // "$" + - // "\n" - // ); - let result = await sendTx( - tx, - "New " + - (await ticker) + - " trades placed at [bid]: " + - newBidPrice.toString() + - "$ and [ask]: " + - newAskPrice.toString() + - "$" + - "\n", - ticker - ); - - oldMidpoint[ticker] = midPoint[ticker]; -} - -// This function should return a positive or negative number reflecting the balance. -async function checkInventory(currentAsk, currentBid, ticker) { - const contractBP = await getContractFromToken(await ticker, "BathToken"); - const contractT = await getContractFromToken(await ticker, "EquityToken"); - - var currentReserveRatio = 80.0 / 100.0; - var assetBalance = await contractT.methods - .balanceOf(process.env["OP_KOVAN_5_BATH" + ticker]) - .call(); - var quoteBalance = await DAIContractKovan.methods - .balanceOf(process.env.OP_KOVAN_5_BATHUSDC) - .call(); - const bathQuoteSupply = await bathUsdcContractKovan.methods - .totalSupply() - .call(); - const bathAssetSupply = await contractBP.methods.totalSupply().call(); - // console.log('Current asset liquidity balance: ', web3.utils.fromWei(assetBalance), ticker); - // console.log('Current quote liquidity balance: ', web3.utils.fromWei(quoteBalance), "USDC"); - - if (assetBalance == 0 || quoteBalance == 0) { - throw "ERROR: no liquidity in quote or asset bathToken"; - } - if (bathAssetSupply * currentReserveRatio >= assetBalance) { - console.log("Hurdle Rate: ", bathAssetSupply * currentReserveRatio); - console.log("Asset balance: ", assetBalance); - throw "ERROR: insufficient asset liquidity to clear reserve ratio"; - } - if (bathQuoteSupply * currentReserveRatio >= quoteBalance) { - console.log("Hurdle Rate: ", bathQuoteSupply * currentReserveRatio); - console.log("Quote balance: ", assetBalance); - throw "ERROR: insufficient quote liquidity to clear reserve ratio"; - } - - // Ratio targets the current orderbook midpoint as the ideal ratio (50/50) - return quoteBalance / assetBalance / ((currentAsk + currentBid) / 2); // This number represents if the pair is overweight in one direction -} - -// This function sets off the chain of calls to successfully marketMake with Pools -async function startBot(token, spread, tM) { - setTimeout(async function () { - // Returns best bid and ask price - stoikov(token).then(async function (data) { - var currentAsk = data[0]; - var currentBid = data[1]; - - // Returns a pair overweight - const IMfactor = checkInventory(currentAsk, currentBid, token); - - // Sends executeTransaction() - await marketMake( - currentAsk, - currentBid, - await token, - await IMfactor, - await spread, - await tM - ); - }); - // console.log( - // "\n⚔⚔⚔ Strategist Bot Market Makes with Diligence and Valor ⚔⚔⚔\n" - // ); - - // Again - startBot(token, spread, tM); - - // Every 2.5 sec - }, 3000); -} - -console.log("\n<* Strategist Bot Begins its Service to Rubicon *>\n"); - -// **** Key inputs **** -const assets = [ - "WBTC", - "MKR", - "SNX", - "REP", - "RGT", - // "ETH", - "COMP", - "OHM", - "AAVE", -]; - -initNonceManager().then(async () => { - // startBot("COMP", 0.02, 825); -}); diff --git a/strategist/nonceManager/example.js b/strategist/nonceManager/example.js deleted file mode 100644 index 3676f98..0000000 --- a/strategist/nonceManager/example.js +++ /dev/null @@ -1,29 +0,0 @@ -const noncemanager = require("./noncemanager.js"); - -var nonceFunction = () => { - return new Promise((resolve, reject) => { - console.log("asking web3 for current nonce.."); - setTimeout(resolve, 2000); - }); -}; - -noncemanager.getInstance(0, nonceFunction); -// noncemanager.getInstance(currentNonce, nonceFunctionWeb3).getTransactionPermission().then( () => { -// console.log("Next nonce", noncemanager.getInstance().getNextNonce()); -// }); - -var printNonce = function () { - var l = Math.floor(Math.random() * 10); - for (var i = 0; i < l; i++) { - noncemanager - .getInstance() - .getTransactionPermission() - .then(() => { - console.log("Next nonce", noncemanager.getInstance().getNextNonce()); - }); - } - - setTimeout(printNonce, Math.floor(Math.random() * 1000 * 45)); -}; - -printNonce(); diff --git a/test/3_pool_test.js b/test/1_pool_test.js similarity index 94% rename from test/3_pool_test.js rename to test/1_pool_test.js index 6a36bed..e1d04ed 100644 --- a/test/3_pool_test.js +++ b/test/1_pool_test.js @@ -4,7 +4,6 @@ const BathToken = artifacts.require("BathToken"); const RubiconMarket = artifacts.require("RubiconMarket"); const DAI = artifacts.require("USDCWithFaucet"); const WETH = artifacts.require("WETH9"); -const WAYNE = artifacts.require("EquityToken"); const helper = require("./testHelpers/timeHelper.js"); @@ -309,6 +308,31 @@ contract("Rubicon Exchange and Pools Test", async function (accounts) { // Idea here is that the start of the local search rolls over after indexs 4-6 are checked in seconds call // assert.equal(await bathPairInstance.start().toString(), "0"); }); + it("index scrub can be used by approved strategists", async function () { + let target = 2; + // let goalScrub = target + outCount.toNumber(); + for (let index = 0; index < target; index++) { + await bathPairInstance.executeStrategy( + askNumerator, + askDenominator, + bidNumerator, + bidDenominator + ); + // await rubiconMarketInstance.buy(4 + (2 * (index + 1)), web3.utils.toWei((0.4).toString()), { + // from: accounts[5], + // }); + } + const outCount = (await bathPairInstance.getOutstandingPairCount()); + logIndented( + "cost of indexScrub:", + await bathPairInstance.indexScrub.estimateGas(0, 2) + ); + await bathPairInstance.indexScrub(0,outCount - 1); + helper.advanceTimeAndBlock(100); + + assert.equal((await bathPairInstance.getOutstandingPairCount()).toString(), '0'); + + }); it("bathTokens are correctly logging outstandingAmount", async function () { let target = 6; for (let index = 0; index < target; index++) { @@ -398,7 +422,8 @@ contract("Rubicon Exchange and Pools Test", async function (accounts) { // }); // } it("Funds are correctly returned to bathTokens", async function () { - await bathPairInstance.bathScrub(); + logIndented("cost of rebalance: ", await bathPairInstance.rebalancePair.estimateGas()); + await bathPairInstance.rebalancePair(); assert.equal( (await WETHInstance.balanceOf(bathQuoteInstance.address)).toString(), "0"