├── 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 |
Go to Dashboard
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | authenticate({onSuccess:()=>toast.success("Wallet Connected Successfully"),onError:()=>toast.error("Error connecting to wallet")})} className={btnStyle}>
23 | Connect Wallet
24 |
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 |
25 | Stream Abstraction
26 |
36 |
37 |
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 | {pending ? "Transaction Pending " : "Stop Stream"}
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 |
46 |
47 | setAmount(e.target.value)} className='pl-8 ' type='number' step="0.10" placeholder="0.00" />
48 |
49 |
50 | {pending ? "Transaction Pending..." : "Withdraw"}
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 |
53 |
54 | setAmount(e.target.value)} className='pl-4' type='number' step="0.10" placeholder="0.00" />
55 |
56 |
57 | {pending ? "Transaction Pending" : "Deposit"}
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 |
16 |
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 |
50 |
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 |
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 |
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 | Repay Debt
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 |
--------------------------------------------------------------------------------