Skip to content

Commit 7d35473

Browse files
authored
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
1 parent 194af54 commit 7d35473

File tree

9 files changed

+147
-23
lines changed

9 files changed

+147
-23
lines changed

00-Meta/Specification.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Ricochet Exchange v2 (Draft)
2+
3+
## Overview
4+
* Ricochet is a stream exchange that uses Superfluid
5+
* Each `RicochetExchange` contract supports two-way streaming swaps
6+
* Streamers can stream either `tokenA` or `tokenB` at any rate and RicochetExchange keepers will trigger swaps periodically
7+
* `RicochetExchange` will perform the following algorithm to swap:
8+
* Determine the `surplusToken`, which of tokenA or tokenB there's an excess amount of the other token available to swap with
9+
* Example: consider there is $100 DAI and $50 ETH that's been streamed into the contract
10+
* ETH is the `surplusToken` because there's _more_ than enough DAI to make the swap
11+
* Set the other token as the `deficitToken`
12+
* Consider the above example, DAI is the deficitToken because there's not enough ETH to swap for DAI
13+
* Next, perform the internal swap, swap the `surplusToken` for the `deficitToken` at the current `exchangeRate`
14+
* Then, take the remaining amount of the `deficitToken` and swap on Sushiswap
15+
* Finally, distribute to `tokenA` and `tokenB` their tokens
16+
17+
## Protocol Speciciations
18+
19+
### Structures
20+
* `Oracle`
21+
* `ITellor oracle` - Address of deployed simple oracle for input//output token
22+
* `uint256 requestId` - The id of the tellor request that has input/output exchange rate
23+
* `uint256 rateTolerance` - The percentage to deviate from the oracle scaled to 1e6
24+
25+
* `Exchange`
26+
* `ISuperfluid host` - Superfluid host contract
27+
* `IConstantFlowAgreementV1 cfa` - The stored constant flow agreement class address
28+
* `IInstantDistributionAgreementV1 ida` - The stored instant dist. agreement class address
29+
* `ISuperToken tokenA` - One of the tokens supported for streaming
30+
* `ISuperToken tokenB` - The other one of the tokens supported for streaming
31+
* `int96 totalInflow` - The fee taken as a % with 6 decimals
32+
* `uint128 feeRate` - The fee taken as a % with 6 decimals
33+
* `IUniswapV2Router02 sushiRouter` - Address of sushsiwap router to use for swapping
34+
* `Oracle oracle` - The oracle to use for the exchange

01-Contracts/arguments.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
// Polygon Mainnet
3+
const HOST_ADDRESS = "0x3E14dC1b13c488a8d5D310918780c983bD5982E7";
4+
const CFA_ADDRESS = "0x6EeE6060f715257b970700bc2656De21dEdF074C";
5+
const IDA_ADDRESS = "0xB0aABBA4B2783A72C52956CDEF62d438ecA2d7a1";
6+
const DAIX_ADDRESS = "0x1305F6B6Df9Dc47159D12Eb7aC2804d4A33173c2";
7+
const ETHX_ADDRESS = "0x27e1e4E6BC79D93032abef01025811B7E4727e85";
8+
const SUSHISWAP_ROUTER_ADDRESS = "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506";
9+
const TELLOR_ORACLE_ADDRESS = "0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA";
10+
const RIC_CONTRACT_ADDRESS = "0x263026e7e53dbfdce5ae55ade22493f828922965";
11+
const TELLOR_REQUEST_ID = 1;
12+
13+
14+
module.exports = [
15+
HOST_ADDRESS,
16+
CFA_ADDRESS,
17+
IDA_ADDRESS,
18+
process.env.INPUT_TOKEN_ADDRESS,
19+
process.env.OUTPUT_TOKEN_ADDRESS,
20+
RIC_CONTRACT_ADDRESS,
21+
SUSHISWAP_ROUTER_ADDRESS,
22+
TELLOR_ORACLE_ADDRESS,
23+
TELLOR_REQUEST_ID,
24+
process.env.SF_REG_KEY
25+
];

01-Contracts/contracts/StreamExchange.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,12 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor {
118118

119119
newCtx = ctx;
120120

121-
// NOTE: Trigger a distribution if there's any inputToken
122-
if (ISuperToken(_exchange.inputToken).balanceOf(address(this)) > 0 && doDistributeFirst) {
121+
(, , uint128 totalUnitsApproved, uint128 totalUnitsPending) = _exchange.ida.getIndex(
122+
_exchange.outputToken,
123+
address(this),
124+
_exchange.outputIndexId);
125+
126+
if (doDistributeFirst && totalUnitsApproved + totalUnitsPending > 0 && ISuperToken(_exchange.inputToken).balanceOf(address(this)) > 0) {
123127
newCtx = _exchange._distribute(newCtx);
124128
}
125129

01-Contracts/contracts/StreamExchangeHelper.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,16 @@ library StreamExchangeHelper {
8282
if (actualAmount == 0) { return newCtx; }
8383

8484
// Calculate the fee for making the distribution
85-
uint256 feeCollected = outputBalance * self.feeRate / 1e6;
86-
uint256 distAmount = outputBalance - feeCollected;
85+
uint256 feeCollected = actualAmount * self.feeRate / 1e6;
86+
uint256 distAmount = actualAmount - feeCollected;
8787

8888

8989
// Calculate subside
9090
uint256 subsidyAmount = (block.timestamp - self.lastDistributionAt) * self.subsidyRate;
9191

9292
// Confirm the app has enough to distribute
9393
require(self.outputToken.balanceOf(address(this)) >= actualAmount, "!enough");
94-
94+
console.log("distAmount", distAmount);
9595
newCtx = _idaDistribute(self, self.outputIndexId, uint128(distAmount), self.outputToken, newCtx);
9696
emit Distribution(distAmount, feeCollected, address(self.outputToken));
9797

@@ -132,7 +132,7 @@ library StreamExchangeHelper {
132132
minOutput = amount * exchangeRate / 1e6;
133133
console.log("minOutput", minOutput);
134134
minOutput = minOutput * (1e6 - self.rateTolerance) / 1e6;
135-
console.log("minOutput", minOutput);
135+
console.log("minOutput after rate tolerance", minOutput);
136136

137137
self.inputToken.downgrade(amount);
138138
inputToken = self.inputToken.getUnderlyingToken();

01-Contracts/hardhat.config.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ require("@nomiclabs/hardhat-web3");
33
require('@nomiclabs/hardhat-ethers');
44
require('@openzeppelin/hardhat-upgrades');
55
require('hardhat-contract-sizer');
6+
require("@nomiclabs/hardhat-etherscan");
67

78
// This is a sample Hardhat task. To learn how to create your own go to
89
// https://hardhat.org/guides/create-task.html
@@ -26,12 +27,12 @@ module.exports = {
2627
timeout: 100000
2728
},
2829
networks: {
29-
// polygon: {
30-
// url: process.env.POLYGON_QUIKNODE_URL,
31-
// accounts: [process.env.PRIVATE_KEY],
32-
// gas: 2000000,
33-
// gasPrice: 2000000000
34-
// },
30+
polygon: {
31+
url: "https://polygon-mainnet.infura.io/v3/" + process.env.INFURA_KEY,
32+
accounts: [process.env.MATIC_PRIVATE_KEY],
33+
gas: 2000000,
34+
gasPrice: 20000000000
35+
},
3536
rinkeby: {
3637
url: "https://rinkeby.infura.io/v3/" + process.env.INFURA_KEY,
3738
accounts: [process.env.PRIVATE_KEY],
@@ -44,11 +45,16 @@ module.exports = {
4445
// },
4546
hardhat: {
4647
forking: {
47-
url: "https://rinkeby.infura.io/v3/" + process.env.INFURA_KEY,
48+
url: process.env.QUICK_NODE_URL,
4849
accounts: [process.env.PRIVATE_KEY_ADMIN, process.env.PRIVATE_KEY_ALICE, process.env.PRIVATE_KEY_BOB],
4950
}
5051
}
5152
},
53+
etherscan: {
54+
// Your API key for Etherscan
55+
// Obtain one at https://etherscan.io/
56+
apiKey: process.env.POLYSCAN_API_KEY
57+
},
5258
contractSizer: {
5359
alphaSort: true,
5460
runOnCompile: true,

01-Contracts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
"license": "ISC",
1212
"devDependencies": {
1313
"@nomiclabs/hardhat-ethers": "^2.0.2",
14+
"@nomiclabs/hardhat-etherscan": "^2.1.4",
1415
"@nomiclabs/hardhat-waffle": "^2.0.1",
1516
"@nomiclabs/hardhat-web3": "^2.0.0",
1617
"@openzeppelin/contracts": "^4.0.0",
18+
"@openzeppelin/hardhat-upgrades": "^1.9.0",
1719
"@superfluid-finance/ethereum-contracts": "^1.0.0-rc.5",
1820
"@truffle/contract": "^4.3.23",
1921
"axios": "^0.21.1",
@@ -26,7 +28,6 @@
2628
},
2729
"dependencies": {
2830
"@openzeppelin/contracts-upgradeable": "^4.1.0",
29-
"@openzeppelin/hardhat-upgrades": "^1.8.2",
3031
"@uniswap/v2-core": "^1.0.1",
3132
"@uniswap/v2-periphery": "^1.1.0-beta.0",
3233
"hardhat-contract-sizer": "^2.0.3",

01-Contracts/scripts/deploy-polygon.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,24 @@ async function main() {
4646
console.log("\tHOST_ADDRESS", HOST_ADDRESS)
4747
console.log("\tCFA_ADDRESS", CFA_ADDRESS)
4848
console.log("\tIDA_ADDRESS", IDA_ADDRESS)
49-
console.log("\tDAIX_ADDRESS",DAIX_ADDRESS)
50-
console.log("\tETHX_ADDRESS", ETHX_ADDRESS)
49+
console.log("\tINPUT_TOKEN", process.env.INPUT_TOKEN_ADDRESS)
50+
console.log("\tOUTPUT_TOKEN", process.env.OUTPUT_TOKEN_ADDRESS)
5151
console.log("\tSUSHISWAP_ROUTER_ADDRESS", SUSHISWAP_ROUTER_ADDRESS)
5252
console.log("\tTELLOR_ORACLE_ADDRESS", TELLOR_ORACLE_ADDRESS)
5353
console.log("\tTELLOR_REQUEST_ID", TELLOR_REQUEST_ID)
54+
55+
56+
5457
const streamExchange = await StreamExchange.deploy( HOST_ADDRESS,
5558
CFA_ADDRESS,
5659
IDA_ADDRESS,
57-
DAIX_ADDRESS,
58-
ETHX_ADDRESS,
60+
process.env.INPUT_TOKEN_ADDRESS,
61+
process.env.OUTPUT_TOKEN_ADDRESS,
5962
RIC_CONTRACT_ADDRESS,
6063
SUSHISWAP_ROUTER_ADDRESS,
6164
TELLOR_ORACLE_ADDRESS,
6265
TELLOR_REQUEST_ID,
63-
"ricochet23" );
66+
process.env.SF_REG_KEY );
6467
await streamExchange.deployed();
6568
console.log("Deployed StreamExchange at address:", streamExchange.address);
6669
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
async function main() {
2+
3+
const [keeper] = await ethers.getSigners();
4+
const RATE_TOLERANCE = "20000"
5+
const STREAM_EXCHANGE_HELPER_ADDRESS = "0x0C7776292AB9E95c54282fD74e47d73338c457D8"
6+
const RICOCHET_CONTRACT_ADDRESS = "0xe0B7907FA4B759FA4cB201F0E02E16374Bc523fd"
7+
8+
const StreamExchangeHelper = await ethers.getContractFactory("StreamExchangeHelper")
9+
const seh = await StreamExchangeHelper.attach(STREAM_EXCHANGE_HELPER_ADDRESS)
10+
11+
const StreamExchange = await ethers.getContractFactory("StreamExchange", {
12+
libraries: {
13+
StreamExchangeHelper: seh.address,
14+
},
15+
});
16+
const ricochet = await StreamExchange.attach(RICOCHET_CONTRACT_ADDRESS)
17+
18+
console.log("rateTolerance", await ricochet.getRateTolerance())
19+
console.log("setRateTolerance", RATE_TOLERANCE, await ricochet.setRateTolerance(RATE_TOLERANCE))
20+
21+
}
22+
23+
main()
24+
.then(() => process.exit(0))
25+
.catch(error => {
26+
console.error(error);
27+
process.exit(1);
28+
});

01-Contracts/test/SteamExchange.test.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe("StreamExchange", () => {
5050
before(async function () {
5151
//process.env.RESET_SUPERFLUID_FRAMEWORK = 1;
5252
let response = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd')
53-
oraclePrice = parseInt(response.data.ethereum.usd * 1.002 * 1000000).toString()
53+
oraclePrice = parseInt(response.data.ethereum.usd * 1.005 * 1000000).toString()
5454
console.log("oraclePrice", oraclePrice)
5555
});
5656

@@ -389,28 +389,43 @@ describe("StreamExchange", () => {
389389
expect(await app.getFeeRate()).to.equal(20000)
390390

391391
await app.connect(owner).setFeeRate(20000);
392+
await app.connect(owner).setRateTolerance(50000);
392393
await app.connect(owner).setSubsidyRate("500000000000000000")
393394

394395
expect(await app.getSubsidyRate()).to.equal("500000000000000000")
395396
expect(await app.getFeeRate()).to.equal(20000)
397+
expect(await app.getRateTolerance()).to.equal(50000)
396398
console.log("Getters and setters correct")
397399

398-
const inflowRate = toWad(0.0000004000);
400+
const inflowRate = toWad(0.00000004000);
399401

400-
await ethx.transfer(u.bob.address, "100000000000000000", {from: u.admin.address});
401-
await ethx.transfer(u.alice.address, "100000000000000000", {from: u.admin.address});
402+
console.log("Transfer bob")
403+
await ethx.transfer(u.bob.address, "7000000000000000", {from: u.admin.address});
404+
console.log("Transfer aliuce")
405+
await ethx.transfer(u.alice.address, "7000000000000000", {from: u.admin.address});
406+
console.log("Done")
402407

403408
await tp.submitValue(1, oraclePrice);
404409

405410
await takeMeasurements();
406411

412+
// Test owner start/stop stream
413+
await u.admin.flow({ flowRate: inflowRate, recipient: u.app });
414+
await traveler.advanceTimeAndBlock(60*60*3);
415+
await tp.submitValue(1, oraclePrice);
416+
await app.distribute()
417+
await u.admin.flow({ flowRate: "0", recipient: u.app });
418+
419+
420+
407421
await u.bob.flow({ flowRate: inflowRate, recipient: u.app });
408422
await traveler.advanceTimeAndBlock(60*60*3);
409423
await tp.submitValue(1, oraclePrice);
410424
await app.distribute()
411425
await takeMeasurements();
412426
await delta("Bob", bobBalances)
413427
await delta("Alice", aliceBalances)
428+
await delta("Owner", ownerBalances)
414429

415430
// Round 2
416431
await u.alice.flow({ flowRate: inflowRate, recipient: u.app });
@@ -420,6 +435,8 @@ describe("StreamExchange", () => {
420435
await takeMeasurements()
421436
await delta("Bob", bobBalances)
422437
await delta("Alice", aliceBalances)
438+
await delta("Owner", ownerBalances)
439+
423440

424441
// Round 3
425442
await traveler.advanceTimeAndBlock(60*60*2);
@@ -428,6 +445,8 @@ describe("StreamExchange", () => {
428445
await takeMeasurements()
429446
await delta("Bob", bobBalances)
430447
await delta("Alice", aliceBalances)
448+
await delta("Owner", ownerBalances)
449+
431450

432451

433452
// Round 4
@@ -438,6 +457,8 @@ describe("StreamExchange", () => {
438457
await takeMeasurements()
439458
await delta("Bob", bobBalances)
440459
await delta("Alice", aliceBalances)
460+
await delta("Owner", ownerBalances)
461+
441462

442463
// Round 5
443464
await traveler.advanceTimeAndBlock(60*60*2);
@@ -446,6 +467,8 @@ describe("StreamExchange", () => {
446467
await takeMeasurements()
447468
await delta("Bob", bobBalances)
448469
await delta("Alice", aliceBalances)
470+
await delta("Owner", ownerBalances)
471+
449472

450473

451474
});

0 commit comments

Comments
 (0)