From 194af5403c8f86784cff9452e895751886f467f9 Mon Sep 17 00:00:00 2001 From: Mike Ghen Date: Wed, 21 Jul 2021 09:32:52 -0400 Subject: [PATCH] v1.1: Events, Better Oracle Setup, ETHx/DAIx Support (#4) * sushiswap oracle, tellor fallback, events * update tests and readme * eth->dai exchange testing --- 01-Contracts/contracts/StreamExchange.sol | 29 ++-- .../contracts/StreamExchangeHelper.sol | 81 +++++---- .../contracts/StreamExchangeStorage.sol | 1 + 01-Contracts/scripts/distribute.js | 17 +- 01-Contracts/scripts/set-oracle-address.js | 29 ++++ 01-Contracts/test/SteamExchange.test.js | 154 ++---------------- README.md | 7 +- 7 files changed, 116 insertions(+), 202 deletions(-) create mode 100644 01-Contracts/scripts/set-oracle-address.js diff --git a/01-Contracts/contracts/StreamExchange.sol b/01-Contracts/contracts/StreamExchange.sol index 5bcd78b3..1b964e44 100644 --- a/01-Contracts/contracts/StreamExchange.sol +++ b/01-Contracts/contracts/StreamExchange.sol @@ -46,11 +46,7 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor { using StreamExchangeStorage for StreamExchangeStorage.StreamExchange; StreamExchangeStorage.StreamExchange internal _exchange; - // TODO: Emit these events where appropriate - // event StartedInboundStream(address from, uint96 rate); - // event EndedInboundStream(address from, uint96 rate); - // event Distribution(uint256 totalAmount, uint256 feeCollected); - + event UpdatedStream(address from, int96 newRate, int96 totalInflow); constructor( ISuperfluid host, @@ -81,6 +77,7 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor { _exchange.oracle = ITellor(oracle); _exchange.requestId = requestId; _exchange.feeRate = 20000; + _exchange.rateTolerance = 10000; _exchange.subsidyIndexId = 1; _exchange.subsidyRate = 4e17; // 0.4 tokens/second ~ 1,000,000 tokens in a month _exchange.owner = msg.sender; @@ -131,19 +128,13 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor { _exchange.streams[requester].rate = _exchange.streams[requester].rate + changeInFlowRate; - // if (_exchange.streams[requester].rate == 0) { - // // Delete the subscription - // console.log("Deleting subscription"); - // newCtx = _exchange._deleteSubscriptionWithContext(newCtx, address(this), _exchange.outputIndexId, requester, _exchange.outputToken); - // newCtx = _exchange._deleteSubscriptionWithContext(newCtx, address(this), _exchange.subsidyIndexId, requester, _exchange.subsidyToken); - // } else { - // Update the subscription - newCtx = _exchange._updateSubscriptionWithContext(newCtx, _exchange.outputIndexId, requester, uint128(uint(int(_exchange.streams[requester].rate)))/100, _exchange.outputToken); - newCtx = _exchange._updateSubscriptionWithContext(newCtx, _exchange.subsidyIndexId, requester, uint128(uint(int(_exchange.streams[requester].rate)))/100, _exchange.subsidyToken); - // } + newCtx = _exchange._updateSubscriptionWithContext(newCtx, _exchange.outputIndexId, requester, uint128(uint(int(_exchange.streams[requester].rate))), _exchange.outputToken); + newCtx = _exchange._updateSubscriptionWithContext(newCtx, _exchange.subsidyIndexId, requester, uint128(uint(int(_exchange.streams[requester].rate))), _exchange.subsidyToken); _exchange.totalInflow = _exchange.totalInflow + changeInFlowRate; + emit UpdatedStream(requester, _exchange.streams[requester].rate, _exchange.totalInflow); + } @@ -159,6 +150,10 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor { _exchange.feeRate = feeRate; } + function setRateTolerance(uint128 rateTolerance) external onlyOwner { + _exchange.rateTolerance = rateTolerance; + } + function setOracle(address oracle) external onlyOwner { _exchange.oracle = ITellor(oracle); } @@ -223,6 +218,10 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor { return _exchange.feeRate; } + function getRateTolerance() external view returns (uint256) { + return _exchange.rateTolerance; + } + function getStreamRate(address streamer) external view returns (int96) { return _exchange.streams[streamer].rate; } diff --git a/01-Contracts/contracts/StreamExchangeHelper.sol b/01-Contracts/contracts/StreamExchangeHelper.sol index 80db0656..56efb36e 100644 --- a/01-Contracts/contracts/StreamExchangeHelper.sol +++ b/01-Contracts/contracts/StreamExchangeHelper.sol @@ -20,6 +20,10 @@ library StreamExchangeHelper { using SafeERC20 for ERC20; + // TODO: Emit these events where appropriate + event Distribution(uint256 totalAmount, uint256 feeCollected, address token); + + function _getCurrentValue( StreamExchangeStorage.StreamExchange storage self, uint256 _requestId @@ -88,11 +92,13 @@ library StreamExchangeHelper { // Confirm the app has enough to distribute require(self.outputToken.balanceOf(address(this)) >= actualAmount, "!enough"); - newCtx = _idaDistribute(self, self.outputIndexId, uint128(actualAmount), self.outputToken, newCtx); + newCtx = _idaDistribute(self, self.outputIndexId, uint128(distAmount), self.outputToken, newCtx); + emit Distribution(distAmount, feeCollected, address(self.outputToken)); // Distribute a subsidy if possible if(self.subsidyToken.balanceOf(address(this)) >= subsidyAmount) { newCtx = _idaDistribute(self, self.subsidyIndexId, uint128(subsidyAmount), self.subsidyToken, newCtx); + emit Distribution(subsidyAmount, 0, address(self.subsidyToken)); } self.lastDistributionAt = block.timestamp; @@ -100,44 +106,61 @@ library StreamExchangeHelper { // Take the fee ISuperToken(self.outputToken).transfer(self.owner, feeCollected); + require(ISuperToken(self.inputToken).balanceOf(address(this)) == 0, "!sellAllInput"); + + return newCtx; } function _swap( StreamExchangeStorage.StreamExchange storage self, - uint256 amount, + uint256 amount, // Assumes this is outputToken.balanceOf(address(this)) uint256 exchangeRate, uint256 deadline ) public returns(uint) { - uint256 minOutput = amount * 1e18 / exchangeRate / 1e12; - - self.inputToken.downgrade(amount); - address inputToken = self.inputToken.getUnderlyingToken(); - address outputToken = self.outputToken.getUnderlyingToken(); - address[] memory path = new address[](2); - path[0] = inputToken; - path[1] = outputToken; - - // approve the router to spend - ERC20(inputToken).safeIncreaseAllowance(address(self.sushiRouter), amount); - - uint[] memory amounts = self.sushiRouter.swapExactTokensForTokens( - amount, - minOutput, - path, - address(this), - deadline - ); - - ERC20(outputToken).safeIncreaseAllowance(address(self.outputToken), amounts[1]); - self.outputToken.upgrade(amounts[1]); - - // TODO: Take a small fee - - return amounts[1]; - } + address inputToken; // The underlying input token address + address outputToken; // The underlying output token address + address[] memory path; // The path to take + uint256 minOutput; // The minimum amount of output tokens based on Tellor + uint256 outputAmount; // The balance before the swap + + console.log("Amount to swap", amount); + // TODO: This needs to be "invertable" + // minOutput = amount * 1e18 / exchangeRate / 1e12; + minOutput = amount * exchangeRate / 1e6; + console.log("minOutput", minOutput); + minOutput = minOutput * (1e6 - self.rateTolerance) / 1e6; + console.log("minOutput", minOutput); + + self.inputToken.downgrade(amount); + inputToken = self.inputToken.getUnderlyingToken(); + outputToken = self.outputToken.getUnderlyingToken(); + path = new address[](2); + path[0] = inputToken; + path[1] = outputToken; + + // Swap on Sushiswap + ERC20(inputToken).safeIncreaseAllowance(address(self.sushiRouter), amount); + self.sushiRouter.swapExactTokensForTokens( + amount, + 0, // Accept any amount but fail if we're too far from the oracle price + path, + address(this), + deadline + ); + // Assumes `amount` was outputToken.balanceOf(address(this)) + outputAmount = ERC20(outputToken).balanceOf(address(this)); + console.log("outputAmount", outputAmount); + require(outputAmount >= minOutput, "BAD_EXCHANGE_RATE: Try again later"); + + // Convert the outputToken back to its supertoken version + ERC20(outputToken).safeIncreaseAllowance(address(self.outputToken), outputAmount); + self.outputToken.upgrade(outputAmount); + + return outputAmount; + } function _initalizeLiquidityMining(StreamExchangeStorage.StreamExchange storage self) internal { diff --git a/01-Contracts/contracts/StreamExchangeStorage.sol b/01-Contracts/contracts/StreamExchangeStorage.sol index 71bf1418..e632f038 100644 --- a/01-Contracts/contracts/StreamExchangeStorage.sol +++ b/01-Contracts/contracts/StreamExchangeStorage.sol @@ -44,6 +44,7 @@ library StreamExchangeStorage { uint256 requestId; // The id of the tellor request that has input/output exchange rate uint128 feeRate; // The fee taken as a % with 6 decimals address owner; // The owner of the exchange + uint256 rateTolerance; // The percentage to deviate from the oracle scaled to 1e6 } } diff --git a/01-Contracts/scripts/distribute.js b/01-Contracts/scripts/distribute.js index c648f98a..46a53121 100644 --- a/01-Contracts/scripts/distribute.js +++ b/01-Contracts/scripts/distribute.js @@ -1,17 +1,8 @@ async function main() { - const [keeper] = await ethers.getSigners(); + const [owner] = await ethers.getSigners(); - // Update the oracle - const TellorPlayground = await ethers.getContractFactory("TellorPlayground"); - const tp = await TellorPlayground.attach("0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA"); - let o = await tp.submitValue(1, 1000000); - console.log("submitValue:", o); - - const StreamExchangeHelper = await ethers.getContractFactory("StreamExchangeHelper") - const seh = await StreamExchangeHelper.attach("0x0942570634A80bcd096873afC9b112A900492fd7") - console.log("Deployed StreamExchangeHelper ") const StreamExchange = await ethers.getContractFactory("StreamExchange", { libraries: { @@ -20,11 +11,11 @@ async function main() { }); const rickoshea = await StreamExchange.attach("0x7E2E5f06e36da0BA58B08940a72Fd6b68FbDfD61") - // console.log("getOuputToken", await rickoshea.getOuputToken()) - // console.log("getInputToken", await rickoshea.getInputToken()) + console.log("getOuputToken", await rickoshea.getOuputToken()) + console.log("getInputToken", await rickoshea.getInputToken()) - let dr = await rickoshea.distribute(); + // let dr = await rickoshea.distribute(); console.log("Distribute:", dr); diff --git a/01-Contracts/scripts/set-oracle-address.js b/01-Contracts/scripts/set-oracle-address.js new file mode 100644 index 00000000..d14dd04c --- /dev/null +++ b/01-Contracts/scripts/set-oracle-address.js @@ -0,0 +1,29 @@ +async function main() { + + const [keeper] = await ethers.getSigners(); + const TELLOR_CONTRACT_ADDRESS = "0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA" + const STREAM_EXCHANGE_HELPER_ADDRESS = "0x0C7776292AB9E95c54282fD74e47d73338c457D8" + const RICOCHET_CONTRACT_ADDRESS = "0x387af38C133056a0744FB6e823CdB459AE3c5a1f" + + 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 rickoshea = await StreamExchange.attach(RICOCHET_CONTRACT_ADDRESS) + + console.log("getTellorOracle", await rickoshea.getTellorOracle()) + console.log("setOracle", TELLOR_CONTRACT_ADDRESS, await rickoshea.setOracle(TELLOR_CONTRACT_ADDRESS)) + // console.log("getTellorOracle", await rickoshea.getTellorOracle()) + +} + +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 7a3ee3a2..53b44254 100644 --- a/01-Contracts/test/SteamExchange.test.js +++ b/01-Contracts/test/SteamExchange.test.js @@ -97,7 +97,7 @@ describe("StreamExchange", () => { console.log(accounts[i]._address) u[names[i].toLowerCase()] = sf.user({ address: accounts[i]._address || accounts[i].address, - token: daix.address, + token: ethx.address, }); u[names[i].toLowerCase()].alias = names[i]; aliases[u[names[i].toLowerCase()].address] = names[i]; @@ -148,8 +148,8 @@ describe("StreamExchange", () => { app = await StreamExchange.deploy(sf.host.address, sf.agreements.cfa.address, sf.agreements.ida.address, - daix.address, ethx.address, + daix.address, RIC_TOKEN_ADDRESS, SUSHISWAP_ROUTER_ADDRESS, //sr.address, TELLOR_ORACLE_ADDRESS, @@ -171,7 +171,7 @@ describe("StreamExchange", () => { )( sf.agreements.ida.address, sf.agreements.ida.contract.methods - .approveSubscription(ethx.address, app.address, 0, "0x") + .approveSubscription(daix.address, app.address, 0, "0x") .encodeABI(), "0x", // user data { @@ -184,7 +184,7 @@ describe("StreamExchange", () => { )( sf.agreements.ida.address, sf.agreements.ida.contract.methods - .approveSubscription(ethx.address, app.address, 0, "0x") + .approveSubscription(daix.address, app.address, 0, "0x") .encodeABI(), "0x", // user data { @@ -198,7 +198,7 @@ describe("StreamExchange", () => { )( sf.agreements.ida.address, sf.agreements.ida.contract.methods - .approveSubscription(ethx.address, app.address, 0, "0x") + .approveSubscription(daix.address, app.address, 0, "0x") .encodeABI(), "0x", // user data { @@ -374,8 +374,8 @@ describe("StreamExchange", () => { // Check setup expect(await app.isAppJailed()).to.equal(false) - expect(await app.getInputToken()).to.equal(daix.address) - expect(await app.getOuputToken()).to.equal(ethx.address) + expect(await app.getInputToken()).to.equal(ethx.address) + expect(await app.getOuputToken()).to.equal(daix.address) expect(await app.getOuputIndexId()).to.equal(0) expect(await app.getSubsidyToken()).to.equal(ric.address) expect(await app.getSubsidyIndexId()).to.equal(1) @@ -395,121 +395,10 @@ describe("StreamExchange", () => { expect(await app.getFeeRate()).to.equal(20000) console.log("Getters and setters correct") -<<<<<<< HEAD - // Give alice and bob some DAIx - await daix.transfer(u.bob.address, "3000000000000000000", {from: u.admin.address}); - await daix.transfer(u.alice.address, "3000000000000000000", {from: u.admin.address}); - - const inflowRate = toWad(0.00004000); - // Test 1 streamer - let bobInitialEth = await ethx.balanceOf(u.bob.address) - let bobInitialDai = await daix.balanceOf(u.bob.address) - await u.bob.flow({ flowRate: inflowRate, recipient: u.app }); - // Go forward in time - console.log("Go forward in time") - await traveler.advanceTimeAndBlock(3600*2); - await tp.submitValue(1, oraclePrice); - let bobFinalDai = await daix.balanceOf(u.bob.address) - await app.distribute() - let bobFinalEth = await ethx.balanceOf(u.bob.address) - - console.log("Bob Initial ETH:", bobInitialEth.toString()) - console.log("Bob Initial Dai:", bobInitialDai.toString()) - console.log("Bob Final ETH:", bobFinalEth.toString()) - console.log("Bob Final Dai:", bobFinalDai.toString()) - console.log("Exhcange rate:", (bobInitialDai - bobFinalDai) / (bobFinalEth - bobInitialEth)) - - - let aliceInitialEth = await ethx.balanceOf(u.alice.address) - let aliceInitialDai = await daix.balanceOf(u.alice.address) - await u.alice.flow({ flowRate: inflowRate, recipient: u.app }); - // Go forward in time - console.log("Go forward in time") - await traveler.advanceTimeAndBlock(3600*2); - await tp.submitValue(1, oraclePrice); - let aliceFinalDai = await daix.balanceOf(u.alice.address) - await app.distribute() - let aliceFinalEth = await ethx.balanceOf(u.alice.address) - - console.log("alice Initial ETH:", aliceInitialEth.toString()) - console.log("alice Initial Dai:", aliceInitialDai.toString()) - console.log("alice Final ETH:", aliceFinalEth.toString()) - console.log("alice Final Dai:", aliceFinalDai.toString()) - console.log("Exhcange rate:", (aliceInitialDai - aliceFinalDai) / (aliceFinalEth - aliceInitialEth)) - - - // For holding measurements - var appBalances = {ethx: [], daix: [], ric: []} - var ownerBalances = {ethx: [], daix: [], ric: []} - var aliceBalances = {ethx: [], daix: [], ric: []} - var bobBalances = {ethx: [], daix: [], ric: []} - - - // Alice and Bob start streaming to the app - await u.bob.flow({ flowRate: inflowRate, recipient: u.app }); - await u.alice.flow({ flowRate: inflowRate*2, recipient: u.app }); - - ownerBalances.ethx.push(await ethx.balanceOf(u.admin.address)); - aliceBalances.ethx.push(await ethx.balanceOf(u.alice.address)); - bobBalances.ethx.push(await ethx.balanceOf(u.bob.address)); - aliceBalances.daix.push(await daix.balanceOf(u.alice.address)); - bobBalances.daix.push(await daix.balanceOf(u.bob.address)); - appBalances.daix.push(await daix.balanceOf(u.app.address)); - - // Go forward in time - console.log("Go forward in time") - await traveler.advanceTimeAndBlock(3600*5); - await tp.submitValue(1, oraclePrice); - - aliceBalances.daix.push(await daix.balanceOf(u.alice.address)); - bobBalances.daix.push(await daix.balanceOf(u.bob.address)); - appBalances.daix.push(await daix.balanceOf(u.app.address)); + const inflowRate = toWad(0.0000004000); - let bobDeltaDaix = bobBalances.daix[0] - bobBalances.daix[1] - let aliceDeltaDaix = aliceBalances.daix[0] - aliceBalances.daix[1] - let appDeltaDaix = appBalances.daix[1] - appBalances.daix[0] - console.log("APP DAIX BAL: ", appDeltaDaix) - expect(appDeltaDaix).to.equal(bobDeltaDaix + aliceDeltaDaix, "DIA lost during streaming"); - - await app.distribute() - - ownerBalances.ethx.push(await ethx.balanceOf(u.admin.address)); - aliceBalances.ethx.push(await ethx.balanceOf(u.alice.address)); - bobBalances.ethx.push(await ethx.balanceOf(u.bob.address)); - - let bobDeltaEthx = bobBalances.ethx[0] - bobBalances.ethx[1] - let aliceDeltaEthx = aliceBalances.ethx[0] - aliceBalances.ethx[1] - let ownerDeltaEthx = ownerBalances.ethx[0] - ownerBalances.ethx[1] - let appBalance = await ethx.balanceOf(app.address) - - console.log("Exchange bob rate", bobDeltaDaix / bobDeltaEthx) - console.log("Exchange alice rate", aliceDeltaDaix / aliceDeltaEthx) - console.log("App balance", appBalance.toString()) - console.log("Alice delta", aliceDeltaEthx) - console.log("Bob delta", bobDeltaEthx) - console.log("Alice deltaDaix", aliceDeltaDaix) - console.log("Bob deltaDaix", bobDeltaDaix) - console.log("Owner delta", ownerDeltaEthx) - console.log("Fee rate", ownerDeltaEthx / (aliceDeltaEthx + bobDeltaEthx + ownerDeltaEthx)) - - // Go forward in time - console.log("Go forward in time") - await traveler.advanceTimeAndBlock(3600*2); - await tp.submitValue(1, oraclePrice); - - aliceBalances.daix.push(await daix.balanceOf(u.alice.address)); - bobBalances.daix.push(await daix.balanceOf(u.bob.address)); - appBalances.daix.push(await daix.balanceOf(u.app.address)); - - bobDeltaDaix = bobBalances.daix[1] - bobBalances.daix[2] - aliceDeltaDaix = aliceBalances.daix[1] - aliceBalances.daix[2] - appDeltaDaix = appBalances.daix[2] - appBalances.daix[1] - -======= - const inflowRate = toWad(0.00004000); - - await daix.transfer(u.bob.address, "3000000000000000000", {from: u.admin.address}); - await daix.transfer(u.alice.address, "3000000000000000000", {from: u.admin.address}); + await ethx.transfer(u.bob.address, "100000000000000000", {from: u.admin.address}); + await ethx.transfer(u.alice.address, "100000000000000000", {from: u.admin.address}); await tp.submitValue(1, oraclePrice); @@ -542,7 +431,7 @@ describe("StreamExchange", () => { // Round 4 - await u.alice.flow({ flowRate: "0", recipient: u.app }); + // await u.alice.flow({ flowRate: "0", recipient: u.app }); await traveler.advanceTimeAndBlock(60*60*2); await tp.submitValue(1, oraclePrice); await app.distribute() @@ -553,33 +442,12 @@ describe("StreamExchange", () => { // Round 5 await traveler.advanceTimeAndBlock(60*60*2); await tp.submitValue(1, oraclePrice); ->>>>>>> v1.0 await app.distribute() await takeMeasurements() await delta("Bob", bobBalances) await delta("Alice", aliceBalances) - -<<<<<<< HEAD - aliceBalances.ethx.push(await ethx.balanceOf(u.alice.address)); - bobBalances.ethx.push(await ethx.balanceOf(u.bob.address)); - - bobDeltaEthx = bobBalances.ethx[1] - bobBalances.ethx[2] - aliceDeltaEthx = aliceBalances.ethx[1] - aliceBalances.ethx[2] - - console.log("Alice delta", aliceDeltaEthx) - console.log("Bob delta", bobDeltaEthx) - console.log("Alice deltaDaix", aliceDeltaDaix) - console.log("Bob deltaDaix", bobDeltaDaix) - console.log("Owner delta", ownerDeltaEthx) -======= ->>>>>>> v1.0 - - console.log("Exchange bob rate", bobDeltaDaix / bobDeltaEthx) - console.log("Exchange alice rate", aliceDeltaDaix / aliceDeltaEthx) - - }); }); diff --git a/README.md b/README.md index 3632ada1..8d770c26 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # Ricochet Ricochet helps you DCA into ETH using Superfluid's ETHx and USDCx. You open a stream of USDCx to Ricochet and you will receive a distribution of ETHx periodically. In this way, you DCA into ETHx using just a single transaction. +## Getting Started +1. Install Hardhat +2. `npm install` +3. `npx hardhat test` + ## Why Ricochet? Ricochet only requires one DCA transaction per period for all users, reducing collective transaction quantity exponentially. ## Architecture ![Architecture](./00-Meta/arch.png) - -