Skip to content

Commit

Permalink
v1.3: Keeper controls to protect streamers (#12)
Browse files Browse the repository at this point in the history
* Added close stream by keeper (#11)

* Added close stream by keeper

* fixed stray typos

* don't allow opening streams without > 8hours of stream balance

* support non-18 decimal underlying tokens (USDC)

* fix typo in name

* add script for transfer ownership

* shorten delay for more test loops

* work in progress on close stream keeper

* Added logging, testing for USDCx decimals

* Added unlimited allowance code along with test statements (#22)

* more WIP on the keeper

* don't downgrade twice

* misc. fixes for decimal tokens

* remove sloppy comments

Co-authored-by: Chinmay Sai Vemuri <55590938+rashtrakoff@users.noreply.github.com>
  • Loading branch information
mikeghen and rashtrakoff authored Aug 16, 2021
1 parent 7d35473 commit 30ced42
Show file tree
Hide file tree
Showing 26 changed files with 2,938 additions and 225 deletions.
11 changes: 2 additions & 9 deletions 01-Contracts/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,2 @@
# Rickoshea Contracts
This directory contains the contracts for Rickshea's `StreamExchange`.

# Network directory
## Rinkeby
| Contract | Address |
|----------|---------|
| TellorPlayground | 0xA0c5d95ec359f4A33371a06C23D89BA6Fc591A97 |
| StreamExchange | 0x3B3775eB7D4EFb5122Bde89B52E9A1a3813bB4F9 |
# Ricochet Contracts
This directory contains the contracts for Ricochet's `StreamExchange`.
4 changes: 2 additions & 2 deletions 01-Contracts/arguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ const IDA_ADDRESS = "0xB0aABBA4B2783A72C52956CDEF62d438ecA2d7a1";
const DAIX_ADDRESS = "0x1305F6B6Df9Dc47159D12Eb7aC2804d4A33173c2";
const ETHX_ADDRESS = "0x27e1e4E6BC79D93032abef01025811B7E4727e85";
const SUSHISWAP_ROUTER_ADDRESS = "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506";
const TELLOR_ORACLE_ADDRESS = "0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA";
const TELLOR_ORACLE_ADDRESS = "0xACC2d27400029904919ea54fFc0b18Bf07C57875";
const RIC_CONTRACT_ADDRESS = "0x263026e7e53dbfdce5ae55ade22493f828922965";
const TELLOR_REQUEST_ID = 1;
const TELLOR_REQUEST_ID = 60;


module.exports = [
Expand Down
62 changes: 45 additions & 17 deletions 01-Contracts/contracts/StreamExchange.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
pragma abicoder v2;

// import "hardhat/console.sol";

Expand Down Expand Up @@ -82,6 +82,13 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor {
_exchange.subsidyRate = 4e17; // 0.4 tokens/second ~ 1,000,000 tokens in a month
_exchange.owner = msg.sender;

// Unlimited approve for sushiswap
ERC20(_exchange.inputToken.getUnderlyingToken()).safeIncreaseAllowance(address(_exchange.sushiRouter), 2**256 - 1);
ERC20(_exchange.outputToken.getUnderlyingToken()).safeIncreaseAllowance(address(_exchange.sushiRouter), 2**256 - 1);
// and Supertoken upgrades
ERC20(_exchange.inputToken.getUnderlyingToken()).safeIncreaseAllowance(address(_exchange.inputToken), 2**256 - 1);
ERC20(_exchange.outputToken.getUnderlyingToken()).safeIncreaseAllowance(address(_exchange.outputToken), 2**256 - 1);

uint256 configWord =
SuperAppDefinitions.APP_LEVEL_FINAL |
SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP |
Expand Down Expand Up @@ -132,6 +139,9 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor {

_exchange.streams[requester].rate = _exchange.streams[requester].rate + changeInFlowRate;

// Make sure the requester has at least 8 hours of balance to stream
require(int(_exchange.inputToken.balanceOf(requester)) >= _exchange.streams[requester].rate * 8 hours, "!enoughTokens");

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);

Expand All @@ -146,6 +156,14 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor {
_exchange._distribute(new bytes(0));
}

function closeStream(address streamer) public {
_exchange._closeStream(streamer);
}

function emergencyCloseStream(address streamer) public {
_exchange._emergencyCloseStream(streamer);
}

function setSubsidyRate(uint128 subsidyRate) external onlyOwner {
_exchange.subsidyRate = subsidyRate;
}
Expand All @@ -170,6 +188,31 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor {
return _exchange.host.isAppJailed(this);
}

function getIDAShares(uint32 index, address streamer) external view returns (bool exist,
bool approved,
uint128 units,
uint256 pendingDistribution) {

ISuperToken idaToken;
if(index == _exchange.outputIndexId) {

idaToken = _exchange.outputToken;

} else if (index == _exchange.subsidyIndexId) {

idaToken = _exchange.subsidyToken;

} else {
return (exist, approved, units, pendingDistribution);
}

(exist, approved, units, pendingDistribution) = _exchange.ida.getSubscription(
idaToken,
address(this),
index,
streamer);
}

function getInputToken() external view returns (ISuperToken) {
return _exchange.inputToken;
}
Expand Down Expand Up @@ -230,22 +273,7 @@ contract StreamExchange is Ownable, SuperAppBase, UsingTellor {
return _exchange.streams[streamer].rate;
}

function emergencyCloseStream(address streamer) public {
// Allows anyone to close any stream iff the app is jailed
bool isJailed = ISuperfluid(msg.sender).isAppJailed(ISuperApp(address(this)));
require(isJailed, "!jailed");
_exchange.host.callAgreement(
_exchange.cfa,
abi.encodeWithSelector(
_exchange.cfa.deleteFlow.selector,
_exchange.inputToken,
streamer,
address(this),
new bytes(0) // placeholder
),
"0x"
);
}


/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
Expand Down
106 changes: 86 additions & 20 deletions 01-Contracts/contracts/StreamExchangeHelper.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
pragma abicoder v2;

import "hardhat/console.sol";

import {
ISuperfluid,
ISuperToken,
ISuperToken,
ISuperAgreement
} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";
Expand All @@ -24,6 +26,49 @@ library StreamExchangeHelper {
event Distribution(uint256 totalAmount, uint256 feeCollected, address token);


function _closeStream(StreamExchangeStorage.StreamExchange storage self, address streamer) public {
// Only closable iff their balance is less than 8 hours of streaming
require(int(self.inputToken.balanceOf(streamer)) <= self.streams[streamer].rate * 8 hours,
"!closable");

self.streams[streamer].rate = 0;

// Update Subscriptions
_updateSubscription(self, self.subsidyIndexId, streamer, 0, self.subsidyToken);
_updateSubscription(self, self.outputIndexId, streamer, 0, self.outputToken);

// Close the streamers stream
self.host.callAgreement(
self.cfa,
abi.encodeWithSelector(
self.cfa.deleteFlow.selector,
self.inputToken,
streamer,
address(this),
new bytes(0) // placeholder
),
"0x"
);

}

function _emergencyCloseStream(StreamExchangeStorage.StreamExchange storage self, address streamer) public {
// Allows anyone to close any stream iff the app is jailed
bool isJailed = ISuperfluid(msg.sender).isAppJailed(ISuperApp(address(this)));
require(isJailed, "!jailed");
self.host.callAgreement(
self.cfa,
abi.encodeWithSelector(
self.cfa.deleteFlow.selector,
self.inputToken,
streamer,
address(this),
new bytes(0) // placeholder
),
"0x"
);
}

function _getCurrentValue(
StreamExchangeStorage.StreamExchange storage self,
uint256 _requestId
Expand Down Expand Up @@ -78,13 +123,20 @@ library StreamExchangeHelper {
self.outputIndexId,
outputBalance);

console.log("outputBalance", outputBalance);
console.log("actualAmount", actualAmount);

// Return if there's not anything to actually distribute
if (actualAmount == 0) { return newCtx; }

// Calculate the fee for making the distribution
uint256 feeCollected = actualAmount * self.feeRate / 1e6;
uint256 distAmount = actualAmount - feeCollected;

console.log("feeCollected", feeCollected);
console.log("distAmount", distAmount);
console.log("Fee rate:", feeCollected * 10000 / (feeCollected + distAmount));


// Calculate subside
uint256 subsidyAmount = (block.timestamp - self.lastDistributionAt) * self.subsidyRate;
Expand All @@ -105,8 +157,10 @@ library StreamExchangeHelper {

// Take the fee
ISuperToken(self.outputToken).transfer(self.owner, feeCollected);

require(ISuperToken(self.inputToken).balanceOf(address(this)) == 0, "!sellAllInput");
// NOTE: After swapping any token with < 18 decimals, there may be dust left so just leave it
require(self.inputToken.balanceOf(address(this)) /
10 ** (18 - ERC20(self.inputToken.getUnderlyingToken()).decimals()) == 0,
"!sellAllInput");


return newCtx;
Expand All @@ -126,23 +180,38 @@ library StreamExchangeHelper {
uint256 minOutput; // The minimum amount of output tokens based on Tellor
uint256 outputAmount; // The balance before the swap

console.log("Amount to swap", amount);
console.log("amount", amount);

inputToken = self.inputToken.getUnderlyingToken();
outputToken = self.outputToken.getUnderlyingToken();

// Downgrade and scale the input amount
self.inputToken.downgrade(amount);
// Scale it to 1e18 for calculations
amount = ERC20(inputToken).balanceOf(address(this)) * (10 ** (18 - ERC20(inputToken).decimals()));

// TODO: This needs to be "invertable"
// minOutput = amount * 1e18 / exchangeRate / 1e12;
minOutput = amount * exchangeRate / 1e6;
// USD >> TOK
minOutput = amount * 1e18 / exchangeRate / 1e12;
console.log("minOutput", minOutput);
// TOK >> USD
// minOutput = amount * exchangeRate / 1e6;
minOutput = minOutput * (1e6 - self.rateTolerance) / 1e6;
console.log("minOutput after rate tolerance", minOutput);
console.log("minOutput", minOutput);

// Scale back from 1e18 to outputToken decimals
minOutput = minOutput * (10 ** (ERC20(outputToken).decimals())) / 1e18;
// Scale it back to inputToken decimals
amount = amount / (10 ** (18 - ERC20(inputToken).decimals()));


console.log("exchangeRate", exchangeRate);
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
Expand All @@ -156,8 +225,9 @@ library StreamExchangeHelper {
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);
self.outputToken.upgrade(outputAmount * (10 ** (18 - ERC20(outputToken).decimals())));
console.log(ERC20(outputToken).balanceOf(address(this)));


return outputAmount;
}
Expand Down Expand Up @@ -228,7 +298,7 @@ library StreamExchangeHelper {
index,
// one share for the to get it started
subscriber,
shares,
shares / 1e9,
new bytes(0) // placeholder ctx
),
new bytes(0) // user data
Expand All @@ -252,7 +322,7 @@ library StreamExchangeHelper {
distToken,
index,
subscriber,
shares, // Number of shares is proportional to their rate
shares / 1e9, // Number of shares is proportional to their rate
new bytes(0)
),
new bytes(0), // user data
Expand Down Expand Up @@ -285,10 +355,6 @@ library StreamExchangeHelper {
}






/**************************************************************************
* SuperApp callbacks
*************************************************************************/
Expand Down
3 changes: 3 additions & 0 deletions 01-Contracts/libraries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
StreamExchangeHelper: "0x5B01d5352A50d0dBB37c4bCFf3886b6C1D6C577a",
};
1 change: 1 addition & 0 deletions 01-Contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"dependencies": {
"@openzeppelin/contracts-upgradeable": "^4.1.0",
"@truffle/abi-utils": "^0.2.3",
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"hardhat-contract-sizer": "^2.0.3",
Expand Down
4 changes: 2 additions & 2 deletions 01-Contracts/scripts/deploy-polygon.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ async function main() {
const DAIX_ADDRESS = "0x1305F6B6Df9Dc47159D12Eb7aC2804d4A33173c2";
const ETHX_ADDRESS = "0x27e1e4E6BC79D93032abef01025811B7E4727e85";
const SUSHISWAP_ROUTER_ADDRESS = "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506";
const TELLOR_ORACLE_ADDRESS = "0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA";
const TELLOR_ORACLE_ADDRESS = "0xACC2d27400029904919ea54fFc0b18Bf07C57875";
const RIC_CONTRACT_ADDRESS = "0x263026e7e53dbfdce5ae55ade22493f828922965";
const TELLOR_REQUEST_ID = 1;
const TELLOR_REQUEST_ID = 60;

console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());
Expand Down
4 changes: 2 additions & 2 deletions 01-Contracts/scripts/set-oracle-address.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
async function main() {

const [keeper] = await ethers.getSigners();
const TELLOR_CONTRACT_ADDRESS = "0xC79255821DA1edf8E1a8870ED5cED9099bf2eAAA"
const TELLOR_CONTRACT_ADDRESS = "0xACC2d27400029904919ea54fFc0b18Bf07C57875"
const STREAM_EXCHANGE_HELPER_ADDRESS = "0x0C7776292AB9E95c54282fD74e47d73338c457D8"
const RICOCHET_CONTRACT_ADDRESS = "0x387af38C133056a0744FB6e823CdB459AE3c5a1f"
const RICOCHET_CONTRACT_ADDRESS = "0x2A7F77D32011fEE97e53F04d7504C6eC49c84e19"

const StreamExchangeHelper = await ethers.getContractFactory("StreamExchangeHelper")
const seh = await StreamExchangeHelper.attach(STREAM_EXCHANGE_HELPER_ADDRESS)
Expand Down
28 changes: 28 additions & 0 deletions 01-Contracts/scripts/transfer-ownership.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
async function main() {

const [keeper] = await ethers.getSigners();
const NEW_OWNER = "0x9C6B5FdC145912dfe6eE13A667aF3C5Eb07CbB89"
const STREAM_EXCHANGE_HELPER_ADDRESS = "0x0C7776292AB9E95c54282fD74e47d73338c457D8"
const RICOCHET_CONTRACT_ADDRESS = "0x44164bf14213fd0d18ee7fa354a70ee4758e917c"

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("Current Owner", await ricochet.owner())
console.log("New Owner", await ricochet.transferOwnership(NEW_OWNER))

}

main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
Loading

0 comments on commit 30ced42

Please sign in to comment.