From 7d354730d6f4572060b4f28e6f1d4db6cac34c87 Mon Sep 17 00:00:00 2001 From: Mike Ghen Date: Sat, 31 Jul 2021 19:54:31 -0400 Subject: [PATCH] v1.2: Fix IDA and fee calculation issues (#6) * sushiswap oracle, tellor fallback, events * update tests and readme * eth->dai exchange testing * check IDA shares before distribute * misc. fixes for deploy * Added notes for v2 spec * calculate fee and distribute the actualAmount * tests for fee and v1.2 deploy config * script for setting rate tolerance * support verification --- 00-Meta/Specification.md | 34 +++++++++++++++++++ 01-Contracts/arguments.js | 25 ++++++++++++++ 01-Contracts/contracts/StreamExchange.sol | 8 +++-- .../contracts/StreamExchangeHelper.sol | 8 ++--- 01-Contracts/hardhat.config.js | 20 +++++++---- 01-Contracts/package.json | 3 +- 01-Contracts/scripts/deploy-polygon.js | 13 ++++--- 01-Contracts/scripts/set-rate-tolerance.js | 28 +++++++++++++++ 01-Contracts/test/SteamExchange.test.js | 31 ++++++++++++++--- 9 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 00-Meta/Specification.md create mode 100644 01-Contracts/arguments.js create mode 100644 01-Contracts/scripts/set-rate-tolerance.js diff --git a/00-Meta/Specification.md b/00-Meta/Specification.md new file mode 100644 index 00000000..24bc5f66 --- /dev/null +++ b/00-Meta/Specification.md @@ -0,0 +1,34 @@ +# Ricochet Exchange v2 (Draft) + +## Overview +* Ricochet is a stream exchange that uses Superfluid +* Each `RicochetExchange` contract supports two-way streaming swaps +* Streamers can stream either `tokenA` or `tokenB` at any rate and RicochetExchange keepers will trigger swaps periodically +* `RicochetExchange` will perform the following algorithm to swap: + * Determine the `surplusToken`, which of tokenA or tokenB there's an excess amount of the other token available to swap with + * Example: consider there is $100 DAI and $50 ETH that's been streamed into the contract + * ETH is the `surplusToken` because there's _more_ than enough DAI to make the swap + * Set the other token as the `deficitToken` + * Consider the above example, DAI is the deficitToken because there's not enough ETH to swap for DAI + * Next, perform the internal swap, swap the `surplusToken` for the `deficitToken` at the current `exchangeRate` + * Then, take the remaining amount of the `deficitToken` and swap on Sushiswap + * Finally, distribute to `tokenA` and `tokenB` their tokens + +## Protocol Speciciations + +### Structures +* `Oracle` + * `ITellor oracle` - Address of deployed simple oracle for input//output token + * `uint256 requestId` - The id of the tellor request that has input/output exchange rate + * `uint256 rateTolerance` - The percentage to deviate from the oracle scaled to 1e6 + +* `Exchange` + * `ISuperfluid host` - Superfluid host contract + * `IConstantFlowAgreementV1 cfa` - The stored constant flow agreement class address + * `IInstantDistributionAgreementV1 ida` - The stored instant dist. agreement class address + * `ISuperToken tokenA` - One of the tokens supported for streaming + * `ISuperToken tokenB` - The other one of the tokens supported for streaming + * `int96 totalInflow` - The fee taken as a % with 6 decimals + * `uint128 feeRate` - The fee taken as a % with 6 decimals + * `IUniswapV2Router02 sushiRouter` - Address of sushsiwap router to use for swapping + * `Oracle oracle` - The oracle to use for the exchange diff --git a/01-Contracts/arguments.js b/01-Contracts/arguments.js new file mode 100644 index 00000000..8280d9f1 --- /dev/null +++ b/01-Contracts/arguments.js @@ -0,0 +1,25 @@ + +// Polygon Mainnet +const HOST_ADDRESS = "0x3E14dC1b13c488a8d5D310918780c983bD5982E7"; +const CFA_ADDRESS = "0x6EeE6060f715257b970700bc2656De21dEdF074C"; +const IDA_ADDRESS = "0xB0aABBA4B2783A72C52956CDEF62d438ecA2d7a1"; +const DAIX_ADDRESS = "0x1305F6B6Df9Dc47159D12Eb7aC2804d4A33173c2"; +const ETHX_ADDRESS = "0x27e1e4E6BC79D93032abef01025811B7E4727e85"; +const SUSHISWAP_ROUTER_ADDRESS = "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"; +const TELLOR_ORACLE_ADDRESS = "0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA"; +const RIC_CONTRACT_ADDRESS = "0x263026e7e53dbfdce5ae55ade22493f828922965"; +const TELLOR_REQUEST_ID = 1; + + +module.exports = [ + HOST_ADDRESS, + CFA_ADDRESS, + IDA_ADDRESS, + process.env.INPUT_TOKEN_ADDRESS, + process.env.OUTPUT_TOKEN_ADDRESS, + RIC_CONTRACT_ADDRESS, + SUSHISWAP_ROUTER_ADDRESS, + TELLOR_ORACLE_ADDRESS, + TELLOR_REQUEST_ID, + process.env.SF_REG_KEY +]; diff --git a/01-Contracts/contracts/StreamExchange.sol b/01-Contracts/contracts/StreamExchange.sol index 1b964e44..fe719d7e 100644 --- a/01-Contracts/contracts/StreamExchange.sol +++ b/01-Contracts/contracts/StreamExchange.sol @@ -118,8 +118,12 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor { newCtx = ctx; - // NOTE: Trigger a distribution if there's any inputToken - if (ISuperToken(_exchange.inputToken).balanceOf(address(this)) > 0 && doDistributeFirst) { + (, , uint128 totalUnitsApproved, uint128 totalUnitsPending) = _exchange.ida.getIndex( + _exchange.outputToken, + address(this), + _exchange.outputIndexId); + + if (doDistributeFirst && totalUnitsApproved + totalUnitsPending > 0 && ISuperToken(_exchange.inputToken).balanceOf(address(this)) > 0) { newCtx = _exchange._distribute(newCtx); } diff --git a/01-Contracts/contracts/StreamExchangeHelper.sol b/01-Contracts/contracts/StreamExchangeHelper.sol index 56efb36e..a3d6871d 100644 --- a/01-Contracts/contracts/StreamExchangeHelper.sol +++ b/01-Contracts/contracts/StreamExchangeHelper.sol @@ -82,8 +82,8 @@ library StreamExchangeHelper { if (actualAmount == 0) { return newCtx; } // Calculate the fee for making the distribution - uint256 feeCollected = outputBalance * self.feeRate / 1e6; - uint256 distAmount = outputBalance - feeCollected; + uint256 feeCollected = actualAmount * self.feeRate / 1e6; + uint256 distAmount = actualAmount - feeCollected; // Calculate subside @@ -91,7 +91,7 @@ library StreamExchangeHelper { // Confirm the app has enough to distribute require(self.outputToken.balanceOf(address(this)) >= actualAmount, "!enough"); - + console.log("distAmount", distAmount); newCtx = _idaDistribute(self, self.outputIndexId, uint128(distAmount), self.outputToken, newCtx); emit Distribution(distAmount, feeCollected, address(self.outputToken)); @@ -132,7 +132,7 @@ library StreamExchangeHelper { minOutput = amount * exchangeRate / 1e6; console.log("minOutput", minOutput); minOutput = minOutput * (1e6 - self.rateTolerance) / 1e6; - console.log("minOutput", minOutput); + console.log("minOutput after rate tolerance", minOutput); self.inputToken.downgrade(amount); inputToken = self.inputToken.getUnderlyingToken(); diff --git a/01-Contracts/hardhat.config.js b/01-Contracts/hardhat.config.js index 4c53158c..195ec580 100644 --- a/01-Contracts/hardhat.config.js +++ b/01-Contracts/hardhat.config.js @@ -3,6 +3,7 @@ require("@nomiclabs/hardhat-web3"); require('@nomiclabs/hardhat-ethers'); require('@openzeppelin/hardhat-upgrades'); require('hardhat-contract-sizer'); +require("@nomiclabs/hardhat-etherscan"); // This is a sample Hardhat task. To learn how to create your own go to // https://hardhat.org/guides/create-task.html @@ -26,12 +27,12 @@ module.exports = { timeout: 100000 }, networks: { - // polygon: { - // url: process.env.POLYGON_QUIKNODE_URL, - // accounts: [process.env.PRIVATE_KEY], - // gas: 2000000, - // gasPrice: 2000000000 - // }, + polygon: { + url: "https://polygon-mainnet.infura.io/v3/" + process.env.INFURA_KEY, + accounts: [process.env.MATIC_PRIVATE_KEY], + gas: 2000000, + gasPrice: 20000000000 + }, rinkeby: { url: "https://rinkeby.infura.io/v3/" + process.env.INFURA_KEY, accounts: [process.env.PRIVATE_KEY], @@ -44,11 +45,16 @@ module.exports = { // }, hardhat: { forking: { - url: "https://rinkeby.infura.io/v3/" + process.env.INFURA_KEY, + url: process.env.QUICK_NODE_URL, accounts: [process.env.PRIVATE_KEY_ADMIN, process.env.PRIVATE_KEY_ALICE, process.env.PRIVATE_KEY_BOB], } } }, + etherscan: { + // Your API key for Etherscan + // Obtain one at https://etherscan.io/ + apiKey: process.env.POLYSCAN_API_KEY + }, contractSizer: { alphaSort: true, runOnCompile: true, diff --git a/01-Contracts/package.json b/01-Contracts/package.json index 5406590d..f5c6ccb4 100644 --- a/01-Contracts/package.json +++ b/01-Contracts/package.json @@ -11,9 +11,11 @@ "license": "ISC", "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-etherscan": "^2.1.4", "@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/contracts": "^4.0.0", + "@openzeppelin/hardhat-upgrades": "^1.9.0", "@superfluid-finance/ethereum-contracts": "^1.0.0-rc.5", "@truffle/contract": "^4.3.23", "axios": "^0.21.1", @@ -26,7 +28,6 @@ }, "dependencies": { "@openzeppelin/contracts-upgradeable": "^4.1.0", - "@openzeppelin/hardhat-upgrades": "^1.8.2", "@uniswap/v2-core": "^1.0.1", "@uniswap/v2-periphery": "^1.1.0-beta.0", "hardhat-contract-sizer": "^2.0.3", diff --git a/01-Contracts/scripts/deploy-polygon.js b/01-Contracts/scripts/deploy-polygon.js index b115306c..4ae80929 100644 --- a/01-Contracts/scripts/deploy-polygon.js +++ b/01-Contracts/scripts/deploy-polygon.js @@ -46,21 +46,24 @@ async function main() { console.log("\tHOST_ADDRESS", HOST_ADDRESS) console.log("\tCFA_ADDRESS", CFA_ADDRESS) console.log("\tIDA_ADDRESS", IDA_ADDRESS) - console.log("\tDAIX_ADDRESS",DAIX_ADDRESS) - console.log("\tETHX_ADDRESS", ETHX_ADDRESS) + console.log("\tINPUT_TOKEN", process.env.INPUT_TOKEN_ADDRESS) + console.log("\tOUTPUT_TOKEN", process.env.OUTPUT_TOKEN_ADDRESS) console.log("\tSUSHISWAP_ROUTER_ADDRESS", SUSHISWAP_ROUTER_ADDRESS) console.log("\tTELLOR_ORACLE_ADDRESS", TELLOR_ORACLE_ADDRESS) console.log("\tTELLOR_REQUEST_ID", TELLOR_REQUEST_ID) + + + const streamExchange = await StreamExchange.deploy( HOST_ADDRESS, CFA_ADDRESS, IDA_ADDRESS, - DAIX_ADDRESS, - ETHX_ADDRESS, + process.env.INPUT_TOKEN_ADDRESS, + process.env.OUTPUT_TOKEN_ADDRESS, RIC_CONTRACT_ADDRESS, SUSHISWAP_ROUTER_ADDRESS, TELLOR_ORACLE_ADDRESS, TELLOR_REQUEST_ID, - "ricochet23" ); + process.env.SF_REG_KEY ); await streamExchange.deployed(); console.log("Deployed StreamExchange at address:", streamExchange.address); } diff --git a/01-Contracts/scripts/set-rate-tolerance.js b/01-Contracts/scripts/set-rate-tolerance.js new file mode 100644 index 00000000..4f7987a6 --- /dev/null +++ b/01-Contracts/scripts/set-rate-tolerance.js @@ -0,0 +1,28 @@ +async function main() { + + const [keeper] = await ethers.getSigners(); + const RATE_TOLERANCE = "20000" + const STREAM_EXCHANGE_HELPER_ADDRESS = "0x0C7776292AB9E95c54282fD74e47d73338c457D8" + const RICOCHET_CONTRACT_ADDRESS = "0xe0B7907FA4B759FA4cB201F0E02E16374Bc523fd" + + const StreamExchangeHelper = await ethers.getContractFactory("StreamExchangeHelper") + const seh = await StreamExchangeHelper.attach(STREAM_EXCHANGE_HELPER_ADDRESS) + + const StreamExchange = await ethers.getContractFactory("StreamExchange", { + libraries: { + StreamExchangeHelper: seh.address, + }, + }); + const ricochet = await StreamExchange.attach(RICOCHET_CONTRACT_ADDRESS) + + console.log("rateTolerance", await ricochet.getRateTolerance()) + console.log("setRateTolerance", RATE_TOLERANCE, await ricochet.setRateTolerance(RATE_TOLERANCE)) + +} + +main() +.then(() => process.exit(0)) +.catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/01-Contracts/test/SteamExchange.test.js b/01-Contracts/test/SteamExchange.test.js index 53b44254..ada77aae 100644 --- a/01-Contracts/test/SteamExchange.test.js +++ b/01-Contracts/test/SteamExchange.test.js @@ -50,7 +50,7 @@ describe("StreamExchange", () => { before(async function () { //process.env.RESET_SUPERFLUID_FRAMEWORK = 1; let response = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd') - oraclePrice = parseInt(response.data.ethereum.usd * 1.002 * 1000000).toString() + oraclePrice = parseInt(response.data.ethereum.usd * 1.005 * 1000000).toString() console.log("oraclePrice", oraclePrice) }); @@ -389,21 +389,35 @@ describe("StreamExchange", () => { expect(await app.getFeeRate()).to.equal(20000) await app.connect(owner).setFeeRate(20000); + await app.connect(owner).setRateTolerance(50000); await app.connect(owner).setSubsidyRate("500000000000000000") expect(await app.getSubsidyRate()).to.equal("500000000000000000") expect(await app.getFeeRate()).to.equal(20000) + expect(await app.getRateTolerance()).to.equal(50000) console.log("Getters and setters correct") - const inflowRate = toWad(0.0000004000); + const inflowRate = toWad(0.00000004000); - await ethx.transfer(u.bob.address, "100000000000000000", {from: u.admin.address}); - await ethx.transfer(u.alice.address, "100000000000000000", {from: u.admin.address}); + console.log("Transfer bob") + await ethx.transfer(u.bob.address, "7000000000000000", {from: u.admin.address}); + console.log("Transfer aliuce") + await ethx.transfer(u.alice.address, "7000000000000000", {from: u.admin.address}); + console.log("Done") await tp.submitValue(1, oraclePrice); await takeMeasurements(); + // Test owner start/stop stream + await u.admin.flow({ flowRate: inflowRate, recipient: u.app }); + await traveler.advanceTimeAndBlock(60*60*3); + await tp.submitValue(1, oraclePrice); + await app.distribute() + await u.admin.flow({ flowRate: "0", recipient: u.app }); + + + await u.bob.flow({ flowRate: inflowRate, recipient: u.app }); await traveler.advanceTimeAndBlock(60*60*3); await tp.submitValue(1, oraclePrice); @@ -411,6 +425,7 @@ describe("StreamExchange", () => { await takeMeasurements(); await delta("Bob", bobBalances) await delta("Alice", aliceBalances) + await delta("Owner", ownerBalances) // Round 2 await u.alice.flow({ flowRate: inflowRate, recipient: u.app }); @@ -420,6 +435,8 @@ describe("StreamExchange", () => { await takeMeasurements() await delta("Bob", bobBalances) await delta("Alice", aliceBalances) + await delta("Owner", ownerBalances) + // Round 3 await traveler.advanceTimeAndBlock(60*60*2); @@ -428,6 +445,8 @@ describe("StreamExchange", () => { await takeMeasurements() await delta("Bob", bobBalances) await delta("Alice", aliceBalances) + await delta("Owner", ownerBalances) + // Round 4 @@ -438,6 +457,8 @@ describe("StreamExchange", () => { await takeMeasurements() await delta("Bob", bobBalances) await delta("Alice", aliceBalances) + await delta("Owner", ownerBalances) + // Round 5 await traveler.advanceTimeAndBlock(60*60*2); @@ -446,6 +467,8 @@ describe("StreamExchange", () => { await takeMeasurements() await delta("Bob", bobBalances) await delta("Alice", aliceBalances) + await delta("Owner", ownerBalances) + });