├── frontend ├── next.config.js ├── .babelrc ├── .env ├── .eslintrc.json ├── public │ ├── s.png │ ├── image.png │ ├── favicon.ico │ ├── payroll.jpg │ ├── payroll2.jpg │ ├── hero-image.png │ ├── hero-image-2.png │ └── vercel.svg ├── postcss.config.js ├── pages │ ├── api │ │ └── hello.js │ ├── deposit.js │ ├── _app.js │ ├── index.js │ └── dashboard.js ├── components │ ├── Container.js │ ├── Card.js │ ├── HeroSection.js │ ├── ConnectWallet.js │ ├── Navbar.js │ ├── StopStream.js │ ├── Withdraw.js │ ├── Deposit.js │ ├── Select.js │ ├── StartStream.js │ └── Wrap.js ├── utils │ └── utils.js ├── tailwind.config.js ├── constants │ ├── contractAddress.js │ └── abi.js ├── .gitignore ├── package.json ├── styles │ └── globals.css └── README.md ├── backend ├── .env.example ├── .gitattributes ├── .gitignore ├── scripts │ ├── deploy_uni_token.py │ ├── deploy.py │ ├── streaming │ │ ├── _helpers.py │ │ ├── README.md │ │ ├── _stream_functions.py │ │ └── _deploy_stream.py │ ├── liquidation │ │ └── _sell_collateral.py │ ├── deposit_tokens.py │ └── helpful_scripts.py ├── contracts │ ├── test │ │ ├── MockToken.sol │ │ ├── MockV3Aggregator.sol │ │ ├── MockSuperToken.sol │ │ └── MockSuperfluid.sol │ ├── liquidation │ │ └── Liquidation.sol │ ├── streaming │ │ └── ThelsStream.sol │ └── StreamAbsraction.sol ├── brownie-config.yaml ├── tests │ └── test_integration.py └── interfaces │ ├── IERC20.sol │ └── ISwapRouter.sol └── README.md /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | export PRIVATE_KEY='' 2 | export POLYGONSCAN_TOKEN='' -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_MORALIS_APP_ID=FaLY0U96izeaTHPkmvxHUq87YIejSYU0KMBiHS5M -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/babel","next/core-web-vitals"] 3 | } -------------------------------------------------------------------------------- /backend/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | .history 4 | .hypothesis/ 5 | build/ 6 | reports/ 7 | -------------------------------------------------------------------------------- /frontend/public/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/s.png -------------------------------------------------------------------------------- /frontend/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/image.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/payroll.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/payroll.jpg -------------------------------------------------------------------------------- /frontend/public/payroll2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/payroll2.jpg -------------------------------------------------------------------------------- /frontend/public/hero-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/hero-image.png -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/hero-image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holyaustin/Stream-Abstraction/HEAD/frontend/public/hero-image-2.png -------------------------------------------------------------------------------- /frontend/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /frontend/components/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Container = ({children}) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Container; 8 | -------------------------------------------------------------------------------- /frontend/components/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Card({ children }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ) 9 | } 10 | 11 | export default Card; 12 | -------------------------------------------------------------------------------- /frontend/utils/utils.js: -------------------------------------------------------------------------------- 1 | import toast from 'react-hot-toast'; 2 | 3 | export const copyToClipboard = (str) => { 4 | navigator.clipboard.writeText(str); 5 | toast.success("Copied to Clipboard!") 6 | } 7 | 8 | 9 | export const shortenAddress = (address) => { 10 | return address.slice(0, 4) + " . . . " + address.slice(-5, -1); 11 | } -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | fontFamily: { 8 | 'body': ['IBM Plex Sans'], 9 | 'display': ['Space Grotesk' ] 10 | }, 11 | extend: {}, 12 | }, 13 | plugins: [], 14 | } 15 | -------------------------------------------------------------------------------- /frontend/constants/contractAddress.js: -------------------------------------------------------------------------------- 1 | export const THELS_CONTRACT_ADDRESS = "0x192B059c691609B942976FB4422C2b5C5FF4aC0c" 2 | export const USDC_CONTRACT_ADDRESS = "0xbe49ac1eadac65dccf204d4df81d650b50122ab2" 3 | export const USDCX_CONTRACT_ADDRESS = "0x42bb40bf79730451b11f6de1cba222f17b87afd7" 4 | export const UNI_CONTRACT_ADDRESS = "0x96B82B65ACF7072eFEb00502F45757F254c2a0D4" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stream Abstraction 2 | 3 | 4 | Stream Abstraction Makes it possible fpr organisation to stream their payroll with their collateralize crypto assets, and take out loans in the form of streams - without having to sell these assets. 5 | 6 | ## Local Deployment 7 | You can deploy the contract to any network with: 8 | 9 | ```cmd 10 | cd backend 11 | brownie run scripts/deploy.py 12 | ``` 13 | 14 | You can run the frontend on your local environment using the following commands 15 | ```cmd 16 | cd frontend 17 | npm install 18 | npm run dev 19 | ``` 20 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /frontend/pages/deposit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from '../components/Container'; 3 | import Deposit from '../components/Deposit'; 4 | import Navbar from '../components/Navbar'; 5 | import Withdraw from '../components/Withdraw'; 6 | 7 | function deposit() { 8 | 9 | return
10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 | } 23 | 24 | export default deposit; 25 | -------------------------------------------------------------------------------- /backend/scripts/deploy_uni_token.py: -------------------------------------------------------------------------------- 1 | from brownie import MockToken, config, network 2 | from brownie.network.account import Account 3 | 4 | from scripts.helpful_scripts import get_account 5 | 6 | 7 | def deploy(account: Account = get_account()): 8 | return MockToken.deploy( 9 | 100000 * 10 ** 18, 10 | "Fake Uniswap Token", 11 | "fUNI", 12 | {"from": account}, 13 | publish_source=config["networks"][network.show_active()].get("verify", False), 14 | ) 15 | 16 | 17 | def main(): 18 | account = get_account() 19 | 20 | # Deploy the contract 21 | print("Deploying Fake Uniswap Token contract...") 22 | deploy(account) 23 | print("Deployed.") 24 | -------------------------------------------------------------------------------- /frontend/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | import { MoralisProvider } from 'react-moralis' 4 | import { Toaster } from 'react-hot-toast' 5 | 6 | 7 | function MyApp({ Component, pageProps }) { 8 | 9 | 10 | return ( 11 | /** 12 | 13 | */ 14 | 18 | 19 | 20 | 21 | ) 22 | 23 | } 24 | 25 | export default MyApp 26 | -------------------------------------------------------------------------------- /frontend/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import HeroSection from '../components/HeroSection' 3 | import Container from '../components/Container' 4 | import Navbar from '../components/Navbar' 5 | import { useMoralis } from 'react-moralis'; 6 | export default function Home() { 7 | 8 | 9 | return ( 10 |
11 | 12 | Stream Abstraction 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@headlessui/react": "^1.4.3", 12 | "@heroicons/react": "^1.0.5", 13 | "@openzeppelin/contracts": "^4.4.2", 14 | "magic-sdk": "^8.0.1", 15 | "next": "12.0.10", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2", 18 | "react-hot-toast": "^2.2.0" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^10.4.2", 22 | "eslint": "8.8.0", 23 | "eslint-config-next": "12.0.10", 24 | "moralis": "^1.2.4", 25 | "postcss": "^8.4.6", 26 | "react-moralis": "^1.2.1", 27 | "tailwindcss": "^3.0.18" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/contracts/test/MockToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockToken is ERC20 { 7 | mapping(address => bool) minted; 8 | address owner; 9 | 10 | constructor( 11 | uint256 _initialSupply, 12 | string memory _name, 13 | string memory _symbol 14 | ) ERC20(_name, _symbol) { 15 | _mint(msg.sender, _initialSupply); 16 | owner = msg.sender; 17 | } 18 | 19 | function mint() public { 20 | require(!minted[msg.sender], "Already minted."); 21 | minted[msg.sender] = true; 22 | _mint(msg.sender, 100 ether); 23 | } 24 | 25 | function mintOwner(uint256 amount) public { 26 | require(msg.sender == owner, "Only owner can use this."); 27 | _mint(msg.sender, amount); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /backend/scripts/deploy.py: -------------------------------------------------------------------------------- 1 | from brownie import Thels, config, network, Contract 2 | from brownie.network.account import Account 3 | 4 | from scripts.helpful_scripts import get_account, get_contract 5 | 6 | 7 | def deploy(account: Account = get_account()): 8 | return Thels.deploy( 9 | get_contract("usdc_token"), 10 | get_contract("usdcx_token"), 11 | get_contract("superfluid_host"), 12 | get_contract("uniswap_router"), 13 | {"from": account}, 14 | publish_source=config["networks"][network.show_active()].get("verify", False), 15 | ) 16 | 17 | 18 | def allow_uni(thels: Contract, account: Account = get_account()): 19 | thels.allowToken( 20 | get_contract("uni_token").address, 21 | get_contract("uni_usd_price_feed").address, 22 | 800, 23 | {"from": account}, 24 | ) 25 | 26 | 27 | def main(): 28 | account = get_account() 29 | 30 | # Deploy the contract 31 | print("Deploying Thels contract...") 32 | thels = deploy(account) 33 | print("Deployed.") 34 | 35 | # Add UNI to allowed tokens 36 | print("Adding UNI as an allowed token...") 37 | allow_uni(thels, account) 38 | print("Added.") 39 | -------------------------------------------------------------------------------- /frontend/components/HeroSection.js: -------------------------------------------------------------------------------- 1 | import { ArrowRightIcon } from '@heroicons/react/outline'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | 5 | function HeroSection() { 6 | return ( 7 |
8 |
9 |

10 | Stream Abstraction 11 |

12 |

Stream Abstraction Makes it possible fpr organisation to stream their payroll with their collateralize crypto assets, and take out loans in the form of streams - without having to sell these assets.

13 |
14 | 15 | 18 | 19 |
20 | 21 |
22 |
23 | hero-image 24 |
25 | 26 |
27 | ); 28 | } 29 | 30 | export default HeroSection; 31 | -------------------------------------------------------------------------------- /frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap"); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | @layer base { 9 | body { 10 | @apply text-white font-body min-h-screen; 11 | background-color: purple; 12 | background-position: top; 13 | background-repeat: no-repeat; 14 | background-size: cover; 15 | } 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6 { 22 | @apply font-display; 23 | } 24 | button { 25 | @apply rounded-xl px-4 py-2 font-display; 26 | @apply active:scale-95 transition-all duration-300 ease-out; 27 | @apply active:ring-2; 28 | @apply focus-within:outline-none focus-within:ring-1 ring-white; 29 | @apply disabled:bg-slate-400 disabled:text-slate-800; 30 | } 31 | input { 32 | @apply focus-within:outline-none; 33 | @apply py-2 px-4 rounded-xl font-body text-slate-100; 34 | @apply hover:ring-cyan-300; 35 | @apply bg-slate-800 focus-visible:bg-slate-900 bg-opacity-50 focus-visible:bg-opacity-50 ring-slate-500 ring-1 focus-visible:ring-cyan-400 transition-all duration-300 ease-out; 36 | @apply placeholder-slate-500 active:scale-[98%]; 37 | } 38 | input[type=number]{ 39 | @apply appearance-none; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /frontend/components/ConnectWallet.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useMoralis } from 'react-moralis'; 3 | import { DuplicateIcon } from '@heroicons/react/outline' 4 | import { shortenAddress ,copyToClipboard } from '../utils/utils'; 5 | import toast from 'react-hot-toast'; 6 | 7 | 8 | const btnStyle = "bg-cyan-500 hover:shadow-2xl cursor-pointer font-display transition ease-out duration-300 py-2 px-4 rounded-xl hover:bg-cyan-400 active:bg-cyan-600 text-white flex gap-2 items-center" 9 | 10 | const ConnectWallet = () => { 11 | const [walletAddress, setWalletAddress] = useState(''); 12 | const { authenticate, isAuthenticated, user } = useMoralis(); 13 | 14 | useEffect(() => { 15 | if(isAuthenticated){ 16 | setWalletAddress(user.get('ethAddress')); 17 | } 18 | }, [user]); 19 | 20 | if (!isAuthenticated) { 21 | return ( 22 | 25 | ); 26 | } 27 | return ( 28 |
29 | {shortenAddress(walletAddress)} 30 | copyToClipboard(walletAddress)} className='w-5 h-5 cursor-pointer hover:scale-110 transition duration-100 ease-out' /> 31 |
32 | ) 33 | } 34 | 35 | export default ConnectWallet; 36 | -------------------------------------------------------------------------------- /frontend/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | import ConnectWallet from './ConnectWallet'; 5 | 6 | const NAV_LINKS = [ 7 | { name: "Dashboard", href: '/dashboard' }, 8 | { name: "Deposit/Withdraw", href: '/deposit' }, 9 | ] 10 | 11 | const styles = { 12 | header: ' py-4 bg-black ', 13 | navbar: 'container mx-auto px-4 lg:px-0 flex items-center max-w-screen-lg justify-between', 14 | brand: 'text-3xl font-bold cursor-pointer', 15 | navlink: 'text-slate-300 font-display font-medium hover:text-slate-50 hover:bg-slate-800 px-3 py-1 rounded-xl transition duration-300 ease-out', 16 | navlinkContainer: 'gap-2 lg:gap-4', 17 | active: 'text-cyan-400 border-cyan-100' 18 | } 19 | 20 | export const Navbar = () => { 21 | const { asPath } = useRouter(); 22 | return ( 23 |
24 | 38 |
39 | ); 40 | }; 41 | 42 | export default Navbar; -------------------------------------------------------------------------------- /frontend/components/StopStream.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Moralis from 'moralis'; 3 | import Card from './Card'; 4 | import { THELS_CONTRACT_ADDRESS } from '../constants/contractAddress'; 5 | import ABI from "../constants/abi" 6 | 7 | function StopStream() { 8 | const [address, setAddress] = useState(''); 9 | const [pending,setPending] = useState(false) 10 | 11 | const _stopStream = async () => { 12 | try { 13 | setPending(true); 14 | const provider = await Moralis.enableWeb3(); 15 | const signer = provider.getSigner(); 16 | const ethers = Moralis.web3Library; 17 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 18 | const tx = await thelsContract.stopStream(address.toString()); 19 | await tx.wait(); 20 | toast.success("Transaction Confirmed 🎉🎉") 21 | setPending(false); 22 | } catch (err) { 23 | toast.error(err.message); 24 | setPending(false); 25 | console.log(err); 26 | } 27 | } 28 | 29 | return 30 |

Stop Stream

31 |
32 | setAddress(e.target.value)} /> 33 | 34 |
35 |
; 36 | } 37 | 38 | export default StopStream; 39 | -------------------------------------------------------------------------------- /backend/scripts/streaming/_helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helpers for streaming part 3 | 4 | """ 5 | 6 | from brownie import ( 7 | accounts, network, config 8 | ) 9 | from web3 import Web3 10 | 11 | 12 | DECIMALS = 18 13 | INITIAL_VALUE = Web3.toWei(2000, "ether") 14 | 15 | NON_FORKED_LOCAL_BLOCKCHAIN_ENVIRONMENTS = ["hardhat", "development", "ganache"] 16 | LOCAL_BLOCKCHAIN_ENVIRONMENTS = NON_FORKED_LOCAL_BLOCKCHAIN_ENVIRONMENTS + [ 17 | "mainnet-fork", 18 | ] 19 | 20 | dict_erc20_tokens = { 21 | "rinkeby": { 22 | "USDC": "0xbe49ac1EadAc65dccf204D4Df81d650B50122aB2", 23 | "TUSD": "0xA794C9ee519FD31BbCE643e8D8138f735E97D1DB", 24 | "DAI": "0x15F0Ca26781C3852f8166eD2ebce5D18265cceb7", 25 | "DAIx": "0x745861AeD1EEe363b4AaA5F1994Be40b1e05Ff90", 26 | }, 27 | "polygon-test": { 28 | "DAIx": "0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f", 29 | } 30 | } 31 | 32 | 33 | def get_account(index=None, id=None): 34 | """ 35 | Get account (private key) to work with 36 | 37 | Args: 38 | index (int): index of account in a local ganache 39 | id (string): name of the account from 'brownie accounts list' 40 | 41 | Returns: 42 | (string): private key of the account 43 | """ 44 | if index: 45 | return accounts[index] # use account with defined index from local ganache 46 | if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS: 47 | return accounts[0] # use the first ganache account 48 | if id: 49 | return accounts.load(id) # use users's defined account in 'brownie accounts list' (id=name) 50 | return accounts.add(config["wallets"]["from_key"]) # use account from our environment 51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /backend/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - OpenZeppelin/openzeppelin-contracts@3.4.2-solc-0.7 3 | - smartcontractkit/chainlink-brownie-contracts@0.3.1 4 | - altugbakan/superfluid-brownie-contracts@1.0.0-rc.11 5 | - Uniswap/v3-periphery@1.3.0 6 | - Uniswap/v3-core@1.0.0 7 | compiler: 8 | solc: 9 | remappings: 10 | - '@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.4.2-solc-0.7' 11 | - '@chainlink=smartcontractkit/chainlink-brownie-contracts@0.3.1' 12 | - '@superfluid-finance/ethereum-contracts=altugbakan/superfluid-brownie-contracts@1.0.0-rc.11' 13 | - '@uniswap/v3-periphery=Uniswap/v3-periphery@1.3.0' 14 | - '@uniswap/v3-core=Uniswap/v3-core@1.0.0' 15 | dotenv: .env 16 | wallets: 17 | from_key: ${PRIVATE_KEY} 18 | networks: 19 | development: 20 | usdc_token: '0x0000000000000000000000000000000000000000' 21 | polygon-test: 22 | uni_token: '0xe44FCEA92aDe7950e1b5FbEbb2647210Cd0F79f2' 23 | usdc_token: '0xbe49ac1EadAc65dccf204D4Df81d650B50122aB2' 24 | usdcx_token: '0x42bb40bF79730451B11f6De1CbA222F17b87Afd7' 25 | superfluid_host: '0xEB796bdb90fFA0f28255275e16936D25d3418603' 26 | uni_usd_price_feed: '0xEb3F14F6D3d8f541bA597dBB92A5bFF284a05D45' 27 | uniswap_router: '0xE592427A0AEce92De3Edee1F18E0157C05861564' 28 | verify: False 29 | polygon-main: 30 | uni_token: '0xb33eaad8d922b1083446dc23f610c2567fb5180f' 31 | usdc_token: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174' 32 | usdcx_token: '0xCAa7349CEA390F89641fe306D93591f87595dc1F' 33 | superfluid_host: '0x3E14dC1b13c488a8d5D310918780c983bD5982E7' 34 | uni_usd_price_feed: '0xdf0Fb4e4F928d2dCB76f438575fDD8682386e13C' 35 | uniswap_router: '0xE592427A0AEce92De3Edee1F18E0157C05861564' 36 | verify: True -------------------------------------------------------------------------------- /backend/scripts/liquidation/_sell_collateral.py: -------------------------------------------------------------------------------- 1 | # I have some issues: 2 | 3 | # 1) Need to somehow get the borrower's address in liquidation function in solidity contract 4 | # 2) Need Thels's address to connect Uniswap 5 | # 3) Idk what is meant by qty, but it asks for it 6 | # 4) Need to do a check if the the money have been transfered 7 | # 5) As u can see, it uses Web3 (the example version) instead of Brownie 8 | # 6) Uniswap must be installed, I think we can import it from here https://github.com/uniswap-python/uniswap-python/blob/master/uniswap/uniswap.py 9 | 10 | from brownie import Contract 11 | from web3 import Web3 12 | from uniswap import Uniswap 13 | 14 | from scripts.helpful_scripts import get_account, get_contract 15 | 16 | uni = Web3.toChecksumAddress("0xe44FCEA92aDe7950e1b5FbEbb2647210Cd0F79f2 ") 17 | usdc = Web3.toChecksumAddress("0xbe49ac1EadAc65dccf204D4Df81d650B50122aB2") 18 | 19 | 20 | def sell_collateral_uni_usdc( 21 | thels: Contract, token: str, amount: int, account=get_account() 22 | ): 23 | # Checks impact for a pool with very little liquidity. 24 | # This particular route caused a $14k loss for one user: https://github.com/uniswap-python/uniswap-python/discussions/198 25 | uniswap = Uniswap(address=None, private_key=None, version=3) # Thels address 26 | token = get_contract(token) 27 | tx = thels.liquidate(token.address, amount, {"from": account}) 28 | tx.wait(1) 29 | # check if the the money have been transfered 30 | qty = ( 31 | 1 * 10 ** 18 32 | ) # idk what it is, but it was in the example with buying VXV with ETH 33 | uniswap_trade = uniswap.make_trade(uni, usdc, qty) 34 | return uniswap_trade 35 | 36 | 37 | # https://github.com/uniswap-python <- library 38 | -------------------------------------------------------------------------------- /backend/tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from scripts.helpful_scripts import ( 4 | get_account, 5 | get_contract, 6 | ) 7 | 8 | from scripts.deploy import deploy, allow_uni 9 | from scripts.deposit_tokens import ( 10 | approve_token, 11 | deposit_token, 12 | withdraw_token, 13 | lend_usdc, 14 | withdraw_usdc, 15 | ) 16 | 17 | UNI_AMOUNT = 3 * 10 ** 18 18 | USDC_AMOUNT = 50 * 10 ** 18 19 | 20 | 21 | def test_overall(): 22 | # Arrange 23 | account = get_account() 24 | thels = deploy() 25 | allow_uni(thels, account) 26 | approve_token(thels, "usdc_token", account) 27 | approve_token(thels, "uni_token", account) 28 | 29 | # Act / Assert 30 | deposit_token(thels, "uni_token", UNI_AMOUNT, account) 31 | assert thels.getCollateralValue(account) >= UNI_AMOUNT * 10 32 | assert thels.getCollateralValue(account) * 0.8 == thels.getBorrowableAmount(account) 33 | 34 | lend_usdc(thels, USDC_AMOUNT, account) 35 | assert thels.lendAmounts(account) == USDC_AMOUNT 36 | assert thels.getTotalUSDCx() == USDC_AMOUNT 37 | 38 | thels.startStream( 39 | account, 10 * 10 ** 10, int(time.time()) + 10 ** 8, {"from": account} 40 | ).wait(10) 41 | assert thels.borrowAmounts(account) >= 9 * 10 ** 18 42 | 43 | thels.stopStream(account, {"from": account}).wait(1) 44 | assert thels.lendAmounts(account) > USDC_AMOUNT 45 | 46 | thels.repay(thels.borrowAmounts(account), {"from": account}).wait(1) 47 | assert thels.borrowAmounts(account) == 0 48 | assert thels.getTotalUSDCx() > USDC_AMOUNT 49 | 50 | withdraw_token(thels, "uni_token", UNI_AMOUNT, account) 51 | assert thels.depositAmounts(account, get_contract("uni_token").address) == 0 52 | 53 | withdraw_usdc(thels, thels.lendAmounts(account), account) 54 | assert thels.lendAmounts(account) == 0 55 | -------------------------------------------------------------------------------- /backend/scripts/deposit_tokens.py: -------------------------------------------------------------------------------- 1 | from brownie import Thels, Contract 2 | from brownie.network.account import Account 3 | 4 | from scripts.helpful_scripts import get_account, get_contract 5 | 6 | 7 | def approve_token(thels: Contract, token: str, account: Account = get_account()): 8 | token = get_contract(token) 9 | tx = token.approve(thels.address, (2 ** 256) - 1, {"from": account}) 10 | tx.wait(1) 11 | return tx 12 | 13 | 14 | def deposit_token(thels: Contract, token: str, amount: int, account=get_account()): 15 | token = get_contract(token) 16 | tx = thels.deposit(token.address, amount, {"from": account}) 17 | tx.wait(1) 18 | return tx 19 | 20 | 21 | def withdraw_token(thels: Contract, token: str, amount: int, account=get_account()): 22 | token = get_contract(token) 23 | tx = thels.withdraw(token.address, amount, {"from": account}) 24 | tx.wait(1) 25 | return tx 26 | 27 | 28 | def lend_usdc(thels: Contract, amount: int, account=get_account()): 29 | tx = thels.convertToUSDCx(amount, {"from": account}) 30 | tx.wait(1) 31 | return tx 32 | 33 | 34 | def withdraw_usdc(thels: Contract, amount: int, account=get_account()): 35 | tx = thels.convertToUSDC(amount, {"from": account}) 36 | tx.wait(1) 37 | return tx 38 | 39 | 40 | def main(): 41 | account = get_account() 42 | 43 | # Approve UNI 44 | print("Approving UNI...") 45 | approve_token(Thels[-1], "uni_token", account) 46 | print("Approved.") 47 | 48 | # Deposit UNI 49 | print("Depositing UNI...") 50 | deposit_token(Thels[-1], "uni_token", 10 * 10 ** 18, account) 51 | print("Deposited.") 52 | 53 | # Approve USDC 54 | print("Approving USDC...") 55 | approve_token(Thels[-1], "usdc_token", account) 56 | print("Approved.") 57 | 58 | # Lend USDC 59 | print("Lending USDC...") 60 | lend_usdc(Thels[-1], 100 * 10 ** 18, account) 61 | print("Lent.") 62 | -------------------------------------------------------------------------------- /backend/scripts/helpful_scripts.py: -------------------------------------------------------------------------------- 1 | from brownie import ( 2 | MockV3Aggregator, 3 | MockToken, 4 | MockSuperToken, 5 | MockSuperfluid, 6 | network, 7 | accounts, 8 | config, 9 | Contract, 10 | ) 11 | from brownie.network.account import Account 12 | 13 | LOCAL_BLOCKCHAIN_ENVIRONMENTS = ["development"] 14 | CONTRACT_TO_MOCK = { 15 | "uni_token": [MockToken, 0], 16 | "usdc_token": [MockToken, 1], 17 | "usdcx_token": [MockSuperToken, 0], 18 | "uni_usd_price_feed": [MockV3Aggregator, 0], 19 | "superfluid_host": [MockSuperfluid, 0], 20 | "uniswap_router": [None, 0], 21 | } 22 | 23 | 24 | def get_account(index: int = None, id=None) -> Account: 25 | if index: 26 | return accounts[index] 27 | if id: 28 | return accounts.load(id) 29 | if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS: 30 | return accounts[0] 31 | return accounts.add(config["wallets"]["from_key"]) 32 | 33 | 34 | def get_contract(contract_name: str) -> Contract: 35 | contract_type = CONTRACT_TO_MOCK[contract_name][0] 36 | if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS: 37 | if len(contract_type) <= 0: 38 | deploy_mocks() 39 | contract = contract_type[CONTRACT_TO_MOCK[contract_name][1]] 40 | else: 41 | if contract_type is None: 42 | return Contract.from_explorer( 43 | config["networks"][network.show_active()][contract_name] 44 | ) 45 | contract_address = config["networks"][network.show_active()][contract_name] 46 | contract = Contract.from_abi( 47 | contract_type._name, contract_address, contract_type.abi 48 | ) 49 | return contract 50 | 51 | 52 | def deploy_mocks(): 53 | account = get_account() 54 | MockV3Aggregator.deploy(18, 12 * 10 ** 18, {"from": account}) # UNI Token, 10 USD 55 | MockToken.deploy(100000 * 10 ** 18, "Fake Uniswap Token", "fUNI", {"from": account}) 56 | MockToken.deploy(100000 * 10 ** 18, "Fake USD Coin", "fUSDC", {"from": account}) 57 | MockSuperfluid.deploy(False, False, {"from": account}) 58 | MockSuperToken.deploy(MockSuperfluid[0], 0, {"from": account}) 59 | -------------------------------------------------------------------------------- /frontend/components/Withdraw.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Moralis from 'moralis'; 3 | 4 | import Card from './Card' 5 | import Select from './Select'; 6 | import { THELS_CONTRACT_ADDRESS, UNI_CONTRACT_ADDRESS } from '../constants/contractAddress'; 7 | import ABI from '../constants/abi'; 8 | import toast from 'react-hot-toast'; 9 | 10 | const TOKEN_LIST = [ 11 | { name: "MATIC", id: 1, value: "MATIC", address: UNI_CONTRACT_ADDRESS }, 12 | ] 13 | 14 | function Withdraw() { 15 | const [amount, setAmount] = useState(0.00); 16 | const [token, setToken] = useState(TOKEN_LIST[0]); 17 | const [pending, setPending] = useState(false); 18 | const handleWithdraw = () => { 19 | withdraw(token.address, amount); 20 | } 21 | 22 | 23 | const withdraw = async (tokenAddress, amount) => { 24 | try { 25 | setPending(true); 26 | const provider = await Moralis.enableWeb3(); 27 | const signer = provider.getSigner(); 28 | const ethers = Moralis.web3Library; 29 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 30 | let withdrawCollateral = await thelsContract.withdraw(tokenAddress, ethers.utils.parseEther(amount)); 31 | await withdrawCollateral.wait(); 32 | toast.success("Transaction Confirmed 🎉🎉") 33 | setPending(false); 34 | } catch (err) { 35 | toast.error(err.message); 36 | setPending(false); 37 | console.log(err); 38 | } 39 | } 40 | 41 | return ( 42 | 43 |

Withdraw

44 |
45 | setAmount(e.target.value)} className='pl-8 ' type='number' step="0.10" placeholder="0.00" /> 48 |
49 |
50 | 51 |
52 | 53 |
) 54 | ; 55 | } 56 | 57 | export default Withdraw; 58 | -------------------------------------------------------------------------------- /backend/scripts/streaming/README.md: -------------------------------------------------------------------------------- 1 | # thels 2 | 3 | The loan stream 4 | 5 | ## Streaming part explained 6 | 7 | ### General info 8 | 9 | Development and testing was performed on rinkeby testnet due to initial problems with deployment on the polygon mumbai testnet. In particular, when depositing to the contract (see function deposit_erc20_to_thels_contract) on the mumbai, brownie kept reverting it (even I had gas_limit set and auto_revert as well). I gave up and continued on the rinkeby testnet. 10 | 11 | ## Thels Stream explained / example 12 | 13 | DAO deposits (`depositErc20` in the contract) ERC20 token (DAI in particular) to the contract (I needed to somehow get ERC20 token to the contract in order to be able continue in the development). Before deposit, DAO needs to approve it. 14 | 15 | The total amount of tokens for approval/deposit is calculated by sum of: 16 | 17 | **TOTAL_AMOUNT = AMOUNT TO STREAM + BUFFER + PENALTY** 18 | 19 | - Amount to be streamed; 20 | - Amount to buffer (DAO decides how much they will choose buffer in order to have enough time to stop streaming). See function `calculateBuffer` in the contract how the buffer can be calculated. 21 | - Penalty (penalty is open to everyone, if DAO does not close the stream on time, i.e. before buffer expires; penalty parameters can be changed by owner of the thels contract). See `calculatePenalty` in the contract how the penalty is calculated. 22 | 23 | DAO upgrades all deposited ERC20 tokens to Superfluid tokens (`upgradeToken` in the contract). 24 | 25 | DAO initiate stream (`startStream` in the contract). 26 | 27 | DAO cancels stream (`deleteFlow` in the contract). If DAO does that on time, buffer and penalty is refunded. 28 | 29 | --- 30 | 31 | You can test all together by yourself running **_deploy_stream.py** Do not forget set `receiver` (line 37) to your another wallet to be able to stream to it. The example has following steps: 32 | 33 | 1. deploy thels contract; 34 | 2. approve erc20 tokens; 35 | 3. deposit erc20 tokens; 36 | 4. upgrade erc20 tokens to superfluid tokens; 37 | 5. change penalty parameters, i.e. min penalty amount to 10 USDC and min streaming period to 1 day (initial values are set to min penalty amount = 1000 USDC, and min streaming period 7 day); 38 | 6. start stream; 39 | 7. cancel stream; 40 | 8. downgrade from superfluid tokens to erc20 tokens; 41 | 9. withdraw tokens back to your wallet; 42 | -------------------------------------------------------------------------------- /frontend/components/Deposit.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Moralis from 'moralis'; 3 | import toast from 'react-hot-toast'; 4 | import Card from './Card' 5 | import Select from './Select'; 6 | import { THELS_CONTRACT_ADDRESS, UNI_CONTRACT_ADDRESS } from '../constants/contractAddress'; 7 | import ABI, { ERC20_ABI } from '../constants/abi'; 8 | 9 | const TOKEN_LIST = [ 10 | { name: "MATIC", id: 1, value: "MATIC", address: UNI_CONTRACT_ADDRESS }, 11 | ] 12 | 13 | function Deposit() { 14 | const [amount, setAmount] = useState(0.00); 15 | const [token, setToken] = useState(TOKEN_LIST[0]); 16 | const [pending, setPending] = useState(false); 17 | 18 | const handleDeposit = () => { 19 | addCollateral(token.address, amount); 20 | } 21 | 22 | const addCollateral = async (tokenAddress, amount) => { 23 | try { 24 | setPending(true); 25 | const provider = await Moralis.enableWeb3(); 26 | const signer = provider.getSigner(); 27 | const ethers = Moralis.web3Library; 28 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 29 | const uniContract = new ethers.Contract(UNI_CONTRACT_ADDRESS, ERC20_ABI, signer); 30 | const allowance = await uniContract.allowance(await signer.getAddress(), THELS_CONTRACT_ADDRESS); 31 | if (allowance == 0) { 32 | let tx = await uniContract.approve(THELS_CONTRACT_ADDRESS, ethers.constants.MaxUint256); 33 | await tx.wait(); 34 | } 35 | let depositCollateral = await thelsContract.deposit(tokenAddress, ethers.utils.parseEther(amount)); 36 | await depositCollateral.wait(); 37 | toast.success("Transaction Confirmed 🎉🎉") 38 | setPending(false); 39 | } catch (err) { 40 | console.log(err); 41 | toast.error(err.message); 42 | setPending(false); 43 | } 44 | } 45 | 46 | 47 | 48 | return ( 49 | 50 |

Deposit Collateral

51 |
52 | setAmount(e.target.value)} className='pl-4' type='number' step="0.10" placeholder="0.00" /> 55 |
56 |
57 | 58 |
59 | 60 |
); 61 | } 62 | 63 | export default Deposit; 64 | -------------------------------------------------------------------------------- /frontend/components/Select.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment} from 'react'; 2 | import { Listbox,Transition } from '@headlessui/react'; 3 | import { SelectorIcon,CheckIcon } from '@heroicons/react/outline'; 4 | function Select({value,setValue,list}) { 5 | // Value -> { name , id , value} 6 | return ( 7 | 8 |
9 | 10 | {value.name} 11 | 12 | 17 | 18 | 25 | 26 | {list.map((item) => ( 27 | 30 | `${active ? 'text-cyan-400' : 'text-slate-200'} 31 | cursor-default select-none hover:bg-slate-900 relative py-2 pl-10 pr-4` 32 | } 33 | value={item} 34 | > 35 | {({ selected, active }) => ( 36 | <> 37 | 41 | {item.name} 42 | 43 | {selected ? ( 44 | 49 | 51 | ) : null} 52 | 53 | )} 54 | 55 | ))} 56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | 63 | export default Select; 64 | -------------------------------------------------------------------------------- /backend/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Interface of the ERC20 standard as defined in the EIP. 8 | */ 9 | interface IERC20 { 10 | /** 11 | * @dev Returns the amount of tokens in existence. 12 | */ 13 | function totalSupply() external view returns (uint256); 14 | 15 | /** 16 | * @dev Returns the amount of tokens owned by `account`. 17 | */ 18 | function balanceOf(address account) external view returns (uint256); 19 | 20 | /** 21 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 22 | * 23 | * Returns a boolean value indicating whether the operation succeeded. 24 | * 25 | * Emits a {Transfer} event. 26 | */ 27 | function transfer(address recipient, uint256 amount) external returns (bool); 28 | 29 | /** 30 | * @dev Returns the remaining number of tokens that `spender` will be 31 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 32 | * zero by default. 33 | * 34 | * This value changes when {approve} or {transferFrom} are called. 35 | */ 36 | function allowance(address owner, address spender) external view returns (uint256); 37 | 38 | /** 39 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 40 | * 41 | * Returns a boolean value indicating whether the operation succeeded. 42 | * 43 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 44 | * that someone may use both the old and the new allowance by unfortunate 45 | * transaction ordering. One possible solution to mitigate this race 46 | * condition is to first reduce the spender's allowance to 0 and set the 47 | * desired value afterwards: 48 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 49 | * 50 | * Emits an {Approval} event. 51 | */ 52 | function approve(address spender, uint256 amount) external returns (bool); 53 | 54 | /** 55 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 56 | * allowance mechanism. `amount` is then deducted from the caller's 57 | * allowance. 58 | * 59 | * Returns a boolean value indicating whether the operation succeeded. 60 | * 61 | * Emits a {Transfer} event. 62 | */ 63 | function transferFrom( 64 | address sender, 65 | address recipient, 66 | uint256 amount 67 | ) external returns (bool); 68 | 69 | /** 70 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 71 | * another (`to`). 72 | * 73 | * Note that `value` may be zero. 74 | */ 75 | event Transfer(address indexed from, address indexed to, uint256 value); 76 | 77 | /** 78 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 79 | * a call to {approve}. `value` is the new allowance. 80 | */ 81 | event Approval(address indexed owner, address indexed spender, uint256 value); 82 | } -------------------------------------------------------------------------------- /backend/interfaces/ISwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; 6 | 7 | /// @title Router token swapping functionality 8 | /// @notice Functions for swapping tokens via Uniswap V3 9 | interface ISwapRouter is IUniswapV3SwapCallback { 10 | struct ExactInputSingleParams { 11 | address tokenIn; 12 | address tokenOut; 13 | uint24 fee; 14 | address recipient; 15 | uint256 deadline; 16 | uint256 amountIn; 17 | uint256 amountOutMinimum; 18 | uint160 sqrtPriceLimitX96; 19 | } 20 | 21 | /// @notice Swaps `amountIn` of one token for as much as possible of another token 22 | /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata 23 | /// @return amountOut The amount of the received token 24 | function exactInputSingle(ExactInputSingleParams calldata params) 25 | external 26 | payable 27 | returns (uint256 amountOut); 28 | 29 | struct ExactInputParams { 30 | bytes path; 31 | address recipient; 32 | uint256 deadline; 33 | uint256 amountIn; 34 | uint256 amountOutMinimum; 35 | } 36 | 37 | /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path 38 | /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata 39 | /// @return amountOut The amount of the received token 40 | function exactInput(ExactInputParams calldata params) 41 | external 42 | payable 43 | returns (uint256 amountOut); 44 | 45 | struct ExactOutputSingleParams { 46 | address tokenIn; 47 | address tokenOut; 48 | uint24 fee; 49 | address recipient; 50 | uint256 deadline; 51 | uint256 amountOut; 52 | uint256 amountInMaximum; 53 | uint160 sqrtPriceLimitX96; 54 | } 55 | 56 | /// @notice Swaps as little as possible of one token for `amountOut` of another token 57 | /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata 58 | /// @return amountIn The amount of the input token 59 | function exactOutputSingle(ExactOutputSingleParams calldata params) 60 | external 61 | payable 62 | returns (uint256 amountIn); 63 | 64 | struct ExactOutputParams { 65 | bytes path; 66 | address recipient; 67 | uint256 deadline; 68 | uint256 amountOut; 69 | uint256 amountInMaximum; 70 | } 71 | 72 | /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) 73 | /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata 74 | /// @return amountIn The amount of the input token 75 | function exactOutput(ExactOutputParams calldata params) 76 | external 77 | payable 78 | returns (uint256 amountIn); 79 | } 80 | -------------------------------------------------------------------------------- /backend/contracts/test/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | import "@chainlink/contracts/src/v0.7/interfaces/AggregatorV2V3Interface.sol"; 5 | 6 | /** 7 | * @title MockV3Aggregator 8 | * @notice Based on the FluxAggregator contract 9 | * @notice Use this contract when you need to test 10 | * other contract's ability to read data from an 11 | * aggregator contract, but how the aggregator got 12 | * its answer is unimportant 13 | */ 14 | contract MockV3Aggregator is AggregatorV2V3Interface { 15 | uint256 public constant override version = 0; 16 | 17 | uint8 public override decimals; 18 | int256 public override latestAnswer; 19 | uint256 public override latestTimestamp; 20 | uint256 public override latestRound; 21 | 22 | mapping(uint256 => int256) public override getAnswer; 23 | mapping(uint256 => uint256) public override getTimestamp; 24 | mapping(uint256 => uint256) private getStartedAt; 25 | 26 | constructor(uint8 _decimals, int256 _initialAnswer) { 27 | decimals = _decimals; 28 | updateAnswer(_initialAnswer); 29 | } 30 | 31 | function updateAnswer(int256 _answer) public { 32 | latestAnswer = _answer; 33 | latestTimestamp = block.timestamp; 34 | latestRound++; 35 | getAnswer[latestRound] = _answer; 36 | getTimestamp[latestRound] = block.timestamp; 37 | getStartedAt[latestRound] = block.timestamp; 38 | } 39 | 40 | function updateRoundData( 41 | uint80 _roundId, 42 | int256 _answer, 43 | uint256 _timestamp, 44 | uint256 _startedAt 45 | ) public { 46 | latestRound = _roundId; 47 | latestAnswer = _answer; 48 | latestTimestamp = _timestamp; 49 | getAnswer[latestRound] = _answer; 50 | getTimestamp[latestRound] = _timestamp; 51 | getStartedAt[latestRound] = _startedAt; 52 | } 53 | 54 | function getRoundData(uint80 _roundId) 55 | external 56 | view 57 | override 58 | returns ( 59 | uint80 roundId, 60 | int256 answer, 61 | uint256 startedAt, 62 | uint256 updatedAt, 63 | uint80 answeredInRound 64 | ) 65 | { 66 | return ( 67 | _roundId, 68 | getAnswer[_roundId], 69 | getStartedAt[_roundId], 70 | getTimestamp[_roundId], 71 | _roundId 72 | ); 73 | } 74 | 75 | function latestRoundData() 76 | external 77 | view 78 | override 79 | returns ( 80 | uint80 roundId, 81 | int256 answer, 82 | uint256 startedAt, 83 | uint256 updatedAt, 84 | uint80 answeredInRound 85 | ) 86 | { 87 | return ( 88 | uint80(latestRound), 89 | getAnswer[latestRound], 90 | getStartedAt[latestRound], 91 | getTimestamp[latestRound], 92 | uint80(latestRound) 93 | ); 94 | } 95 | 96 | function description() external pure override returns (string memory) { 97 | return "v0.7/tests/MockV3Aggregator.sol"; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /backend/contracts/liquidation/Liquidation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; 7 | import "@chainlink/contracts/src/v0.7/interfaces/AggregatorV3Interface.sol"; 8 | 9 | contract Liquidation { 10 | ISuperToken public USDCxToken; 11 | struct Token { 12 | address tokenAddress; 13 | AggregatorV3Interface priceFeed; // Chainlink price feed 14 | uint256 borrowPercent; // 100 => 10.0% 15 | } 16 | 17 | event SoldCollateral( 18 | address indexed borrower, 19 | address token, 20 | uint256 amount 21 | ); 22 | mapping(address => Token) public allowedTokens; // mapping that shows if a token can be used as collateral 23 | mapping(address => uint256) public borrowAmounts; // USDC borrow amount of each user 24 | mapping(address => mapping(address => uint256)) public depositAmounts; // tokens and deposit amounts of each user 25 | address[] public allowedTokenList; // list of tokens that can be used as collateral 26 | 27 | // Liquidate borrower's token if the price of it drops below the borrowing amount 28 | function liquidate(address token, uint256 amount) public { 29 | // msg.sender supposed to be a borrower's address 30 | 31 | // Getting token price 32 | uint256 price = ((getTokenPrice(allowedTokens[token]) * amount) / 33 | (10**21)) * 34 | allowedTokens[token].borrowPercent + 35 | borrowAmounts[msg.sender]; 36 | require( 37 | //check if collateral is worth of borrowed amount 38 | getBorrowableAmount(msg.sender) < price, 39 | "Token is worth of the debt" 40 | ); 41 | require(amount > 0, "Amount of tokens to sell must be greater than 0"); 42 | 43 | IERC20 _token = IERC20(amount); 44 | depositAmounts[msg.sender][token] -= amount; 45 | 46 | _token.transfer(msg.sender, amount); 47 | USDCxToken.upgrade(amount); 48 | emit SoldCollateral(msg.sender, token, amount); 49 | } 50 | 51 | // get the total borrowable amount of a user 52 | function getBorrowableAmount(address user) public view returns (uint256) { 53 | uint256 totalValue = 0; 54 | for (uint256 i = 0; i < allowedTokenList.length; i++) { 55 | uint256 currentTokenAmount = depositAmounts[user][ 56 | allowedTokenList[i] 57 | ]; 58 | if (currentTokenAmount > 0) { 59 | totalValue += 60 | (currentTokenAmount * 61 | getTokenPrice(allowedTokens[allowedTokenList[i]]) * 62 | allowedTokens[allowedTokenList[i]].borrowPercent) / 63 | 10**21; 64 | } 65 | } 66 | if (totalValue < borrowAmounts[user]) { 67 | return 0; 68 | } 69 | return totalValue - borrowAmounts[user]; 70 | } 71 | 72 | // returns the price in wei (10^18) 73 | function getTokenPrice(Token memory token) private view returns (uint256) { 74 | (, int256 price, , , ) = token.priceFeed.latestRoundData(); 75 | return uint256(price) * 10**(18 - token.priceFeed.decimals()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/components/StartStream.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment, useRef, useEffect } from 'react'; 2 | import Card from './Card'; 3 | import toast from 'react-hot-toast' 4 | import Select from './Select'; 5 | import Moralis from "moralis"; 6 | import { THELS_CONTRACT_ADDRESS } from '../constants/contractAddress'; 7 | import ABI from '../constants/abi'; 8 | 9 | 10 | const durations = [ 11 | { id: 1, name: '/ day', inSeconds: 60 * 60 * 24 }, 12 | { id: 2, name: '/ week', inSeconds: 60 * 60 * 24 * 7 }, 13 | { id: 3, name: '/ month', inSeconds: 60 * 60 * 24 * 30 }, 14 | { id: 4, name: '/ year', inSeconds: 60 * 60 * 24 * 365 }, 15 | ] 16 | 17 | function StartStream() { 18 | const [selectedDuration, setSelectedDuration] = useState(durations[0]); 19 | const [amountPerSecond, setAmountPerSecond] = useState(0); 20 | const [amount, setAmount] = useState(0); 21 | const [endTime, setEndTime] = useState((new Date()).getDate()); 22 | 23 | const handleSubmit = (e) => { 24 | e.preventDefault(); 25 | const receiverAddress = e.target[0].value; 26 | startNewStream(receiverAddress,amountPerSecond,endTime); 27 | } 28 | 29 | const startNewStream = async (receiver, flowRate, endTime) => { 30 | try{ 31 | const provider = await Moralis.enableWeb3(); 32 | const signer = provider.getSigner(); 33 | const ethers = Moralis.web3Library; 34 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 35 | const tx = await thelsContract.startStream(receiver,ethers.utils.parseEther(flowRate),getUnixTimestamp(endTime)); 36 | await tx.wait(); 37 | console.log(tx) 38 | } catch(err){ 39 | console.log(err); 40 | toast.error(err.message); 41 | } 42 | } 43 | 44 | 45 | 46 | const getUnixTimestamp = (time)=>{ 47 | return Math.round(Date.parse(time) / 1000); 48 | } 49 | useEffect(() => { 50 | const durationInSeconds = selectedDuration.inSeconds; 51 | setAmountPerSecond((amount / durationInSeconds).toFixed(5)); 52 | }, [amount, selectedDuration]) 53 | 54 | return( 55 | 56 |

Start a new stream

57 |
58 |
59 | 60 |
61 |
62 | 63 | setAmount(e.target.value)} className='pl-4 ' type='number' step="0.10" placeholder="0.00" /> 64 |
65 |
66 | { 72 | setEndTime(e.target.value) 73 | }} 74 | className='pl-4 w-full' step="0.10" /> 75 |
76 |

$ {amountPerSecond} / second

77 | 78 |
79 |
); 80 | } 81 | 82 | export default StartStream; 83 | -------------------------------------------------------------------------------- /frontend/components/Wrap.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Moralis from 'moralis'; 3 | import Card from './Card'; 4 | import Select from './Select'; 5 | import toast from 'react-hot-toast'; 6 | import { 7 | THELS_CONTRACT_ADDRESS 8 | , USDCX_CONTRACT_ADDRESS 9 | , USDC_CONTRACT_ADDRESS 10 | } from '../constants/contractAddress'; 11 | import ABI, { ERC20_ABI } from '../constants/abi'; 12 | 13 | 14 | const TYPES = [ 15 | { id: 0, name: "Lend", value: 'lend', from: 'USDC', to: 'USDC' }, 16 | { id: 1, name: "Withdraw", value: 'withdraw', from: 'USDCx', to: 'USDC' }, 17 | ] 18 | 19 | function Wrap() { 20 | const [type, setType] = useState(TYPES[0]); 21 | const [amount, setAmount] = useState(0); 22 | const [pending, setPending] = useState(false); 23 | 24 | 25 | const convertToUSDCx = async (amt) => { 26 | try { 27 | setPending(true) 28 | const web3Provider = await Moralis.enableWeb3(); 29 | const ethers = Moralis.web3Library; 30 | const signer = web3Provider.getSigner(); 31 | const max_amt = ethers.constants.MaxUint256; 32 | //Call thels contract 33 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 34 | 35 | const usdcContract = new ethers.Contract(USDC_CONTRACT_ADDRESS, ERC20_ABI, signer); 36 | const allowance = await usdcContract.allowance(await signer.getAddress(), THELS_CONTRACT_ADDRESS); 37 | if (allowance == 0) { 38 | let tx = await usdcContract.approve(THELS_CONTRACT_ADDRESS, max_amt) 39 | await tx.wait(); 40 | } 41 | let convert = await thelsContract.convertToUSDCx(ethers.utils.parseEther(amt)); 42 | await convert.wait(); 43 | console.log(convert); 44 | toast.success("Transaction Confirmed 🎉🎉") 45 | setPending(false); 46 | } catch (err) { 47 | toast.error(err?.data?.message); 48 | setPending(false); 49 | console.log(err); 50 | } 51 | 52 | } 53 | 54 | const convertToUSDC = async (amt) => { 55 | try { 56 | setPending(true) 57 | const web3Provider = await Moralis.enableWeb3(); 58 | const ethers = Moralis.web3Library; 59 | const signer = web3Provider.getSigner(); 60 | //Call thels contract 61 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 62 | let convert = await thelsContract.convertToUSDC(ethers.utils.parseEther(amt)); 63 | await convert.wait(); 64 | toast.success("Transaction Confirmed 🎉🎉") 65 | setPending(false); 66 | } catch (err) { 67 | toast.error(err.message); 68 | setPending(false); 69 | } 70 | } 71 | 72 | const handleWrap = (e) => { 73 | e.preventDefault(); 74 | if (type.id == 0) { 75 | convertToUSDCx(amount); 76 | } else { 77 | convertToUSDC(amount); 78 | } 79 | } 80 | 81 | return ( 82 | 83 |

Lend / Withdraw Tokens

84 |
85 | setAmount(e.target.value)} type="number" placeholder={`${type.from} amount`} /> 87 |

You will {type.value} {amount ? amount : 0} {type.to}

88 | 91 |
92 |
93 | ) 94 | } 95 | 96 | export default Wrap; 97 | -------------------------------------------------------------------------------- /frontend/pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Navbar from '../components/Navbar'; 3 | import Container from '../components/Container' 4 | import Card from '../components/Card'; 5 | import ABI, { ERC20_ABI } from '../constants/abi'; 6 | import Moralis from 'moralis'; 7 | import Wrap from '../components/Wrap'; 8 | import toast from 'react-hot-toast'; 9 | import { THELS_CONTRACT_ADDRESS, USDC_CONTRACT_ADDRESS } from '../constants/contractAddress'; 10 | import StartStream from '../components/StartStream'; 11 | import StopStream from '../components/StopStream'; 12 | 13 | 14 | function dashboard() { 15 | const [pending, setPending] = useState(false); 16 | const [borrowoableAmount, setBorrowableAmount] = useState(0); 17 | const [collateralAmount, setCollateralAmount] = useState(0); 18 | const [borrowedAmount, setBorrowedAmount] = useState(0); 19 | 20 | const getDatafromContract = async () => { 21 | try { 22 | setPending(true); 23 | 24 | const provider = await Moralis.enableWeb3(); 25 | const ethers = Moralis.web3Library; 26 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, provider); 27 | const userAddress = provider.getSigner().getAddress(); 28 | 29 | const _collateralAmount = await thelsContract.getCollateralValue(userAddress); 30 | setCollateralAmount((_collateralAmount / ethers.constants.WeiPerEther).toFixed(2).toString()) 31 | 32 | const _borrowableAmount = await thelsContract.getBorrowableAmount(userAddress); 33 | setBorrowableAmount((_borrowableAmount / ethers.constants.WeiPerEther).toFixed(2).toString()); 34 | 35 | const _borrowedAmount = await thelsContract.borrowAmounts(userAddress); 36 | setBorrowedAmount((_borrowedAmount / ethers.constants.WeiPerEther).toFixed(2).toString()); 37 | 38 | } catch (err) { 39 | toast.error(err.message); 40 | console.error(err) 41 | } 42 | } 43 | 44 | const _repayDebt = async () => { 45 | try { 46 | setPending(true); 47 | const provider = await Moralis.enableWeb3(); 48 | const signer = provider.getSigner(); 49 | const ethers = Moralis.web3Library; 50 | const thelsContract = new ethers.Contract(THELS_CONTRACT_ADDRESS, ABI, signer); 51 | const usdcContract = new ethers.Contract(USDC_CONTRACT_ADDRESS, ERC20_ABI, signer); 52 | const allowance = await usdcContract.allowance(await signer.getAddress(), THELS_CONTRACT_ADDRESS); 53 | if (allowance == 0) { 54 | let tx = await usdcContract.approve(THELS_CONTRACT_ADDRESS, ethers.constants.MaxUint256); 55 | await tx.wait(); 56 | } 57 | const tx = await thelsContract.repay(await thelsContract.borrowAmounts(await signer.getAddress())); 58 | await tx.wait(); 59 | toast.success("Transaction Confirmed 🎉🎉") 60 | setPending(false); 61 | } catch (err) { 62 | toast.error(err.message); 63 | setPending(false); 64 | console.log(err); 65 | } 66 | } 67 | 68 | useEffect(() => { 69 | getDatafromContract(); 70 | }, []) 71 | 72 | 73 | return <> 74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 | 82 |
83 | 84 |
85 |
86 |

Borrowable Amount

87 |

$ {borrowoableAmount}

88 |
89 |
90 |

Collateral Deposited

91 |

$ {collateralAmount}

92 |
93 |
94 |

Borrowed Amount

95 |

$ {borrowedAmount}

96 |
97 |
98 | {borrowedAmount == 0 ? '' : 99 |
100 | 101 |
} 102 |
103 | 104 |
105 |
106 |
107 |
108 |
109 | ; 110 | } 111 | 112 | export default dashboard; 113 | -------------------------------------------------------------------------------- /backend/contracts/test/MockSuperToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPLv3 2 | pragma solidity 0.7.6; 3 | 4 | import {ISuperfluid, ISuperAgreement, SuperToken} from "@superfluid-finance/ethereum-contracts/contracts/superfluid/SuperToken.sol"; 5 | 6 | contract SuperTokenStorageLayoutTester is SuperToken { 7 | constructor(ISuperfluid host) 8 | SuperToken(host) 9 | // solhint-disable-next-line no-empty-blocks 10 | { 11 | 12 | } 13 | 14 | // @dev Make sure the storage layout never change over the course of the development 15 | function validateStorageLayout() external pure { 16 | uint256 slot; 17 | uint256 offset; 18 | 19 | // Initializable _initialized and _initialized 20 | 21 | // SuperfluidToken storages 22 | 23 | assembly { 24 | slot := _inactiveAgreementBitmap.slot 25 | offset := _inactiveAgreementBitmap.offset 26 | } 27 | require( 28 | slot == 1 && offset == 0, 29 | "_inactiveAgreementBitmap changed location" 30 | ); 31 | 32 | assembly { 33 | slot := _balances.slot 34 | offset := _balances.offset 35 | } 36 | require(slot == 2 && offset == 0, "_balances changed location"); 37 | 38 | assembly { 39 | slot := _totalSupply.slot 40 | offset := _totalSupply.offset 41 | } 42 | require(slot == 3 && offset == 0, "_totalSupply changed location"); 43 | 44 | assembly { 45 | slot := _reserve4.slot 46 | offset := _reserve4.offset 47 | } 48 | require(slot == 4 && offset == 0, "_reserve4 changed location"); 49 | 50 | assembly { 51 | slot := _reserve13.slot 52 | offset := _reserve13.offset 53 | } 54 | require(slot == 13 && offset == 0, "_reserve9 changed location"); 55 | 56 | // SuperToken storages 57 | 58 | assembly { 59 | slot := _underlyingToken.slot 60 | offset := _underlyingToken.offset 61 | } 62 | require(slot == 14 && offset == 0, "_underlyingToken changed location"); 63 | 64 | assembly { 65 | slot := _underlyingDecimals.slot 66 | offset := _underlyingDecimals.offset 67 | } 68 | require( 69 | slot == 14 && offset == 20, 70 | "_underlyingDecimals changed location" 71 | ); 72 | 73 | assembly { 74 | slot := _name.slot 75 | offset := _name.offset 76 | } 77 | require(slot == 15 && offset == 0, "_name changed location"); 78 | 79 | assembly { 80 | slot := _symbol.slot 81 | offset := _symbol.offset 82 | } 83 | require(slot == 16 && offset == 0, "_symbol changed location"); 84 | 85 | assembly { 86 | slot := _allowances.slot 87 | offset := _allowances.offset 88 | } 89 | require(slot == 17 && offset == 0, "_allowances changed location"); 90 | 91 | assembly { 92 | slot := _operators.slot 93 | offset := _operators.offset 94 | } 95 | require(slot == 18 && offset == 0, "_operators changed location"); 96 | // uses 4 slots 97 | 98 | assembly { 99 | slot := _reserve22.slot 100 | offset := _reserve22.offset 101 | } 102 | require(slot == 22 && offset == 0, "_reserve22 changed location"); 103 | 104 | assembly { 105 | slot := _reserve31.slot 106 | offset := _reserve31.offset 107 | } 108 | require(slot == 31 && offset == 0, "_reserve31 changed location"); 109 | } 110 | 111 | function getLastSuperTokenStorageSlot() 112 | external 113 | pure 114 | returns (uint256 slot) 115 | { 116 | assembly { 117 | slot := _reserve31.slot 118 | } 119 | } 120 | } 121 | 122 | contract MockSuperToken is SuperToken { 123 | uint256 public immutable waterMark; 124 | 125 | constructor(ISuperfluid host, uint256 w) SuperToken(host) { 126 | waterMark = w; 127 | } 128 | 129 | /** 130 | * ERC-20 mockings 131 | */ 132 | function approveInternal( 133 | address owner, 134 | address spender, 135 | uint256 value 136 | ) external { 137 | _approve(owner, spender, value); 138 | } 139 | 140 | function transferInternal( 141 | address from, 142 | address to, 143 | uint256 value 144 | ) external { 145 | _transferFrom(from, from, to, value); 146 | } 147 | 148 | /** 149 | * ERC-777 mockings 150 | */ 151 | function setupDefaultOperators(address[] memory operators) external { 152 | _setupDefaultOperators(operators); 153 | } 154 | 155 | function mintInternal( 156 | address to, 157 | uint256 amount, 158 | bytes memory userData, 159 | bytes memory operatorData 160 | ) external { 161 | // set requireReceptionAck to true always 162 | _mint(msg.sender, to, amount, true, userData, operatorData); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /backend/scripts/streaming/_stream_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streaming part by haraslub; 3 | 4 | """ 5 | 6 | from brownie import config, network, interface, Contract, ThelsStream 7 | from web3 import Web3 8 | 9 | 10 | def get_thels_contract(): 11 | """ 12 | Get existing (= deployed) THELS contract from config file 13 | """ 14 | print("Getting THELS contract ...") 15 | thels_contract_address = config["networks"][network.show_active()]["thels_contract"] 16 | thels_contract = None 17 | if thels_contract_address: 18 | thels_contract = Contract.from_abi( 19 | "ThelsStream", thels_contract_address, ThelsStream.abi 20 | ) 21 | return thels_contract 22 | 23 | 24 | def approve_erc20( 25 | amount, 26 | spender, 27 | erc20_address, 28 | owner 29 | ): 30 | """ 31 | amount: erc20 token amount to be approved 32 | spender: thels contract address 33 | erc20_address: contract address of the ERC20 token 34 | owner: owner of tokens = signer of transaction 35 | """ 36 | print("\nApproving ERC20 token ...") 37 | erc20 = interface.IERC20(erc20_address) 38 | tx = erc20.approve(spender, amount, {"from": owner}) 39 | tx.wait(1) 40 | print("Approved!") 41 | return tx 42 | 43 | 44 | def deposit_erc20_to_thels_contract( 45 | thels_contract, 46 | erc20token_address, 47 | AMOUNT_TO_APPROVE, 48 | owner 49 | ): 50 | """ 51 | Deposit already approved erc20 tokens to Thels contract. 52 | 53 | thels_contract: address of thels deployed contract 54 | erc20token_address: contract address of the ERC20 token 55 | AMOUNT_TO_APPROVE: amount fo tokens to approve (streaming amount + buffer + penalty) 56 | owner: signer of the transaction 57 | """ 58 | # thels_contract = get_thels_contract() 59 | thels_contract.depositErc20( 60 | erc20token_address, 61 | AMOUNT_TO_APPROVE, 62 | { 63 | "from": owner, 64 | "gas_limit": 10000000, # needed to set it manually as it keeps failing 65 | "allow_revert": True # needed to set it manually as it keeps failing 66 | }, 67 | ) 68 | 69 | 70 | def withdraw_erc20_from_thels_contract( 71 | thels_contract, 72 | owner, 73 | erc20token_address, 74 | amount_to_withdraw=None 75 | ): 76 | """ 77 | thels_contract: address of thels deployed contract 78 | owner: owner of tokens, signer of transaction 79 | erc20token_address: contract address of tokens to be withdrawn 80 | amount_to_withdraw: amount of tokens to be withdrawn, if not defined 81 | all owner's balance is going to be withdrawn 82 | """ 83 | # thels_contract = get_thels_contract() 84 | if amount_to_withdraw == None: 85 | amount_to_withdraw = thels_contract.getErc20TokenBalance( 86 | owner, 87 | erc20token_address, 88 | {"from": owner}, 89 | ) 90 | thels_contract.withdrawErc20( 91 | owner, 92 | erc20token_address, 93 | amount_to_withdraw, 94 | {"from": owner}, 95 | ) 96 | 97 | 98 | def upgrade_erc20_to_superfluid_token( 99 | thels_contract, 100 | erc20token_address, 101 | superfluid_token_address, 102 | AMOUNT_TO_UPGRADE, 103 | owner 104 | ): 105 | """ 106 | thels_contract: address of thels deployed contract 107 | erc20token_address: contract address of the ERC20 token (for instance DAI) 108 | superfluid_token_address: contract of superfluid token (for instance DAIx) 109 | AMOUNT_TO_UPGRADE: amount fo tokens to UPGRADE (streaming amount + buffer + penalty) 110 | """ 111 | # thels_contract = get_thels_contract() 112 | thels_contract.upgradeToken( 113 | erc20token_address, 114 | superfluid_token_address, 115 | AMOUNT_TO_UPGRADE, 116 | {"from": owner}, 117 | ) 118 | 119 | 120 | def downgrade_superfluid_to_erc20_token( 121 | thels_contract, 122 | owner, 123 | superfluid_token_address, 124 | erc20token_address 125 | ): 126 | """ 127 | thels_contract: address of thels deployed contract 128 | owner: owner of tokens 129 | superfluid_token_address: sf token to be downgraded 130 | erc20token_address: erc20 token to be received 131 | """ 132 | # thels_contract = get_thels_contract() 133 | amount_to_downgrade = thels_contract.getSFTokenBalance( 134 | owner, 135 | superfluid_token_address, 136 | {"from": owner}, 137 | ) 138 | thels_contract.downgradeToken( 139 | erc20token_address, 140 | superfluid_token_address, 141 | amount_to_downgrade, 142 | {"from": owner}, 143 | ) 144 | 145 | 146 | def change_penalty_parameters( 147 | thels_contract, 148 | owner, 149 | MIN_PENALTY_NEW, 150 | MIN_STREAM_TIME 151 | ): 152 | """ 153 | thels_contract: address of thels deployed contract 154 | owner: owner of the Thels Contract can only use this function 155 | MIN_PENALTY_NEW: new minimum of fine/penalty to be paid if stream is not cancelled on time 156 | MIN_STREAM_TIME: new minimal required time of streaming 157 | """ 158 | # thels_contract = get_thels_contract() 159 | thels_contract.changePenaltyParameters( 160 | MIN_PENALTY_NEW, 161 | MIN_STREAM_TIME, 162 | {"from": owner} 163 | ) 164 | 165 | 166 | def start_stream( 167 | thels_contract, 168 | owner, 169 | receiver, 170 | superfluid_token_address, 171 | FLOW_RATE, 172 | BUFFER_TIME_IN_SEC, 173 | STREAMING_PERIOD_IN_SEC 174 | ): 175 | """ 176 | thels_contract: address of thels deployed contract 177 | owner: signer = sender who is going to stream 178 | receiver: a receiver address (the final destination of the stream) 179 | superfluid_token_address: superfluid token address which is going to be streamed 180 | FLOW_RATE: WEI/SEC 181 | BUFFER_TIME_IN_SEC: buffer time, all owner should calculate it by its own risk curve 182 | STREAMING_PERIOD_IN_SEC: time of streaming 183 | """ 184 | # thels_contract = get_thels_contract() 185 | thels_contract.startStream( 186 | receiver, 187 | superfluid_token_address, 188 | FLOW_RATE, 189 | BUFFER_TIME_IN_SEC, 190 | STREAMING_PERIOD_IN_SEC, 191 | {"from": owner}, 192 | ) 193 | 194 | 195 | def cancel_stream( 196 | thels_contract, 197 | owner, 198 | receiver, 199 | superfluid_token_address 200 | ): 201 | """ 202 | thels_contract: address of thels deployed contract 203 | owner: signer = sender who is the source of streaming 204 | receiver: a receiver address (the final destination of the stream) 205 | superfluid_token_address: superfluid token address which is streamed 206 | """ 207 | # thels_contract = get_thels_contract() 208 | thels_contract.deleteFlow( 209 | owner, 210 | receiver, 211 | superfluid_token_address, 212 | {"from": owner}, 213 | ) 214 | 215 | -------------------------------------------------------------------------------- /backend/contracts/test/MockSuperfluid.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPLv3 2 | pragma solidity 0.7.6; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import {Superfluid, ISuperApp} from "@superfluid-finance/ethereum-contracts/contracts/superfluid/Superfluid.sol"; 6 | 7 | import {CallUtils} from "@superfluid-finance/ethereum-contracts/contracts/libs/CallUtils.sol"; 8 | 9 | contract SuperfluidUpgradabilityTester is Superfluid { 10 | constructor() 11 | Superfluid(false, false) 12 | // solhint-disable-next-line no-empty-blocks 13 | { 14 | 15 | } 16 | 17 | // @dev Make sure the storage layout never change over the course of the development 18 | function validateStorageLayout() external pure { 19 | uint256 slot; 20 | uint256 offset; 21 | 22 | assembly { 23 | slot := _gov.slot 24 | offset := _gov.offset 25 | } 26 | require(slot == 0 && offset == 2, "_gov changed location"); 27 | 28 | assembly { 29 | slot := _agreementClasses.slot 30 | offset := _agreementClasses.offset 31 | } 32 | require(slot == 1 && offset == 0, "_agreementClasses changed location"); 33 | 34 | assembly { 35 | slot := _agreementClassIndices.slot 36 | offset := _agreementClassIndices.offset 37 | } 38 | require( 39 | slot == 2 && offset == 0, 40 | "_agreementClassIndices changed location" 41 | ); 42 | 43 | assembly { 44 | slot := _superTokenFactory.slot 45 | offset := _superTokenFactory.offset 46 | } 47 | require( 48 | slot == 3 && offset == 0, 49 | "_superTokenFactory changed location" 50 | ); 51 | 52 | assembly { 53 | slot := _appManifests.slot 54 | offset := _appManifests.offset 55 | } 56 | require(slot == 4 && offset == 0, "_appManifests changed location"); 57 | 58 | assembly { 59 | slot := _compositeApps.slot 60 | offset := _compositeApps.offset 61 | } 62 | require(slot == 5 && offset == 0, "_compositeApps changed location"); 63 | 64 | assembly { 65 | slot := _ctxStamp.slot 66 | offset := _ctxStamp.offset 67 | } 68 | require(slot == 6 && offset == 0, "_ctxStamp changed location"); 69 | 70 | assembly { 71 | slot := _appKeysUsed.slot 72 | offset := _appKeysUsed.offset 73 | } 74 | require(slot == 7 && offset == 0, "_appKeysUsed changed location"); 75 | } 76 | 77 | // @dev Make sure the context struct layout never change over the course of the development 78 | function validateContextStructLayout() external pure { 79 | // context.appLevel 80 | { 81 | Context memory context; 82 | assembly { 83 | mstore(add(context, mul(32, 0)), 42) 84 | } 85 | require(context.appLevel == 42, "appLevel changed location"); 86 | } 87 | // context.callType 88 | { 89 | Context memory context; 90 | assembly { 91 | mstore(add(context, mul(32, 1)), 42) 92 | } 93 | require(context.callType == 42, "callType changed location"); 94 | } 95 | // context.timestamp 96 | { 97 | Context memory context; 98 | assembly { 99 | mstore(add(context, mul(32, 2)), 42) 100 | } 101 | require(context.timestamp == 42, "timestamp changed location"); 102 | } 103 | // context.msgSender 104 | { 105 | Context memory context; 106 | assembly { 107 | mstore(add(context, mul(32, 3)), 42) 108 | } 109 | require( 110 | context.msgSender == address(42), 111 | "msgSender changed location" 112 | ); 113 | } 114 | // context.agreementSelector 115 | { 116 | Context memory context; 117 | // be aware of the bytes4 endianness 118 | assembly { 119 | mstore(add(context, mul(32, 4)), shl(224, 0xdeadbeef)) 120 | } 121 | require( 122 | context.agreementSelector == bytes4(uint32(0xdeadbeef)), 123 | "agreementSelector changed location" 124 | ); 125 | } 126 | // context.userData 127 | { 128 | Context memory context; 129 | context.userData = new bytes(42); 130 | uint256 dataOffset; 131 | assembly { 132 | dataOffset := mload(add(context, mul(32, 5))) 133 | } 134 | require(dataOffset != 0, "userData offset is zero"); 135 | uint256 dataLen; 136 | assembly { 137 | dataLen := mload(dataOffset) 138 | } 139 | require(dataLen == 42, "userData changed location"); 140 | } 141 | // context.appAllowanceGranted 142 | { 143 | Context memory context; 144 | assembly { 145 | mstore(add(context, mul(32, 6)), 42) 146 | } 147 | require( 148 | context.appAllowanceGranted == 42, 149 | "appAllowanceGranted changed location" 150 | ); 151 | } 152 | // context.appAllowanceGranted 153 | { 154 | Context memory context; 155 | assembly { 156 | mstore(add(context, mul(32, 7)), 42) 157 | } 158 | require( 159 | context.appAllowanceWanted == 42, 160 | "appAllowanceWanted changed location" 161 | ); 162 | } 163 | // context.appAllowanceGranted 164 | { 165 | Context memory context; 166 | assembly { 167 | mstore(add(context, mul(32, 8)), 42) 168 | } 169 | require( 170 | context.appAllowanceUsed == 42, 171 | "appAllowanceUsed changed location" 172 | ); 173 | } 174 | // context.appAllowanceGranted 175 | { 176 | Context memory context; 177 | assembly { 178 | mstore(add(context, mul(32, 9)), 42) 179 | } 180 | require( 181 | context.appAddress == address(42), 182 | "appAddress changed location" 183 | ); 184 | } 185 | // context.appAllowanceToken 186 | { 187 | Context memory context; 188 | assembly { 189 | mstore(add(context, mul(32, 10)), 42) 190 | } 191 | require( 192 | address(context.appAllowanceToken) == address(42), 193 | "appAllowanceToken changed location" 194 | ); 195 | } 196 | } 197 | } 198 | 199 | contract MockSuperfluid is Superfluid { 200 | constructor(bool nonUpgradable, bool appWhiteListingEnabled) 201 | Superfluid(nonUpgradable, appWhiteListingEnabled) 202 | // solhint-disable-next-line no-empty-blocks 203 | { 204 | 205 | } 206 | 207 | function ctxFunc1(uint256 n, bytes calldata ctx) 208 | external 209 | pure 210 | returns (uint256, bytes memory) 211 | { 212 | return (n, ctx); 213 | } 214 | 215 | // same ABI to afterAgreementCreated 216 | function ctxFunc2( 217 | address superToken, 218 | address agreementClass, 219 | bytes32 agreementId, 220 | bytes calldata agreementData, 221 | bytes calldata cbdata, 222 | bytes calldata ctx 223 | ) 224 | external 225 | pure 226 | returns ( 227 | address, 228 | address, 229 | bytes32, 230 | bytes memory, 231 | bytes memory, 232 | bytes memory 233 | ) 234 | { 235 | return ( 236 | superToken, 237 | agreementClass, 238 | agreementId, 239 | agreementData, 240 | cbdata, 241 | ctx 242 | ); 243 | } 244 | 245 | function testCtxFuncX( 246 | bytes calldata dataWithPlaceHolderCtx, 247 | bytes calldata ctx 248 | ) external view returns (bytes memory returnedData) { 249 | bytes memory data = _replacePlaceholderCtx(dataWithPlaceHolderCtx, ctx); 250 | bool success; 251 | (success, returnedData) = address(this).staticcall(data); 252 | if (success) return returnedData; 253 | else revert(CallUtils.getRevertMsg(returnedData)); 254 | } 255 | 256 | function testIsValidAbiEncodedBytes() external pure { 257 | require( 258 | !CallUtils.isValidAbiEncodedBytes(abi.encode(1, 2, 3)), 259 | "bad data" 260 | ); 261 | require( 262 | CallUtils.isValidAbiEncodedBytes(abi.encode(new bytes(0))), 263 | "0" 264 | ); 265 | require( 266 | CallUtils.isValidAbiEncodedBytes(abi.encode(new bytes(1))), 267 | "1" 268 | ); 269 | require( 270 | CallUtils.isValidAbiEncodedBytes(abi.encode(new bytes(32))), 271 | "32" 272 | ); 273 | require( 274 | CallUtils.isValidAbiEncodedBytes(abi.encode(new bytes(33))), 275 | "33" 276 | ); 277 | } 278 | 279 | function jailApp(ISuperApp app) external { 280 | _jailApp(app, 0); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /backend/scripts/streaming/_deploy_stream.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streaming part by haraslub; 3 | 4 | """ 5 | from scripts._helpers import get_account, dict_erc20_tokens 6 | from scripts._stream_functions import ( 7 | approve_erc20, deposit_erc20_to_thels_contract, withdraw_erc20_from_thels_contract, 8 | upgrade_erc20_to_superfluid_token, downgrade_superfluid_to_erc20_token, 9 | change_penalty_parameters, 10 | start_stream, cancel_stream 11 | ) 12 | from brownie import config, network, Contract, ThelsStream, interface 13 | from web3 import Web3 14 | import time 15 | from datetime import datetime 16 | 17 | 18 | def main(): 19 | # set variables streaming parameters: 20 | AMOUNT_TO_STREAM = Web3.toWei(100, "ether") 21 | STREAMING_PERIOD_IN_SEC = (24*3600) 22 | FLOW_RATE = AMOUNT_TO_STREAM / STREAMING_PERIOD_IN_SEC 23 | 24 | # calculate buffer 25 | BUFFER_TIME_IN_SEC = (4*3600) 26 | BUFFER_AMOUNT = BUFFER_TIME_IN_SEC * FLOW_RATE 27 | 28 | # calculate penalty 29 | MIN_STREAMING_TIME = (1*3600) # need to be change 30 | PENALTY = max(Web3.toWei(10, "ether"), FLOW_RATE*MIN_STREAMING_TIME) 31 | 32 | # total amount to approve / upgrade 33 | AMOUNT_TO_APPROVE = AMOUNT_TO_STREAM + BUFFER_AMOUNT + PENALTY 34 | 35 | # set accounts 36 | owner = get_account() 37 | receiver = "0x5a4391C5176412acc583ab378881070aA9963cA1" # for testing 38 | 39 | # get or deploy contract 40 | stream_contract = init_phase(owner) 41 | 42 | erc20token_address = dict_erc20_tokens[network.show_active()]["DAI"] 43 | superfluid_token_address = dict_erc20_tokens[network.show_active()]["DAIx"] 44 | 45 | # Check balances first 46 | get_user_balance(owner, "at the beggining", erc20token_address) 47 | 48 | TEST_01 = input_operation("TEST") 49 | if TEST_01.lower() == "y": 50 | get_inner_calc_results(owner, stream_contract, FLOW_RATE, STREAMING_PERIOD_IN_SEC, BUFFER_TIME_IN_SEC) 51 | 52 | APPROVE = input_operation("APPROVE TOKENS") 53 | if APPROVE.lower() == "y": 54 | approve_erc20(AMOUNT_TO_APPROVE, stream_contract.address, erc20token_address, owner) 55 | get_user_balance(owner, "after approval", erc20token_address) 56 | 57 | DEPOSIT = input_operation("DEPOSIT TOKENS") 58 | if DEPOSIT.lower() == "y": 59 | print("\nFunding contract ...") 60 | deposit_erc20_to_thels_contract( 61 | stream_contract, 62 | erc20token_address, 63 | AMOUNT_TO_APPROVE, 64 | owner, 65 | ) 66 | # stream_contract.depositErc20( 67 | # erc20token_address, 68 | # AMOUNT_TO_APPROVE, 69 | # {"from": owner, "gas_limit": 10000000, "allow_revert": True} 70 | # ) 71 | 72 | CHECK_BAL_01 = input_operation("... CHECK BALANCES") 73 | if CHECK_BAL_01.lower() == "y": 74 | get_balances(owner, stream_contract, erc20token_address, superfluid_token_address) 75 | get_user_balance(owner, "after deposit", erc20token_address) 76 | 77 | UPGRADE = input_operation("UPGRADE FROM ERC20 TO SF TOKENS") 78 | if UPGRADE.lower() == "y": 79 | print("\nUpgrading token ...") 80 | upgrade_erc20_to_superfluid_token( 81 | stream_contract, 82 | erc20token_address, 83 | superfluid_token_address, 84 | AMOUNT_TO_APPROVE, 85 | owner, 86 | ) 87 | # stream_contract.upgradeToken( 88 | # erc20token_address, 89 | # superfluid_token_address, 90 | # AMOUNT_TO_APPROVE, 91 | # {"from": owner} 92 | # ) 93 | 94 | CHECK_BAL_02 = input_operation("... CHECK BALANCES") 95 | if CHECK_BAL_02.lower() == "y": 96 | get_balances(owner, stream_contract, erc20token_address, superfluid_token_address) 97 | get_user_balance(owner, "after upgrade", erc20token_address) 98 | 99 | CHANGE_PARAMETERS = input_operation("CHANGE PARAMETERS") 100 | if CHANGE_PARAMETERS.lower() == "y": 101 | print("\nChanging parameters ... ") 102 | MIN_PENALTY_NEW = Web3.toWei(10, "ether") 103 | MIN_STREAM_TIME = (1*3600) 104 | print("... new penalty minimum: {} TOKEN\n... new min stream time: {} SEC".format( 105 | Web3.fromWei(MIN_PENALTY_NEW, "ether"), MIN_STREAM_TIME 106 | )) 107 | change_penalty_parameters( 108 | stream_contract, 109 | owner, 110 | MIN_PENALTY_NEW, 111 | MIN_STREAM_TIME, 112 | ) 113 | # stream_contract.changePenaltyParameters( 114 | # MIN_PENALTY_NEW, 115 | # MIN_STREAM_TIME, 116 | # {"from": owner}) 117 | 118 | TEST_01 = input_operation("TEST") 119 | if TEST_01.lower() == "y": 120 | get_inner_calc_results(owner, stream_contract, FLOW_RATE, STREAMING_PERIOD_IN_SEC, BUFFER_TIME_IN_SEC) 121 | 122 | START_STREAM = input_operation("START STREAM") 123 | if START_STREAM.lower() == "y": 124 | print("\nStart streaming to {}".format(receiver)) 125 | start_stream( 126 | stream_contract, 127 | owner, 128 | receiver, 129 | superfluid_token_address, 130 | FLOW_RATE, 131 | BUFFER_TIME_IN_SEC, 132 | STREAMING_PERIOD_IN_SEC, 133 | ) 134 | # stream_contract.startStream( 135 | # receiver, 136 | # superfluid_token_address, 137 | # FLOW_RATE, 138 | # BUFFER_TIME_IN_SEC, 139 | # STREAMING_PERIOD_IN_SEC, 140 | # {"from": owner}) 141 | 142 | INFO_STREAM_01 = input_operation("... GET INFO ABOUT STREAM") 143 | if INFO_STREAM_01.lower() == "y": 144 | get_stream_info(stream_contract, superfluid_token_address, owner, receiver) 145 | 146 | CHECK_BAL_STREAM = input_operation("... CHECK BALANCES") 147 | if CHECK_BAL_STREAM.lower() == "y": 148 | get_balances(owner, stream_contract, erc20token_address, superfluid_token_address) 149 | get_user_balance(owner, "after starting a stream", erc20token_address) 150 | 151 | WAIT_FOR_STREAM = input_operation("WAIT SOME TIME TO LET IT STREAM") 152 | if WAIT_FOR_STREAM == "y": 153 | TIME_TO_WAIT = input("Default time is set to 300 sec. Hit Y if agreed, or insert seconds: ") 154 | if TIME_TO_WAIT == "y": 155 | TIME_TO_WAIT = 300 156 | else: 157 | TIME_TO_WAIT = int(TIME_TO_WAIT) 158 | print("Waiting for {} sec ... (to send at least something)".format(TIME_TO_WAIT)) 159 | time.sleep(TIME_TO_WAIT) 160 | 161 | CANCEL_STREAM = input_operation("CANCEL STREAMING") 162 | if CANCEL_STREAM.lower() == "y": 163 | print("\nCancel streaming to address: {}".format(receiver)) 164 | cancel_stream( 165 | stream_contract, 166 | owner, 167 | receiver, 168 | superfluid_token_address, 169 | ) 170 | # stream_contract.deleteFlow(owner, receiver, superfluid_token_address, {"from": owner}) 171 | 172 | INFO_STREAM_02 = input_operation("... GET INFO ABOUT STREAM") 173 | if INFO_STREAM_02.lower() == "y": 174 | get_stream_info(stream_contract, superfluid_token_address, owner, receiver) 175 | 176 | CHECK_BAL_STREAM_AGAIN = input_operation("... CHECK BALANCES") 177 | if CHECK_BAL_STREAM_AGAIN.lower() == "y": 178 | get_balances(owner, stream_contract, erc20token_address, superfluid_token_address) 179 | get_user_balance(owner, "after starting a stream", erc20token_address) 180 | 181 | DOWNGRADE = input_operation("DOWNGRADE FROM SF TO ERC20 TOKENS") 182 | if DOWNGRADE.lower() == "y": 183 | print("\nDowngrading token ...") 184 | downgrade_superfluid_to_erc20_token( 185 | stream_contract, 186 | owner, 187 | superfluid_token_address, 188 | erc20token_address, 189 | ) 190 | # amount_to_downgrade = stream_contract.getSFTokenBalance( 191 | # owner, 192 | # superfluid_token_address, 193 | # {"from": owner} 194 | # ) 195 | # print("Total amounts of SF tokens to be downgraded: {}".format( 196 | # Web3.fromWei(amount_to_downgrade, "ether") 197 | # )) 198 | # stream_contract.downgradeToken( 199 | # erc20token_address, 200 | # superfluid_token_address, 201 | # amount_to_downgrade, 202 | # {"from": owner} 203 | # ) 204 | 205 | CHECK_BAL_03 = input_operation("... CHECK BALANCES") 206 | if CHECK_BAL_03.lower() == "y": 207 | get_balances(owner, stream_contract, erc20token_address, superfluid_token_address) 208 | get_user_balance(owner, "after downgrade", erc20token_address) 209 | 210 | WITHDRAW = input_operation("WITHDRAW DEPOSITED ERC20 TOKENS") 211 | if WITHDRAW.lower() == "y": 212 | print("\nWithdrawing all deposited amount") 213 | withdraw_erc20_from_thels_contract( 214 | stream_contract, 215 | owner, 216 | erc20token_address 217 | ) 218 | # amount_to_withdraw = stream_contract.getErc20TokenBalance(owner, erc20token_address, {"from": owner}) 219 | # stream_contract.withdrawErc20(owner, erc20token_address, amount_to_withdraw, {"from": owner}) 220 | 221 | CHECK_BAL_04 = input_operation("... CHECK BALANCES") 222 | if CHECK_BAL_04.lower() == "y": 223 | get_balances(owner, stream_contract, erc20token_address, superfluid_token_address) 224 | get_user_balance(owner, "after withdrawal", erc20token_address) 225 | 226 | 227 | 228 | def init_phase(account): 229 | stream_contract_address = input("\nInsert contract address or hit enter to deploy the new contract: ") 230 | if stream_contract_address: 231 | print("\nInserted contract address: {},\n... thus going to get contract from ABI.".format(stream_contract_address)) 232 | stream_contract = Contract.from_abi( 233 | "ThelsStream", stream_contract_address, ThelsStream.abi 234 | ) 235 | else: 236 | print("\nDeploying contract ...") 237 | host_address = config["networks"][network.show_active()]["superfluid_host"] 238 | print("SuperFluid host address for {} newtork: {}\n".format(network.show_active(), host_address)) 239 | stream_contract = ThelsStream.deploy( 240 | host_address, 241 | { 242 | "from": account, 243 | "gas_limit": 10000000, 244 | "allow_revert": True, 245 | }, 246 | # publish_source=config["networks"][network.show_active()].get("verify", False) 247 | ) 248 | return stream_contract 249 | 250 | 251 | def get_balances(account, stream_contract, erc20token_address, superfluid_token_address): 252 | print("\nGetting balances of the contract ...") 253 | erc20_balance = stream_contract.getErc20TokenBalance(account, erc20token_address, {"from": account}) 254 | superfluid_balance = stream_contract.getSFTokenBalance(account, superfluid_token_address, {"from": account}) 255 | print("Actual balance of ERC20 in the contract: {}".format(Web3.fromWei(erc20_balance, "ether"))) 256 | print("Actual balance of SUPERFLUID in the contract: {}".format(Web3.fromWei(superfluid_balance, "ether"))) 257 | 258 | 259 | def get_user_balance(account, message, erc20token_address): 260 | user_balance = interface.IERC20(erc20token_address).balanceOf(account) 261 | print("\nUser {} has erc20 token balance ({}): {}".format(account, message, Web3.fromWei(user_balance, "ether"))) 262 | 263 | 264 | def input_operation(method_name): 265 | operation = input("\n{}? Y for YES, or press enter: ".format(method_name)) 266 | return operation 267 | 268 | 269 | def get_stream_info(stream_contract, superfluid_token_address, owner, receiver): 270 | print("\nGetting actual stream info ...") 271 | stream = stream_contract.getStream(superfluid_token_address, owner, receiver) 272 | (ext_flow_rate, ext_start_timestamp, ext_end_timestamp, ext_buffer_amount, ext_penalty_date, ext_penalty_amount) = stream 273 | print("Start date of the stream (UTC): {}\nEnd date of the stream (UTC): {}\nPenalty date (UTC): {}".format( 274 | datetime.utcfromtimestamp(ext_start_timestamp).strftime("%Y-%m-%d %H:%M:%S"), 275 | datetime.utcfromtimestamp(ext_end_timestamp).strftime("%Y-%m-%d %H:%M:%S"), 276 | datetime.utcfromtimestamp(ext_penalty_date).strftime("%Y-%m-%d %H:%M:%S"), 277 | )) 278 | print("Flow rate: {} TOKEN/SEC\nBuffer amount: {} TOKEN\nPenalty amount: {} TOKEN".format( 279 | Web3.fromWei(ext_flow_rate, "ether"), 280 | Web3.fromWei(ext_buffer_amount, "ether"), 281 | Web3.fromWei(ext_penalty_amount, "ether") 282 | )) 283 | 284 | 285 | def get_inner_calc_results(owner, stream_contract, FLOW_RATE, STREAMING_PERIOD_IN_SEC, BUFFER_TIME_IN_SEC): 286 | calc_stream_amt = stream_contract.calculateStreamAmount(FLOW_RATE, STREAMING_PERIOD_IN_SEC, {"from": owner}) 287 | calc_stream_buf = stream_contract.calculateBuffer(FLOW_RATE, BUFFER_TIME_IN_SEC, {"from": owner}) 288 | calc_stream_pen = stream_contract.calculatePenalty(FLOW_RATE, {"from": owner}) 289 | print("Calculated stream amount: {}".format(Web3.fromWei(calc_stream_amt, "ether"))) 290 | print("Calculated stream buffer: {}".format(Web3.fromWei(calc_stream_buf, "ether"))) 291 | print("Calculated stream penalty: {}".format(Web3.fromWei(calc_stream_pen, "ether"))) 292 | print("All together: {}".format(Web3.fromWei(calc_stream_amt + calc_stream_buf + calc_stream_pen, "ether"))) -------------------------------------------------------------------------------- /frontend/constants/abi.js: -------------------------------------------------------------------------------- 1 | const ABI = [{ "inputs": [{ "internalType": "address", "name": "_USDCToken", "type": "address" }, { "internalType": "address", "name": "_USDCxToken", "type": "address" }, { "internalType": "address", "name": "_SuperfluidHost", "type": "address" }], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "borrower", "type": "address" }, { "indexed": false, "internalType": "address", "name": "token", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "AddedCollateral", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "lender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "AddedUSDC", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" }], "name": "OwnershipTransferred", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "borrower", "type": "address" }, { "indexed": false, "internalType": "address", "name": "token", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "RemovedCollateral", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "lender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "RemovedUSDC", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "borrower", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "RepaidDebt", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "borrower", "type": "address" }, { "indexed": false, "internalType": "address", "name": "receiver", "type": "address" }, { "components": [{ "internalType": "address", "name": "receiver", "type": "address" }, { "internalType": "int96", "name": "flowRate", "type": "int96" }, { "internalType": "uint256", "name": "start", "type": "uint256" }, { "internalType": "uint256", "name": "end", "type": "uint256" }], "indexed": false, "internalType": "struct Thels.Stream", "name": "stream", "type": "tuple" }], "name": "StartedStream", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "borrower", "type": "address" }, { "indexed": false, "internalType": "address", "name": "receiver", "type": "address" }, { "components": [{ "internalType": "address", "name": "receiver", "type": "address" }, { "internalType": "int96", "name": "flowRate", "type": "int96" }, { "internalType": "uint256", "name": "start", "type": "uint256" }, { "internalType": "uint256", "name": "end", "type": "uint256" }], "indexed": false, "internalType": "struct Thels.Stream", "name": "stream", "type": "tuple" }], "name": "StoppedStream", "type": "event" }, { "inputs": [], "name": "FEE", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "USDCToken", "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "USDCxToken", "outputs": [{ "internalType": "contract ISuperToken", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "tokenAddress", "type": "address" }, { "internalType": "address", "name": "priceFeedAddress", "type": "address" }, { "internalType": "uint256", "name": "borrowPercent", "type": "uint256" }], "name": "allowToken", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "name": "allowedTokenList", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "allowedTokens", "outputs": [{ "internalType": "address", "name": "tokenAddress", "type": "address" }, { "internalType": "contract AggregatorV3Interface", "name": "priceFeed", "type": "address" }, { "internalType": "uint256", "name": "borrowPercent", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "borrowAmounts", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "cfaV1", "outputs": [{ "internalType": "contract ISuperfluid", "name": "host", "type": "address" }, { "internalType": "contract IConstantFlowAgreementV1", "name": "cfa", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "convertToUSDC", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "convertToUSDCx", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "deposit", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }, { "internalType": "address", "name": "", "type": "address" }], "name": "depositAmounts", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "name": "deposited", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], "name": "getBorrowableAmount", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], "name": "getCollateralValue", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalUSDCx", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "lendAmounts", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "repay", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "tokenAddress", "type": "address" }], "name": "revokeToken", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "receiver", "type": "address" }, { "internalType": "int96", "name": "flowRate", "type": "int96" }, { "internalType": "uint256", "name": "endTime", "type": "uint256" }], "name": "startStream", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "receiver", "type": "address" }], "name": "stopStream", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }, { "internalType": "address", "name": "", "type": "address" }], "name": "streams", "outputs": [{ "internalType": "address", "name": "receiver", "type": "address" }, { "internalType": "int96", "name": "flowRate", "type": "int96" }, { "internalType": "uint256", "name": "start", "type": "uint256" }, { "internalType": "uint256", "name": "end", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "token", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function" }] 2 | export const ERC20_ABI = [ 3 | { 4 | "constant": true, 5 | "inputs": [], 6 | "name": "name", 7 | "outputs": [ 8 | { 9 | "name": "", 10 | "type": "string" 11 | } 12 | ], 13 | "payable": false, 14 | "stateMutability": "view", 15 | "type": "function" 16 | }, 17 | { 18 | "constant": false, 19 | "inputs": [ 20 | { 21 | "name": "_spender", 22 | "type": "address" 23 | }, 24 | { 25 | "name": "_value", 26 | "type": "uint256" 27 | } 28 | ], 29 | "name": "approve", 30 | "outputs": [ 31 | { 32 | "name": "", 33 | "type": "bool" 34 | } 35 | ], 36 | "payable": false, 37 | "stateMutability": "nonpayable", 38 | "type": "function" 39 | }, 40 | { 41 | "constant": true, 42 | "inputs": [], 43 | "name": "totalSupply", 44 | "outputs": [ 45 | { 46 | "name": "", 47 | "type": "uint256" 48 | } 49 | ], 50 | "payable": false, 51 | "stateMutability": "view", 52 | "type": "function" 53 | }, 54 | { 55 | "constant": false, 56 | "inputs": [ 57 | { 58 | "name": "_from", 59 | "type": "address" 60 | }, 61 | { 62 | "name": "_to", 63 | "type": "address" 64 | }, 65 | { 66 | "name": "_value", 67 | "type": "uint256" 68 | } 69 | ], 70 | "name": "transferFrom", 71 | "outputs": [ 72 | { 73 | "name": "", 74 | "type": "bool" 75 | } 76 | ], 77 | "payable": false, 78 | "stateMutability": "nonpayable", 79 | "type": "function" 80 | }, 81 | { 82 | "constant": true, 83 | "inputs": [], 84 | "name": "decimals", 85 | "outputs": [ 86 | { 87 | "name": "", 88 | "type": "uint8" 89 | } 90 | ], 91 | "payable": false, 92 | "stateMutability": "view", 93 | "type": "function" 94 | }, 95 | { 96 | "constant": true, 97 | "inputs": [ 98 | { 99 | "name": "_owner", 100 | "type": "address" 101 | } 102 | ], 103 | "name": "balanceOf", 104 | "outputs": [ 105 | { 106 | "name": "balance", 107 | "type": "uint256" 108 | } 109 | ], 110 | "payable": false, 111 | "stateMutability": "view", 112 | "type": "function" 113 | }, 114 | { 115 | "constant": true, 116 | "inputs": [], 117 | "name": "symbol", 118 | "outputs": [ 119 | { 120 | "name": "", 121 | "type": "string" 122 | } 123 | ], 124 | "payable": false, 125 | "stateMutability": "view", 126 | "type": "function" 127 | }, 128 | { 129 | "constant": false, 130 | "inputs": [ 131 | { 132 | "name": "_to", 133 | "type": "address" 134 | }, 135 | { 136 | "name": "_value", 137 | "type": "uint256" 138 | } 139 | ], 140 | "name": "transfer", 141 | "outputs": [ 142 | { 143 | "name": "", 144 | "type": "bool" 145 | } 146 | ], 147 | "payable": false, 148 | "stateMutability": "nonpayable", 149 | "type": "function" 150 | }, 151 | { 152 | "constant": true, 153 | "inputs": [ 154 | { 155 | "name": "_owner", 156 | "type": "address" 157 | }, 158 | { 159 | "name": "_spender", 160 | "type": "address" 161 | } 162 | ], 163 | "name": "allowance", 164 | "outputs": [ 165 | { 166 | "name": "", 167 | "type": "uint256" 168 | } 169 | ], 170 | "payable": false, 171 | "stateMutability": "view", 172 | "type": "function" 173 | }, 174 | { 175 | "payable": true, 176 | "stateMutability": "payable", 177 | "type": "fallback" 178 | }, 179 | { 180 | "anonymous": false, 181 | "inputs": [ 182 | { 183 | "indexed": true, 184 | "name": "owner", 185 | "type": "address" 186 | }, 187 | { 188 | "indexed": true, 189 | "name": "spender", 190 | "type": "address" 191 | }, 192 | { 193 | "indexed": false, 194 | "name": "value", 195 | "type": "uint256" 196 | } 197 | ], 198 | "name": "Approval", 199 | "type": "event" 200 | }, 201 | { 202 | "anonymous": false, 203 | "inputs": [ 204 | { 205 | "indexed": true, 206 | "name": "from", 207 | "type": "address" 208 | }, 209 | { 210 | "indexed": true, 211 | "name": "to", 212 | "type": "address" 213 | }, 214 | { 215 | "indexed": false, 216 | "name": "value", 217 | "type": "uint256" 218 | } 219 | ], 220 | "name": "Transfer", 221 | "type": "event" 222 | } 223 | ] 224 | export default ABI; -------------------------------------------------------------------------------- /backend/contracts/streaming/ThelsStream.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.7.0; 4 | pragma experimental ABIEncoderV2; 5 | 6 | import {ISuperfluid, ISuperfluidToken} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 7 | 8 | import {ISuperToken} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; 9 | 10 | import {IConstantFlowAgreementV1} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol"; 11 | 12 | import {CFAv1Library} from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; 13 | 14 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 15 | import "@openzeppelin/contracts/access/Ownable.sol"; 16 | import "@openzeppelin/contracts/math/SafeMath.sol"; 17 | 18 | contract SAStream is Ownable { 19 | using CFAv1Library for CFAv1Library.InitData; 20 | using SafeMath for uint256; 21 | 22 | uint256 public _minPenalty = 1000 * (10**18); // for STABLES 1000 USD 23 | uint256 public _minStreamTime = 604800; // 7 DAYS in SECs 24 | 25 | struct Stream { 26 | int96 flowRate; 27 | uint256 start; 28 | uint256 end; 29 | uint256 bufferAmount; 30 | uint256 claimPenaltyAvailable; 31 | uint256 penaltyAmount; 32 | } 33 | 34 | // superToken => sender => recipient => Stream 35 | mapping(address => mapping(address => mapping(address => Stream))) 36 | public streams; 37 | // avalaible Superfluid tokens 38 | mapping(address => mapping(address => uint256)) 39 | public availableSFTokenBalances; 40 | // avalaible ERC20 tokens 41 | mapping(address => mapping(address => uint256)) public erc20TokenBalances; 42 | 43 | event Erc20Deposited(address erc20token, address from, uint256 amount); 44 | event TokenAllowed(address erc20token); 45 | event StreamInitiated( 46 | address superToken, 47 | address sender, 48 | address receiver, 49 | int96 flowRate 50 | ); 51 | event StreamCanceled(address superToken, address sender, address receiver); 52 | 53 | //initialize cfaV1 variable 54 | CFAv1Library.InitData public cfaV1; 55 | 56 | constructor(ISuperfluid host) { 57 | //initialize InitData struct, and set equal to cfaV1 58 | cfaV1 = CFAv1Library.InitData( 59 | host, 60 | //here, we are deriving the address of the CFA using the host contract 61 | IConstantFlowAgreementV1( 62 | address( 63 | host.getAgreementClass( 64 | keccak256( 65 | "org.superfluid-finance.agreements.ConstantFlowAgreement.v1" 66 | ) 67 | ) 68 | ) 69 | ) 70 | ); 71 | } 72 | 73 | // deposit ERC20 token to the contract 74 | function depositErc20(address _token, uint256 _amount) 75 | public 76 | returns (bool) 77 | { 78 | require(_amount > 0, "Amount must be higher than zero!"); 79 | erc20TokenBalances[_token][msg.sender] = erc20TokenBalances[_token][ 80 | msg.sender 81 | ].add(_amount); 82 | IERC20(_token).transferFrom(msg.sender, address(this), _amount); 83 | emit Erc20Deposited(_token, msg.sender, _amount); 84 | return true; 85 | } 86 | 87 | // withdraw ERC20 token from the contract 88 | function withdrawErc20( 89 | address _recipient, 90 | address _token, 91 | uint256 _amount 92 | ) public returns (bool) { 93 | require( 94 | erc20TokenBalances[_token][msg.sender].sub(_amount) >= 0, 95 | "Not enough tokens to withdraw" 96 | ); 97 | erc20TokenBalances[_token][_recipient] = erc20TokenBalances[_token][ 98 | _recipient 99 | ].sub(_amount); 100 | IERC20(_token).transfer(_recipient, _amount); 101 | return true; 102 | } 103 | 104 | // get ERC 20 token balance of the owner 105 | function getErc20TokenBalance(address _tokenOwner, address _token) 106 | public 107 | view 108 | returns (uint256) 109 | { 110 | return erc20TokenBalances[_token][_tokenOwner]; 111 | } 112 | 113 | // get superfluid token balance of the owner 114 | function getSFTokenBalance(address _tokenOwner, address _superfluidToken) 115 | public 116 | view 117 | returns (uint256) 118 | { 119 | // return ISuperToken(_superfluidToken).balanceOf(_tokenOwner); // wont work as a sender is this contract 120 | return availableSFTokenBalances[_superfluidToken][_tokenOwner]; 121 | } 122 | 123 | // get info of stream 124 | function getStream( 125 | address _superToken, 126 | address _sender, 127 | address _recipient 128 | ) public view returns (Stream memory) { 129 | return streams[_superToken][_sender][_recipient]; 130 | } 131 | 132 | // upgrade erc20 token to superfluid token 133 | function upgradeToken( 134 | address _token, 135 | address _superToken, 136 | uint256 _amount 137 | ) public returns (bool) { 138 | require( 139 | erc20TokenBalances[_token][msg.sender].sub(_amount) >= 0, 140 | "Not enough tokens to upgrade" 141 | ); 142 | // update balances 143 | erc20TokenBalances[_token][msg.sender] = erc20TokenBalances[_token][ 144 | msg.sender 145 | ].sub(_amount); 146 | availableSFTokenBalances[_superToken][ 147 | msg.sender 148 | ] = availableSFTokenBalances[_superToken][msg.sender].add(_amount); 149 | // alow to upgrade erc20 tokens 150 | IERC20(_token).approve(_superToken, _amount); 151 | // upgrade erc20 token to Superfluid token 152 | ISuperToken(_superToken).upgrade(_amount); 153 | } 154 | 155 | // downgrade superfluid token to erc20 token 156 | function downgradeToken( 157 | address _token, 158 | address _superToken, 159 | uint256 _amount 160 | ) public returns (bool) { 161 | require( 162 | availableSFTokenBalances[_superToken][msg.sender].sub(_amount) >= 0, 163 | "Not enough Superfluid Tokens to downgrade" 164 | ); 165 | // update balances 166 | erc20TokenBalances[_token][msg.sender] = erc20TokenBalances[_token][ 167 | msg.sender 168 | ].add(_amount); 169 | availableSFTokenBalances[_superToken][ 170 | msg.sender 171 | ] = availableSFTokenBalances[_superToken][msg.sender].sub(_amount); 172 | ISuperToken(_superToken).downgrade(_amount); 173 | } 174 | 175 | // init stream 176 | function startStream( 177 | address _receiver, 178 | address _superToken, 179 | int96 _flowRate, // in wei/second 180 | uint256 _bufferTime, // in seconds 181 | uint256 _streamingPeriod // in seconds 182 | ) public { 183 | require( 184 | (availableSFTokenBalances[_superToken][msg.sender] - 185 | calculateStreamAmount(_flowRate, _streamingPeriod) - 186 | calculateBuffer(_flowRate, _bufferTime) - 187 | calculatePenalty(_flowRate)) > 0, 188 | "Not enough Supertokens to stream" 189 | ); 190 | require( 191 | streams[_superToken][msg.sender][_receiver].flowRate == 0, 192 | "You have been already streaming to this account, you need to cancel stream first." 193 | ); 194 | 195 | ISuperfluidToken superToken = ISuperfluidToken(_superToken); 196 | // update superfluid token balance of the sender 197 | uint256 _streamAmount = calculateStreamAmount( 198 | _flowRate, 199 | _streamingPeriod 200 | ); 201 | uint256 _bufferAmount = calculateBuffer(_flowRate, _bufferTime); 202 | uint256 _penaltyAmount = calculatePenalty(_flowRate); 203 | uint256 reservedTokens = _streamAmount + _bufferAmount + _penaltyAmount; 204 | 205 | // update balance 206 | availableSFTokenBalances[_superToken][ 207 | msg.sender 208 | ] = availableSFTokenBalances[_superToken][msg.sender].sub( 209 | reservedTokens 210 | ); 211 | 212 | // set the stream into mapping 213 | uint256 _end = (block.timestamp).add(_streamingPeriod); 214 | uint256 _claimPenaltyAvailable = _end.add(_bufferTime); 215 | addStream( 216 | _superToken, 217 | msg.sender, 218 | _receiver, 219 | _flowRate, 220 | block.timestamp, 221 | _end, 222 | _bufferAmount, 223 | _claimPenaltyAvailable, 224 | _penaltyAmount 225 | ); 226 | 227 | // start stream 228 | cfaV1.createFlow(_receiver, superToken, _flowRate); 229 | emit StreamInitiated(_superToken, msg.sender, _receiver, _flowRate); 230 | } 231 | 232 | // add stream info to streams struct 233 | function addStream( 234 | address _superToken, 235 | address _sender, 236 | address _receiver, 237 | int96 _flowrate, 238 | uint256 _start, 239 | uint256 _end, 240 | uint256 _bufferAmount, 241 | uint256 _claimPenaltyAvailable, 242 | uint256 _penaltyAmount 243 | ) internal { 244 | Stream storage s = streams[_superToken][_sender][_receiver]; 245 | s.flowRate = _flowrate; 246 | s.start = _start; 247 | s.end = _end; 248 | s.bufferAmount = _bufferAmount; 249 | s.claimPenaltyAvailable = _claimPenaltyAvailable; 250 | s.penaltyAmount = _penaltyAmount; 251 | } 252 | 253 | function calculateStreamAmount(int96 _flowRate, uint256 _streamingPeriod) 254 | public 255 | view 256 | returns (uint256) 257 | { 258 | return (uint256(_flowRate) * _streamingPeriod); 259 | } 260 | 261 | function calculateBuffer(int96 _flowrate, uint256 _bufferTime) 262 | public 263 | view 264 | returns (uint256) 265 | { 266 | uint256 buffer; 267 | buffer = uint256(_flowrate) * _bufferTime; 268 | return buffer; 269 | } 270 | 271 | function calculatePenalty(int96 _flowrate) public view returns (uint256) { 272 | uint256 calculatedPenalty = uint256(_flowrate) * _minStreamTime; 273 | return 274 | calculatedPenalty < _minPenalty ? _minPenalty : calculatedPenalty; 275 | } 276 | 277 | function changePenaltyParameters( 278 | uint256 _newMinPenalty, 279 | uint256 _newStreamTine 280 | ) public onlyOwner { 281 | _minPenalty = _newMinPenalty; 282 | _minStreamTime = _newStreamTine; 283 | } 284 | 285 | function resetStream( 286 | address _superToken, 287 | address _sender, 288 | address _receiver 289 | ) internal { 290 | streams[_superToken][_sender][_receiver].flowRate = 0; 291 | streams[_superToken][_sender][_receiver].start = 0; 292 | streams[_superToken][_sender][_receiver].end = 0; 293 | streams[_superToken][_sender][_receiver].bufferAmount = 0; 294 | streams[_superToken][_sender][_receiver].claimPenaltyAvailable = 0; 295 | streams[_superToken][_sender][_receiver].penaltyAmount = 0; 296 | } 297 | 298 | function deleteFlow( 299 | address _sender, 300 | address _receiver, 301 | address _superToken 302 | ) public { 303 | require( 304 | streams[_superToken][_sender][_receiver].flowRate != 0, 305 | "There is no stream to be deleted" 306 | ); 307 | 308 | ISuperfluidToken superToken = ISuperfluidToken(_superToken); 309 | 310 | uint256 _endStreamTime = streams[_superToken][_sender][_receiver].end; 311 | uint256 _endBufferTime = streams[_superToken][_sender][_receiver] 312 | .claimPenaltyAvailable; 313 | uint256 _paybackTotal = 0; 314 | 315 | if (block.timestamp <= _endBufferTime) { 316 | require( 317 | _sender == msg.sender, 318 | "Only owner can delete flow right now!" 319 | ); 320 | 321 | uint256 _paybackPenalty = streams[_superToken][_sender][_receiver] 322 | .penaltyAmount; 323 | 324 | if (block.timestamp <= _endStreamTime) { 325 | uint256 _paybackStream = calculateRemainingStream( 326 | _sender, 327 | _receiver, 328 | _superToken 329 | ); 330 | uint256 _paybackBuffer = streams[_superToken][_sender][ 331 | _receiver 332 | ].bufferAmount; 333 | _paybackTotal = 334 | _paybackPenalty + 335 | _paybackBuffer + 336 | _paybackStream; 337 | } 338 | 339 | if ( 340 | (block.timestamp > _endStreamTime) && 341 | (block.timestamp <= _endBufferTime) 342 | ) { 343 | uint256 _paybackBuffer = calculateRemainingBuffer( 344 | _sender, 345 | _receiver, 346 | _superToken 347 | ); 348 | _paybackTotal = _paybackPenalty + _paybackBuffer; 349 | } 350 | 351 | // update balance and reset stream struct 352 | availableSFTokenBalances[_superToken][ 353 | msg.sender 354 | ] = availableSFTokenBalances[_superToken][msg.sender].add( 355 | _paybackTotal 356 | ); 357 | resetStream(_superToken, _sender, _receiver); 358 | // cancel flow 359 | cfaV1.deleteFlow(address(this), _receiver, superToken); 360 | emit StreamCanceled(_superToken, _sender, _receiver); 361 | } else { 362 | _paybackTotal = calculateRemainingPenalty( 363 | _sender, 364 | _receiver, 365 | _superToken 366 | ); 367 | // update balance and reset stream struct 368 | availableSFTokenBalances[_superToken][ 369 | msg.sender 370 | ] = availableSFTokenBalances[_superToken][msg.sender].add( 371 | _paybackTotal 372 | ); 373 | resetStream(_superToken, _sender, _receiver); 374 | // cancel flow 375 | cfaV1.deleteFlow(address(this), _receiver, superToken); 376 | emit StreamCanceled(_superToken, _sender, _receiver); 377 | } 378 | } 379 | 380 | function calculateRemainingStream( 381 | address _sender, 382 | address _receiver, 383 | address _superToken 384 | ) internal returns (uint256) { 385 | uint256 _timeNow = block.timestamp; 386 | uint256 _startTime = streams[_superToken][_sender][_receiver].start; 387 | uint256 _endTime = streams[_superToken][_sender][_receiver].end; 388 | uint256 _flowRate = uint256( 389 | streams[_superToken][_sender][_receiver].flowRate 390 | ); 391 | uint256 _amountSent = ((_timeNow - _startTime) * _flowRate); 392 | uint256 _originalAmountToSend = ((_endTime - _startTime) * _flowRate); 393 | return (_originalAmountToSend - _amountSent); 394 | } 395 | 396 | function calculateRemainingBuffer( 397 | address _sender, 398 | address _receiver, 399 | address _superToken 400 | ) internal returns (uint256) { 401 | uint256 _timeNow = block.timestamp; 402 | uint256 _endTime = streams[_superToken][_sender][_receiver].end; 403 | uint256 _flowRate = uint256( 404 | streams[_superToken][_sender][_receiver].flowRate 405 | ); 406 | return ((_timeNow - _endTime) * _flowRate); 407 | } 408 | 409 | function calculateRemainingPenalty( 410 | address _sender, 411 | address _receiver, 412 | address _superToken 413 | ) internal returns (uint256) { 414 | uint256 _penalty = streams[_superToken][_sender][_receiver] 415 | .penaltyAmount; 416 | uint256 _flowRate = uint256( 417 | streams[_superToken][_sender][_receiver].flowRate 418 | ); 419 | uint256 _endBufferTime = streams[_superToken][_sender][_receiver] 420 | .claimPenaltyAvailable; 421 | return (_penalty - ((block.timestamp - _endBufferTime) * _flowRate)); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /backend/contracts/StreamAbsraction.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; 8 | import "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; 9 | import "@chainlink/contracts/src/v0.7/interfaces/AggregatorV3Interface.sol"; 10 | import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; 11 | 12 | contract StreamAbsraction is Ownable { 13 | using CFAv1Library for CFAv1Library.InitData; 14 | 15 | struct Token { 16 | address tokenAddress; 17 | AggregatorV3Interface priceFeed; // Chainlink price feed 18 | uint256 borrowPercent; // 100 => 10.0% 19 | } 20 | 21 | struct Stream { 22 | address receiver; 23 | int96 flowRate; 24 | uint256 start; 25 | uint256 end; 26 | uint256 fee; 27 | uint256 buffer; 28 | } 29 | 30 | event AddedCollateral( 31 | address indexed borrower, 32 | address token, 33 | uint256 amount 34 | ); 35 | event RemovedCollateral( 36 | address indexed borrower, 37 | address token, 38 | uint256 amount 39 | ); 40 | event RepaidDebt(address indexed borrower, uint256 amount); 41 | event StartedStream( 42 | address indexed borrower, 43 | address receiver, 44 | Stream stream 45 | ); 46 | event StoppedStream( 47 | address indexed borrower, 48 | address receiver, 49 | Stream stream 50 | ); 51 | event AddedUSDC(address indexed lender, uint256 amount); 52 | event RemovedUSDC(address indexed lender, uint256 amount); 53 | event Liquidated(address indexed borrower, uint256 amount); 54 | 55 | uint256 public constant FEE = 50; // 50 => 5.0% 56 | uint256 public constant BUFFER = 20; // 20 => 2.0% 57 | 58 | mapping(address => mapping(address => uint256)) public depositAmounts; // tokens and deposit amounts of each user 59 | mapping(address => mapping(address => Stream)) public streams; // opened streams of each user 60 | mapping(address => uint256) public lendAmounts; // USDC lend amount of each user 61 | mapping(address => uint256) public borrowAmounts; // USDC borrow amount of each user 62 | mapping(address => Token) public allowedTokens; // mapping that shows if a token can be used as collateral 63 | address[] public deposited; // users who have made a deposit 64 | address[] public allowedTokenList; // list of tokens that can be used as collateral 65 | IERC20 public USDCToken; 66 | ISuperToken public USDCxToken; 67 | CFAv1Library.InitData public cfaV1; 68 | ISwapRouter public router; 69 | 70 | constructor( 71 | address _USDCToken, 72 | address _USDCxToken, 73 | address _SuperfluidHost, 74 | address _UniswapRouter 75 | ) { 76 | USDCToken = IERC20(_USDCToken); 77 | USDCxToken = ISuperToken(_USDCxToken); 78 | ISuperfluid _host = ISuperfluid(_SuperfluidHost); 79 | router = ISwapRouter(_UniswapRouter); 80 | // initialize InitData struct, and set equal to cfaV1 81 | cfaV1 = CFAv1Library.InitData( 82 | _host, 83 | // here, we are deriving the address of the CFA using the host contract 84 | IConstantFlowAgreementV1( 85 | address( 86 | _host.getAgreementClass( 87 | keccak256( 88 | "org.superfluid-finance.agreements.ConstantFlowAgreement.v1" 89 | ) 90 | ) 91 | ) 92 | ) 93 | ); 94 | 95 | // approve tokens for Superfluid contract 96 | USDCToken.approve(_USDCxToken, type(uint256).max); 97 | } 98 | 99 | // deposit an allowed token 100 | function deposit(address token, uint256 amount) public { 101 | require( 102 | allowedTokens[token].tokenAddress != address(0), 103 | "Token is not allowed as collateral." 104 | ); 105 | IERC20 _token = IERC20(token); 106 | _token.transferFrom(msg.sender, address(this), amount); 107 | depositAmounts[msg.sender][token] += amount; 108 | emit AddedCollateral(msg.sender, token, amount); 109 | } 110 | 111 | // withdraw deposited token 112 | function withdraw(address token, uint256 amount) public { 113 | require( 114 | allowedTokens[token].tokenAddress != address(0), 115 | "Token is not allowed as collateral." 116 | ); 117 | require( 118 | depositAmounts[msg.sender][token] >= amount, 119 | "Not enough balance." 120 | ); 121 | require( 122 | getBorrowableAmount(msg.sender) >= 123 | (((getTokenPrice(allowedTokens[token]) * amount) / (10**21)) * 124 | allowedTokens[token].borrowPercent + 125 | borrowAmounts[msg.sender]), 126 | "Cannot withdraw without paying debt." 127 | ); 128 | IERC20 _token = IERC20(token); 129 | depositAmounts[msg.sender][token] -= amount; 130 | _token.transfer(msg.sender, amount); 131 | emit RemovedCollateral(msg.sender, token, amount); 132 | } 133 | 134 | // repay debt 135 | function repay(uint256 amount) public { 136 | require( 137 | amount <= borrowAmounts[msg.sender], 138 | "Cannot repay more than owed." 139 | ); 140 | USDCToken.transferFrom(msg.sender, address(this), amount); 141 | USDCxToken.upgrade(amount); 142 | borrowAmounts[msg.sender] -= amount; 143 | emit RepaidDebt(msg.sender, amount); 144 | } 145 | 146 | // start a stream to an address 147 | // receiver: receiving address 148 | // flowRate: amount of wei / second 149 | // endTime: unix timestamp of ending time 150 | function startStream( 151 | address receiver, 152 | int96 flowRate, 153 | uint256 endTime 154 | ) public { 155 | require(endTime > block.timestamp, "Cannot set end time to past."); 156 | Stream storage stream = streams[msg.sender][receiver]; 157 | require(stream.start == 0, "Stream already exists."); 158 | uint256 totalBorrow = uint256(flowRate) * (endTime - block.timestamp); 159 | uint256 fee = (totalBorrow * FEE) / 1000; 160 | uint256 buffer = (totalBorrow * BUFFER) / 1000; 161 | require( 162 | totalBorrow + fee + buffer < getBorrowableAmount(msg.sender), 163 | "Cannot borrow more than allowed." 164 | ); 165 | borrowAmounts[msg.sender] += totalBorrow + fee + buffer; 166 | addStream( 167 | stream, 168 | receiver, 169 | flowRate, 170 | block.timestamp, 171 | endTime, 172 | fee, 173 | buffer 174 | ); 175 | cfaV1.createFlow(receiver, USDCxToken, flowRate); 176 | emit StartedStream(msg.sender, receiver, stream); 177 | } 178 | 179 | // stop a previously opened stream and distribute fee rewards, 180 | // also refund buffer if closed before expiring. 181 | function stopStream(address receiver) public { 182 | Stream storage stream = streams[msg.sender][receiver]; 183 | require(stream.start != 0, "Stream does not exist."); 184 | cfaV1.deleteFlow(address(this), receiver, USDCxToken); 185 | uint256 extraDebt = getRemainingAmount(stream); 186 | if (!hasElapsed(stream)) { 187 | extraDebt += stream.buffer; 188 | } 189 | if (extraDebt > 0) { 190 | // will be reduced from debt, if debt is paid, 191 | // it is added to lent amount instead 192 | if (borrowAmounts[msg.sender] < extraDebt) { 193 | lendAmounts[msg.sender] += 194 | extraDebt - 195 | borrowAmounts[msg.sender]; 196 | borrowAmounts[msg.sender] = 0; 197 | } else { 198 | borrowAmounts[msg.sender] -= extraDebt; 199 | } 200 | } 201 | distributeRewards(stream.fee); 202 | emit StoppedStream(msg.sender, receiver, stream); 203 | delete streams[msg.sender][receiver]; 204 | } 205 | 206 | // lend USDC 207 | function convertToUSDCx(uint256 amount) public { 208 | USDCToken.transferFrom(msg.sender, address(this), amount); 209 | USDCxToken.upgrade(amount); 210 | if (lendAmounts[msg.sender] == 0) { 211 | deposited.push(msg.sender); 212 | } 213 | lendAmounts[msg.sender] += amount; 214 | emit AddedUSDC(msg.sender, amount); 215 | } 216 | 217 | // withdraw USDC 218 | function convertToUSDC(uint256 amount) public { 219 | require( 220 | amount <= lendAmounts[msg.sender], 221 | "Cannot withdraw more than supplied." 222 | ); 223 | lendAmounts[msg.sender] -= amount; 224 | USDCxToken.downgrade(amount); 225 | USDCToken.transfer(msg.sender, amount); 226 | emit RemovedUSDC(msg.sender, amount); 227 | } 228 | 229 | // liquidate borrower's token if the price of it drops below the borrowing amount 230 | function liquidate(address user) public { 231 | require( 232 | getBorrowableAmount(user) < borrowAmounts[user], 233 | "Colatteral is larger than debt." 234 | ); 235 | 236 | uint256 amount = 0; 237 | for (uint256 i = 0; i < allowedTokenList.length; i++) { 238 | ISwapRouter.ExactInputSingleParams memory params = ISwapRouter 239 | .ExactInputSingleParams( 240 | allowedTokenList[i], 241 | address(USDCToken), 242 | 3000, 243 | address(this), 244 | block.timestamp + 600, 245 | depositAmounts[user][allowedTokenList[i]], 246 | 0, 247 | 0 248 | ); 249 | depositAmounts[user][allowedTokenList[i]] = 0; 250 | amount += router.exactInputSingle(params); 251 | } 252 | uint256 rewardAmount = (amount * FEE) / 1000; 253 | USDCToken.transfer(msg.sender, rewardAmount); 254 | USDCxToken.upgrade(amount - rewardAmount); 255 | lendAmounts[address(this)] += amount - rewardAmount; 256 | emit Liquidated(user, amount); 257 | } 258 | 259 | // stop stream if the stream has past the end time 260 | function stopFinishedStream(address sender, address receiver) public { 261 | Stream storage stream = streams[sender][receiver]; 262 | require(stream.end < block.timestamp, "Stream has not finished."); 263 | cfaV1.deleteFlow(address(this), receiver, USDCxToken); 264 | uint256 rewardAmount = (stream.buffer * FEE) / 1000; 265 | USDCxToken.downgrade(rewardAmount); 266 | USDCToken.transfer(msg.sender, rewardAmount); 267 | lendAmounts[address(this)] += stream.buffer - rewardAmount; 268 | distributeRewards(stream.fee); 269 | emit StoppedStream(sender, receiver, stream); 270 | delete streams[sender][receiver]; 271 | } 272 | 273 | // gets the total collateral value of a user 274 | function getCollateralValue(address user) public view returns (uint256) { 275 | uint256 totalValue = 0; 276 | for (uint256 i = 0; i < allowedTokenList.length; i++) { 277 | uint256 currentTokenAmount = depositAmounts[user][ 278 | allowedTokenList[i] 279 | ]; 280 | if (currentTokenAmount > 0) { 281 | totalValue += 282 | (currentTokenAmount * 283 | getTokenPrice(allowedTokens[allowedTokenList[i]])) / 284 | 10**18; 285 | } 286 | } 287 | return totalValue; 288 | } 289 | 290 | // get the total borrowable amount of a user 291 | function getBorrowableAmount(address user) public view returns (uint256) { 292 | uint256 totalValue = 0; 293 | for (uint256 i = 0; i < allowedTokenList.length; i++) { 294 | uint256 currentTokenAmount = depositAmounts[user][ 295 | allowedTokenList[i] 296 | ]; 297 | if (currentTokenAmount > 0) { 298 | totalValue += 299 | (currentTokenAmount * 300 | getTokenPrice(allowedTokens[allowedTokenList[i]]) * 301 | allowedTokens[allowedTokenList[i]].borrowPercent) / 302 | 10**21; 303 | } 304 | } 305 | if (totalValue < borrowAmounts[user]) { 306 | return 0; 307 | } 308 | return totalValue - borrowAmounts[user]; 309 | } 310 | 311 | function getTotalUSDCx() public view returns (uint256) { 312 | return USDCxToken.balanceOf(address(this)); 313 | } 314 | 315 | // allow token as collateral: admin function 316 | function allowToken( 317 | address tokenAddress, 318 | address priceFeedAddress, 319 | uint256 borrowPercent 320 | ) public onlyOwner { 321 | require( 322 | allowedTokens[tokenAddress].tokenAddress == address(0), 323 | "Token is already allowed." 324 | ); 325 | allowedTokens[tokenAddress] = Token( 326 | tokenAddress, 327 | AggregatorV3Interface(priceFeedAddress), 328 | borrowPercent 329 | ); 330 | allowedTokenList.push(tokenAddress); 331 | } 332 | 333 | // remove token from being a collateral: admin function 334 | // TODO: repay all deposited tokens 335 | function revokeToken(address tokenAddress) public onlyOwner { 336 | require( 337 | allowedTokens[tokenAddress].tokenAddress != address(0), 338 | "Token is not allowed." 339 | ); 340 | delete allowedTokens[tokenAddress]; 341 | 342 | // remove token from array 343 | for (uint256 i = 0; i < allowedTokenList.length; i++) { 344 | if (allowedTokenList[i] == tokenAddress) { 345 | allowedTokenList[i] = allowedTokenList[ 346 | allowedTokenList.length - 1 347 | ]; 348 | allowedTokenList.pop(); 349 | return; 350 | } 351 | } 352 | } 353 | 354 | // withdraw protocol fees: admin function 355 | function withdrawFees(uint256 amount) public onlyOwner { 356 | require( 357 | amount <= lendAmounts[address(this)], 358 | "Cannot withdraw more than earned." 359 | ); 360 | USDCxToken.downgrade(amount); 361 | USDCToken.transfer(msg.sender, amount); 362 | lendAmounts[address(this)] -= amount; 363 | } 364 | 365 | // distributes amount of fee to lenders 366 | function distributeRewards(uint256 amount) private { 367 | uint256 totalLendAmount = 0; 368 | for (uint256 i = 0; i < deposited.length; i++) { 369 | totalLendAmount += lendAmounts[deposited[i]]; 370 | } 371 | 372 | for (uint256 i = 0; i < deposited.length; i++) { 373 | lendAmounts[deposited[i]] += 374 | (amount * lendAmounts[deposited[i]]) / 375 | totalLendAmount; 376 | } 377 | } 378 | 379 | // add stream info to streams struct 380 | function addStream( 381 | Stream storage stream, 382 | address receiver, 383 | int96 flowRate, 384 | uint256 start, 385 | uint256 end, 386 | uint256 fee, 387 | uint256 buffer 388 | ) private { 389 | stream.receiver = receiver; 390 | stream.flowRate = flowRate; 391 | stream.start = start; 392 | stream.end = end; 393 | stream.fee = fee; 394 | stream.buffer = buffer; 395 | } 396 | 397 | // returns the price in wei (10^18) 398 | function getTokenPrice(Token memory token) private view returns (uint256) { 399 | (, int256 price, , , ) = token.priceFeed.latestRoundData(); 400 | return uint256(price) * 10**(18 - token.priceFeed.decimals()); 401 | } 402 | 403 | // gets the remaining time for a stream in seconds 404 | function getRemainingAmount(Stream memory stream) 405 | private 406 | view 407 | returns (uint256) 408 | { 409 | if (stream.end < block.timestamp) { 410 | return 0; 411 | } 412 | return (stream.end - block.timestamp) * uint256(stream.flowRate); 413 | } 414 | 415 | // returns true if stream has elapsed 416 | function hasElapsed(Stream memory stream) private view returns (bool) { 417 | return (block.timestamp < stream.end); 418 | } 419 | 420 | // gets the total amount of stream in seconds 421 | function getTotalAmount(Stream memory stream) 422 | private 423 | pure 424 | returns (uint256) 425 | { 426 | return (stream.end - stream.start) * uint256(stream.flowRate); 427 | } 428 | 429 | // adds fee to an amount 430 | function addFee(uint256 amount, uint256 fee) 431 | private 432 | pure 433 | returns (uint256) 434 | { 435 | return (amount * ((1000 + fee) / 1000)); 436 | } 437 | } 438 | --------------------------------------------------------------------------------